@spaceflow/review 1.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.0.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@1.0.0...@spaceflow/review@2.0.0) (2026-04-13)
|
|
4
|
+
|
|
5
|
+
### 代码重构
|
|
6
|
+
|
|
7
|
+
* **review:** 优化 Example 标题和描述解析逻辑 ([decb54f](https://github.com/Lydanne/spaceflow/commit/decb54f6832acaeddd83ccfb6a3439c47294f11d))
|
|
8
|
+
|
|
9
|
+
### 其他修改
|
|
10
|
+
|
|
11
|
+
* **review-summary:** released version 1.0.0 [no ci] ([742d53e](https://github.com/Lydanne/spaceflow/commit/742d53efb7f16e33c50d9b1c4b9e31a7c0e8da21))
|
|
12
|
+
|
|
13
|
+
## [1.0.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.83.0...@spaceflow/review@1.0.0) (2026-04-13)
|
|
14
|
+
|
|
15
|
+
### ⚠ BREAKING CHANGES
|
|
16
|
+
|
|
17
|
+
* **review:** 重构 example 数据结构,支持 `### Example:` 分组和 `#### Good:/Bad:` 标题语法
|
|
18
|
+
|
|
19
|
+
### 新特性
|
|
20
|
+
|
|
21
|
+
* **review:** 新增 includes 变更类型前缀语法支持 ([2f5655f](https://github.com/Lydanne/spaceflow/commit/2f5655fc414828ea3a0269fba04611d2bf2591a9))
|
|
22
|
+
* **review:** 重构 example 数据结构,支持 `### Example:` 分组和 `#### Good:/Bad:` 标题语法 ([e45bd5a](https://github.com/Lydanne/spaceflow/commit/e45bd5aab5e317b8f70a8b107c17aab6548c3296))
|
|
23
|
+
|
|
24
|
+
### 其他修改
|
|
25
|
+
|
|
26
|
+
* **review-summary:** released version 0.52.0 [no ci] ([1d0cdac](https://github.com/Lydanne/spaceflow/commit/1d0cdacdbe12db98399e69fd53bbfdd23825cc10))
|
|
27
|
+
|
|
3
28
|
## [0.83.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.82.0...@spaceflow/review@0.83.0) (2026-04-10)
|
|
4
29
|
|
|
5
30
|
### 代码重构
|
package/dist/index.js
CHANGED
|
@@ -305,7 +305,7 @@ const CODE_BLOCK_TYPES = [
|
|
|
305
305
|
function formatExample(example) {
|
|
306
306
|
let text = "";
|
|
307
307
|
if (example.title) {
|
|
308
|
-
text += `##### ${example.title}\n`;
|
|
308
|
+
text += `##### Example: ${example.title}\n`;
|
|
309
309
|
}
|
|
310
310
|
if (example.description) {
|
|
311
311
|
text += `${example.description}\n`;
|
|
@@ -903,13 +903,18 @@ class ReviewSpecService {
|
|
|
903
903
|
for (const groupSection of groupSections){
|
|
904
904
|
const trimmedGroup = groupSection.trim();
|
|
905
905
|
if (!trimmedGroup) continue;
|
|
906
|
-
//
|
|
906
|
+
// 提取分组标题和描述,如 "### Example: 函数行数"
|
|
907
907
|
let exampleTitle = "";
|
|
908
908
|
let exampleDescription = "";
|
|
909
909
|
const groupMatch = trimmedGroup.match(/^###\s+Example\s*[::]\s*(.+)/i);
|
|
910
910
|
if (groupMatch) {
|
|
911
|
-
exampleTitle =
|
|
912
|
-
|
|
911
|
+
exampleTitle = groupMatch[1].trim();
|
|
912
|
+
// 提取标题行和第一个 #### 之间的文本作为 description
|
|
913
|
+
const afterTitle = trimmedGroup.slice(trimmedGroup.indexOf("\n")).trim();
|
|
914
|
+
const firstSubIdx = afterTitle.search(/(?:^|\n)####\s+/);
|
|
915
|
+
if (firstSubIdx > 0) {
|
|
916
|
+
exampleDescription = afterTitle.slice(0, firstSubIdx).trim();
|
|
917
|
+
}
|
|
913
918
|
}
|
|
914
919
|
// 在分组内按 #### 提取 Good/Bad
|
|
915
920
|
const ruleContents = [];
|
|
@@ -983,12 +988,12 @@ class ReviewSpecService {
|
|
|
983
988
|
* 如果 spec 没有 includes 配置,则保留该 spec 的所有 issues
|
|
984
989
|
* 支持 `added|`/`modified|`/`deleted|` 前缀语法
|
|
985
990
|
*/ filterIssuesByIncludes(issues, specs, changedFiles) {
|
|
986
|
-
// 构建
|
|
991
|
+
// 构建 rule.id -> includes 的映射(规则级优先,文件级兜底)
|
|
987
992
|
const specIncludesMap = new Map();
|
|
988
993
|
for (const spec of specs){
|
|
989
|
-
// 从规则 ID 前缀推断 spec filename
|
|
990
994
|
for (const rule of spec.rules){
|
|
991
|
-
|
|
995
|
+
// 规则级 includes 覆盖文件级 includes,与 severity 优先级一致
|
|
996
|
+
specIncludesMap.set(rule.id, rule.includes ?? spec.includes);
|
|
992
997
|
}
|
|
993
998
|
}
|
|
994
999
|
return issues.filter((issue)=>{
|
package/package.json
CHANGED
|
@@ -31,7 +31,7 @@ export function buildSpecsSection(specs: ReviewSpec[]): string {
|
|
|
31
31
|
function formatExample(example: RuleExample): string {
|
|
32
32
|
let text = "";
|
|
33
33
|
if (example.title) {
|
|
34
|
-
text += `##### ${example.title}\n`;
|
|
34
|
+
text += `##### Example: ${example.title}\n`;
|
|
35
35
|
}
|
|
36
36
|
if (example.description) {
|
|
37
37
|
text += `${example.description}\n`;
|
|
@@ -1674,8 +1674,8 @@ const bad_name = 1;
|
|
|
1674
1674
|
});
|
|
1675
1675
|
});
|
|
1676
1676
|
|
|
1677
|
-
describe("filterIssuesByIncludes -
|
|
1678
|
-
it("should use rule-level includes
|
|
1677
|
+
describe("filterIssuesByIncludes - includes priority", () => {
|
|
1678
|
+
it("should use rule-level includes over file-level includes", () => {
|
|
1679
1679
|
const specs = [
|
|
1680
1680
|
{
|
|
1681
1681
|
filename: "nest.md",
|
|
@@ -1706,12 +1706,301 @@ const bad_name = 1;
|
|
|
1706
1706
|
];
|
|
1707
1707
|
const issues = [
|
|
1708
1708
|
{ file: "user.model.ts", ruleId: "JsTs.Nest.Model" },
|
|
1709
|
+
{ file: "order.model.ts", ruleId: "JsTs.Nest.Model" },
|
|
1709
1710
|
{ file: "user.controller.ts", ruleId: "JsTs.Nest" },
|
|
1711
|
+
{ file: "order.controller.ts", ruleId: "JsTs.Nest" },
|
|
1712
|
+
{ file: "user.service.ts", ruleId: "JsTs.Nest" },
|
|
1713
|
+
{ file: "user.module.ts", ruleId: "JsTs.Nest" },
|
|
1714
|
+
{ file: "user.model.ts", ruleId: "JsTs.Nest" }, // model 文件但 ruleId 是 Nest(文件级)
|
|
1710
1715
|
];
|
|
1711
1716
|
const result = service.filterIssuesByIncludes(issues, specs);
|
|
1712
|
-
//
|
|
1713
|
-
|
|
1714
|
-
|
|
1717
|
+
// JsTs.Nest.Model → 规则级 *.model.ts,user.model.ts / order.model.ts 匹配 → 保留
|
|
1718
|
+
// JsTs.Nest → 文件级 *.controller.ts,user.controller.ts / order.controller.ts 匹配 → 保留
|
|
1719
|
+
// user.service.ts / user.module.ts 不匹配文件级 *.controller.ts → 过滤
|
|
1720
|
+
// user.model.ts(ruleId=Nest) → 文件级 *.controller.ts,不匹配 → 过滤
|
|
1721
|
+
expect(result).toHaveLength(4);
|
|
1722
|
+
expect(result.map((i) => i.file)).toEqual([
|
|
1723
|
+
"user.model.ts",
|
|
1724
|
+
"order.model.ts",
|
|
1725
|
+
"user.controller.ts",
|
|
1726
|
+
"order.controller.ts",
|
|
1727
|
+
]);
|
|
1728
|
+
});
|
|
1729
|
+
|
|
1730
|
+
it("should fall back to file-level includes when rule has no includes", () => {
|
|
1731
|
+
const specs = [
|
|
1732
|
+
{
|
|
1733
|
+
filename: "nest.md",
|
|
1734
|
+
extensions: ["ts"],
|
|
1735
|
+
type: "nest",
|
|
1736
|
+
content: "",
|
|
1737
|
+
overrides: [],
|
|
1738
|
+
severity: "error" as const,
|
|
1739
|
+
includes: ["*.controller.ts"],
|
|
1740
|
+
rules: [
|
|
1741
|
+
{
|
|
1742
|
+
id: "JsTs.Nest",
|
|
1743
|
+
title: "Nest",
|
|
1744
|
+
description: "",
|
|
1745
|
+
examples: [],
|
|
1746
|
+
overrides: [],
|
|
1747
|
+
},
|
|
1748
|
+
{
|
|
1749
|
+
id: "JsTs.Nest.DirStructure",
|
|
1750
|
+
title: "DirStructure",
|
|
1751
|
+
description: "",
|
|
1752
|
+
examples: [],
|
|
1753
|
+
overrides: [],
|
|
1754
|
+
// 无 rule.includes → 回退到 spec.includes
|
|
1755
|
+
},
|
|
1756
|
+
],
|
|
1757
|
+
},
|
|
1758
|
+
];
|
|
1759
|
+
const issues = [
|
|
1760
|
+
{ file: "user.controller.ts", ruleId: "JsTs.Nest.DirStructure" },
|
|
1761
|
+
{ file: "order.controller.ts", ruleId: "JsTs.Nest.DirStructure" },
|
|
1762
|
+
{ file: "user.service.ts", ruleId: "JsTs.Nest.DirStructure" },
|
|
1763
|
+
{ file: "user.module.ts", ruleId: "JsTs.Nest.DirStructure" },
|
|
1764
|
+
{ file: "user.controller.ts", ruleId: "JsTs.Nest" },
|
|
1765
|
+
{ file: "user.service.ts", ruleId: "JsTs.Nest" },
|
|
1766
|
+
];
|
|
1767
|
+
const result = service.filterIssuesByIncludes(issues, specs);
|
|
1768
|
+
// DirStructure 无规则级 includes → 回退文件级 *.controller.ts
|
|
1769
|
+
// user.controller.ts / order.controller.ts 匹配 → 保留
|
|
1770
|
+
// user.service.ts / user.module.ts 不匹配 → 过滤
|
|
1771
|
+
// JsTs.Nest 同样回退文件级,user.controller.ts 匹配 → 保留,user.service.ts 不匹配 → 过滤
|
|
1772
|
+
expect(result).toHaveLength(3);
|
|
1773
|
+
expect(result.map((i) => i.file)).toEqual([
|
|
1774
|
+
"user.controller.ts",
|
|
1775
|
+
"order.controller.ts",
|
|
1776
|
+
"user.controller.ts",
|
|
1777
|
+
]);
|
|
1778
|
+
});
|
|
1779
|
+
|
|
1780
|
+
it("should use rule-level includes even when it narrows the file-level scope", () => {
|
|
1781
|
+
const specs = [
|
|
1782
|
+
{
|
|
1783
|
+
filename: "js&ts.md",
|
|
1784
|
+
extensions: ["ts"],
|
|
1785
|
+
type: "base",
|
|
1786
|
+
content: "",
|
|
1787
|
+
overrides: [],
|
|
1788
|
+
severity: "error" as const,
|
|
1789
|
+
includes: ["*.ts"], // 文件级:所有 ts 文件
|
|
1790
|
+
rules: [
|
|
1791
|
+
{
|
|
1792
|
+
id: "JsTs.Base",
|
|
1793
|
+
title: "Base",
|
|
1794
|
+
description: "",
|
|
1795
|
+
examples: [],
|
|
1796
|
+
overrides: [],
|
|
1797
|
+
},
|
|
1798
|
+
{
|
|
1799
|
+
id: "JsTs.Base.TestRule",
|
|
1800
|
+
title: "TestRule",
|
|
1801
|
+
description: "",
|
|
1802
|
+
examples: [],
|
|
1803
|
+
overrides: [],
|
|
1804
|
+
includes: ["*.spec.ts"], // 规则级:仅 spec 文件(收窄)
|
|
1805
|
+
},
|
|
1806
|
+
],
|
|
1807
|
+
},
|
|
1808
|
+
];
|
|
1809
|
+
const issues = [
|
|
1810
|
+
{ file: "app.spec.ts", ruleId: "JsTs.Base.TestRule" },
|
|
1811
|
+
{ file: "utils.spec.ts", ruleId: "JsTs.Base.TestRule" },
|
|
1812
|
+
{ file: "app.ts", ruleId: "JsTs.Base.TestRule" },
|
|
1813
|
+
{ file: "utils.ts", ruleId: "JsTs.Base.TestRule" },
|
|
1814
|
+
{ file: "app.ts", ruleId: "JsTs.Base" },
|
|
1815
|
+
{ file: "utils.ts", ruleId: "JsTs.Base" },
|
|
1816
|
+
{ file: "app.spec.ts", ruleId: "JsTs.Base" },
|
|
1817
|
+
];
|
|
1818
|
+
const result = service.filterIssuesByIncludes(issues, specs);
|
|
1819
|
+
// JsTs.Base.TestRule → 规则级 *.spec.ts,app.spec.ts / utils.spec.ts 匹配 → 保留
|
|
1820
|
+
// app.ts / utils.ts 不匹配 *.spec.ts → 过滤
|
|
1821
|
+
// JsTs.Base → 文件级 *.ts,app.ts / utils.ts / app.spec.ts 匹配 → 保留
|
|
1822
|
+
expect(result).toHaveLength(5);
|
|
1823
|
+
expect(result.map((i) => i.file)).toEqual([
|
|
1824
|
+
"app.spec.ts",
|
|
1825
|
+
"utils.spec.ts",
|
|
1826
|
+
"app.ts",
|
|
1827
|
+
"utils.ts",
|
|
1828
|
+
"app.spec.ts",
|
|
1829
|
+
]);
|
|
1830
|
+
});
|
|
1831
|
+
|
|
1832
|
+
it("should use rule-level includes even when it widens the file-level scope", () => {
|
|
1833
|
+
const specs = [
|
|
1834
|
+
{
|
|
1835
|
+
filename: "nest.md",
|
|
1836
|
+
extensions: ["ts"],
|
|
1837
|
+
type: "nest",
|
|
1838
|
+
content: "",
|
|
1839
|
+
overrides: [],
|
|
1840
|
+
severity: "error" as const,
|
|
1841
|
+
includes: ["*.controller.ts"], // 文件级:仅 controller
|
|
1842
|
+
rules: [
|
|
1843
|
+
{
|
|
1844
|
+
id: "JsTs.Nest",
|
|
1845
|
+
title: "Nest",
|
|
1846
|
+
description: "",
|
|
1847
|
+
examples: [],
|
|
1848
|
+
overrides: [],
|
|
1849
|
+
},
|
|
1850
|
+
{
|
|
1851
|
+
id: "JsTs.Nest.AllFiles",
|
|
1852
|
+
title: "AllFiles",
|
|
1853
|
+
description: "",
|
|
1854
|
+
examples: [],
|
|
1855
|
+
overrides: [],
|
|
1856
|
+
includes: ["*.ts"], // 规则级:所有 ts 文件(放宽)
|
|
1857
|
+
},
|
|
1858
|
+
],
|
|
1859
|
+
},
|
|
1860
|
+
];
|
|
1861
|
+
const issues = [
|
|
1862
|
+
{ file: "user.service.ts", ruleId: "JsTs.Nest.AllFiles" },
|
|
1863
|
+
{ file: "user.controller.ts", ruleId: "JsTs.Nest.AllFiles" },
|
|
1864
|
+
{ file: "user.module.ts", ruleId: "JsTs.Nest.AllFiles" },
|
|
1865
|
+
{ file: "user.controller.ts", ruleId: "JsTs.Nest" },
|
|
1866
|
+
{ file: "order.controller.ts", ruleId: "JsTs.Nest" },
|
|
1867
|
+
{ file: "user.service.ts", ruleId: "JsTs.Nest" },
|
|
1868
|
+
{ file: "user.module.ts", ruleId: "JsTs.Nest" },
|
|
1869
|
+
];
|
|
1870
|
+
const result = service.filterIssuesByIncludes(issues, specs);
|
|
1871
|
+
// JsTs.Nest.AllFiles → 规则级 *.ts,全部匹配 → 保留 3 个
|
|
1872
|
+
// JsTs.Nest → 文件级 *.controller.ts,user.controller.ts / order.controller.ts 匹配 → 保留
|
|
1873
|
+
// user.service.ts / user.module.ts 不匹配 → 过滤
|
|
1874
|
+
expect(result).toHaveLength(5);
|
|
1875
|
+
expect(result.map((i) => i.file)).toEqual([
|
|
1876
|
+
"user.service.ts",
|
|
1877
|
+
"user.controller.ts",
|
|
1878
|
+
"user.module.ts",
|
|
1879
|
+
"user.controller.ts",
|
|
1880
|
+
"order.controller.ts",
|
|
1881
|
+
]);
|
|
1882
|
+
});
|
|
1883
|
+
|
|
1884
|
+
it("should support status prefix in rule-level includes", () => {
|
|
1885
|
+
const specs = [
|
|
1886
|
+
{
|
|
1887
|
+
filename: "nest.md",
|
|
1888
|
+
extensions: ["ts"],
|
|
1889
|
+
type: "nest",
|
|
1890
|
+
content: "",
|
|
1891
|
+
overrides: [],
|
|
1892
|
+
severity: "error" as const,
|
|
1893
|
+
includes: ["*.controller.ts"], // 文件级:无前缀
|
|
1894
|
+
rules: [
|
|
1895
|
+
{
|
|
1896
|
+
id: "JsTs.Nest",
|
|
1897
|
+
title: "Nest",
|
|
1898
|
+
description: "",
|
|
1899
|
+
examples: [],
|
|
1900
|
+
overrides: [],
|
|
1901
|
+
},
|
|
1902
|
+
{
|
|
1903
|
+
id: "JsTs.Nest.Model",
|
|
1904
|
+
title: "Model",
|
|
1905
|
+
description: "",
|
|
1906
|
+
examples: [],
|
|
1907
|
+
overrides: [],
|
|
1908
|
+
includes: ["added|*.model.ts", "modified|*.dto.ts"], // 规则级:added 的 model + modified 的 dto
|
|
1909
|
+
},
|
|
1910
|
+
],
|
|
1911
|
+
},
|
|
1912
|
+
];
|
|
1913
|
+
const changedFiles = {
|
|
1914
|
+
getStatus: (file: string) => {
|
|
1915
|
+
if (file === "user.model.ts") return "added";
|
|
1916
|
+
if (file === "order.model.ts") return "modified";
|
|
1917
|
+
if (file === "user.dto.ts") return "modified";
|
|
1918
|
+
if (file === "order.dto.ts") return "added";
|
|
1919
|
+
return "modified";
|
|
1920
|
+
},
|
|
1921
|
+
} as any;
|
|
1922
|
+
const issues = [
|
|
1923
|
+
{ file: "user.model.ts", ruleId: "JsTs.Nest.Model" }, // added + *.model.ts → 匹配
|
|
1924
|
+
{ file: "order.model.ts", ruleId: "JsTs.Nest.Model" }, // modified + *.model.ts → 不匹配 added|
|
|
1925
|
+
{ file: "user.dto.ts", ruleId: "JsTs.Nest.Model" }, // modified + *.dto.ts → 匹配 modified|
|
|
1926
|
+
{ file: "order.dto.ts", ruleId: "JsTs.Nest.Model" }, // added + *.dto.ts → 不匹配 modified|
|
|
1927
|
+
{ file: "user.controller.ts", ruleId: "JsTs.Nest" }, // 文件级 *.controller.ts → 匹配
|
|
1928
|
+
{ file: "user.service.ts", ruleId: "JsTs.Nest" }, // 文件级 *.controller.ts → 不匹配
|
|
1929
|
+
];
|
|
1930
|
+
const result = service.filterIssuesByIncludes(issues, specs, changedFiles);
|
|
1931
|
+
expect(result).toHaveLength(3);
|
|
1932
|
+
expect(result.map((i) => i.file)).toEqual([
|
|
1933
|
+
"user.model.ts",
|
|
1934
|
+
"user.dto.ts",
|
|
1935
|
+
"user.controller.ts",
|
|
1936
|
+
]);
|
|
1937
|
+
|
|
1938
|
+
// 全部 modified 时,added|*.model.ts 不匹配,modified|*.dto.ts 匹配
|
|
1939
|
+
const changedFiles2 = {
|
|
1940
|
+
getStatus: () => "modified",
|
|
1941
|
+
} as any;
|
|
1942
|
+
const result2 = service.filterIssuesByIncludes(issues, specs, changedFiles2);
|
|
1943
|
+
// user.model.ts: modified, added|*.model.ts 不匹配 → 过滤
|
|
1944
|
+
// order.model.ts: modified, added|*.model.ts 不匹配 → 过滤
|
|
1945
|
+
// user.dto.ts: modified, modified|*.dto.ts 匹配 → 保留
|
|
1946
|
+
// order.dto.ts: modified, modified|*.dto.ts 匹配 → 保留
|
|
1947
|
+
// user.controller.ts: 文件级匹配 → 保留
|
|
1948
|
+
// user.service.ts: 文件级不匹配 → 过滤
|
|
1949
|
+
expect(result2).toHaveLength(3);
|
|
1950
|
+
expect(result2.map((i) => i.file)).toEqual([
|
|
1951
|
+
"user.dto.ts",
|
|
1952
|
+
"order.dto.ts",
|
|
1953
|
+
"user.controller.ts",
|
|
1954
|
+
]);
|
|
1955
|
+
});
|
|
1956
|
+
|
|
1957
|
+
it("should use empty rule-level includes to remove file-level restriction", () => {
|
|
1958
|
+
const specs = [
|
|
1959
|
+
{
|
|
1960
|
+
filename: "nest.md",
|
|
1961
|
+
extensions: ["ts"],
|
|
1962
|
+
type: "nest",
|
|
1963
|
+
content: "",
|
|
1964
|
+
overrides: [],
|
|
1965
|
+
severity: "error" as const,
|
|
1966
|
+
includes: ["*.controller.ts"], // 文件级:仅 controller
|
|
1967
|
+
rules: [
|
|
1968
|
+
{
|
|
1969
|
+
id: "JsTs.Nest",
|
|
1970
|
+
title: "Nest",
|
|
1971
|
+
description: "",
|
|
1972
|
+
examples: [],
|
|
1973
|
+
overrides: [],
|
|
1974
|
+
},
|
|
1975
|
+
{
|
|
1976
|
+
id: "JsTs.Nest.Global",
|
|
1977
|
+
title: "Global",
|
|
1978
|
+
description: "",
|
|
1979
|
+
examples: [],
|
|
1980
|
+
overrides: [],
|
|
1981
|
+
includes: [], // 规则级:空数组 = 无限制(覆盖文件级)
|
|
1982
|
+
},
|
|
1983
|
+
],
|
|
1984
|
+
},
|
|
1985
|
+
];
|
|
1986
|
+
const issues = [
|
|
1987
|
+
{ file: "user.service.ts", ruleId: "JsTs.Nest.Global" },
|
|
1988
|
+
{ file: "user.controller.ts", ruleId: "JsTs.Nest.Global" },
|
|
1989
|
+
{ file: "user.module.ts", ruleId: "JsTs.Nest.Global" },
|
|
1990
|
+
{ file: "user.service.ts", ruleId: "JsTs.Nest" },
|
|
1991
|
+
{ file: "user.controller.ts", ruleId: "JsTs.Nest" },
|
|
1992
|
+
{ file: "user.module.ts", ruleId: "JsTs.Nest" },
|
|
1993
|
+
];
|
|
1994
|
+
const result = service.filterIssuesByIncludes(issues, specs);
|
|
1995
|
+
// JsTs.Nest.Global → 规则级 includes=[] (空=无限制),全部保留 3 个
|
|
1996
|
+
// JsTs.Nest → 文件级 *.controller.ts,仅 user.controller.ts 匹配 → 保留
|
|
1997
|
+
expect(result).toHaveLength(4);
|
|
1998
|
+
expect(result.map((i) => i.file)).toEqual([
|
|
1999
|
+
"user.service.ts",
|
|
2000
|
+
"user.controller.ts",
|
|
2001
|
+
"user.module.ts",
|
|
2002
|
+
"user.controller.ts",
|
|
2003
|
+
]);
|
|
1715
2004
|
});
|
|
1716
2005
|
});
|
|
1717
2006
|
|
|
@@ -1821,18 +2110,55 @@ const statusActive = "active";
|
|
|
1821
2110
|
expect(spec!.rules[1].examples).toHaveLength(2);
|
|
1822
2111
|
|
|
1823
2112
|
// 第一组
|
|
1824
|
-
expect(spec!.rules[1].examples[0].title).toBe("
|
|
1825
|
-
expect(spec!.rules[1].examples[0].description).toBe("
|
|
2113
|
+
expect(spec!.rules[1].examples[0].title).toBe("下面的明明规则说明");
|
|
2114
|
+
expect(spec!.rules[1].examples[0].description).toBe("");
|
|
1826
2115
|
expect(spec!.rules[1].examples[0].content).toHaveLength(2);
|
|
1827
2116
|
expect(spec!.rules[1].examples[0].content[0].type).toBe("good");
|
|
1828
2117
|
expect(spec!.rules[1].examples[0].content[1].type).toBe("bad");
|
|
1829
2118
|
|
|
1830
2119
|
// 第二组
|
|
1831
|
-
expect(spec!.rules[1].examples[1].title).toBe("
|
|
1832
|
-
expect(spec!.rules[1].examples[1].description).toBe("
|
|
2120
|
+
expect(spec!.rules[1].examples[1].title).toBe("另一种场景");
|
|
2121
|
+
expect(spec!.rules[1].examples[1].description).toBe("");
|
|
1833
2122
|
expect(spec!.rules[1].examples[1].content).toHaveLength(2);
|
|
1834
2123
|
expect(spec!.rules[1].examples[1].content[0].type).toBe("good");
|
|
1835
2124
|
expect(spec!.rules[1].examples[1].content[1].type).toBe("bad");
|
|
1836
2125
|
});
|
|
2126
|
+
|
|
2127
|
+
it("should parse Example with title and description paragraph", () => {
|
|
2128
|
+
const mockContent = `# 基础代码规范 \`[JsTs.Base]\`
|
|
2129
|
+
|
|
2130
|
+
## 函数行数限制 \`[JsTs.Base.FuncLines]\`
|
|
2131
|
+
|
|
2132
|
+
- 函数不能太长
|
|
2133
|
+
|
|
2134
|
+
### Example: 函数行数
|
|
2135
|
+
|
|
2136
|
+
例子说明 desc
|
|
2137
|
+
|
|
2138
|
+
#### Good: 函数不超过 200 行
|
|
2139
|
+
|
|
2140
|
+
\`\`\`javascript
|
|
2141
|
+
function getUserInfo() {
|
|
2142
|
+
// ... 小于等于 200
|
|
2143
|
+
}
|
|
2144
|
+
\`\`\`
|
|
2145
|
+
|
|
2146
|
+
#### Bad: 函数超过 200 行
|
|
2147
|
+
|
|
2148
|
+
\`\`\`javascript
|
|
2149
|
+
function getUserInfo() {
|
|
2150
|
+
// ... 大于 200
|
|
2151
|
+
}
|
|
2152
|
+
\`\`\``;
|
|
2153
|
+
|
|
2154
|
+
const spec = service.parseSpecFile("js&ts.base.md", mockContent);
|
|
2155
|
+
expect(spec).not.toBeNull();
|
|
2156
|
+
expect(spec!.rules[1].examples).toHaveLength(1);
|
|
2157
|
+
expect(spec!.rules[1].examples[0].title).toBe("函数行数");
|
|
2158
|
+
expect(spec!.rules[1].examples[0].description).toBe("例子说明 desc");
|
|
2159
|
+
expect(spec!.rules[1].examples[0].content).toHaveLength(2);
|
|
2160
|
+
expect(spec!.rules[1].examples[0].content[0].type).toBe("good");
|
|
2161
|
+
expect(spec!.rules[1].examples[0].content[0].title).toBe("函数不超过 200 行");
|
|
2162
|
+
});
|
|
1837
2163
|
});
|
|
1838
2164
|
});
|
|
@@ -694,13 +694,18 @@ export class ReviewSpecService {
|
|
|
694
694
|
const trimmedGroup = groupSection.trim();
|
|
695
695
|
if (!trimmedGroup) continue;
|
|
696
696
|
|
|
697
|
-
//
|
|
697
|
+
// 提取分组标题和描述,如 "### Example: 函数行数"
|
|
698
698
|
let exampleTitle = "";
|
|
699
699
|
let exampleDescription = "";
|
|
700
700
|
const groupMatch = trimmedGroup.match(/^###\s+Example\s*[::]\s*(.+)/i);
|
|
701
701
|
if (groupMatch) {
|
|
702
|
-
exampleTitle =
|
|
703
|
-
|
|
702
|
+
exampleTitle = groupMatch[1].trim();
|
|
703
|
+
// 提取标题行和第一个 #### 之间的文本作为 description
|
|
704
|
+
const afterTitle = trimmedGroup.slice(trimmedGroup.indexOf("\n")).trim();
|
|
705
|
+
const firstSubIdx = afterTitle.search(/(?:^|\n)####\s+/);
|
|
706
|
+
if (firstSubIdx > 0) {
|
|
707
|
+
exampleDescription = afterTitle.slice(0, firstSubIdx).trim();
|
|
708
|
+
}
|
|
704
709
|
}
|
|
705
710
|
|
|
706
711
|
// 在分组内按 #### 提取 Good/Bad
|
|
@@ -794,12 +799,12 @@ export class ReviewSpecService {
|
|
|
794
799
|
specs: ReviewSpec[],
|
|
795
800
|
changedFiles?: ChangedFileCollection,
|
|
796
801
|
): T[] {
|
|
797
|
-
// 构建
|
|
802
|
+
// 构建 rule.id -> includes 的映射(规则级优先,文件级兜底)
|
|
798
803
|
const specIncludesMap = new Map<string, string[]>();
|
|
799
804
|
for (const spec of specs) {
|
|
800
|
-
// 从规则 ID 前缀推断 spec filename
|
|
801
805
|
for (const rule of spec.rules) {
|
|
802
|
-
|
|
806
|
+
// 规则级 includes 覆盖文件级 includes,与 severity 优先级一致
|
|
807
|
+
specIncludesMap.set(rule.id, rule.includes ?? spec.includes);
|
|
803
808
|
}
|
|
804
809
|
}
|
|
805
810
|
|