frontend-guardian-core 2.6.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.
Files changed (152) hide show
  1. package/LICENSE +21 -0
  2. package/bin/fg-core.js +1238 -0
  3. package/bin/watch-mode.js +123 -0
  4. package/dist/engine/cache.d.ts +68 -0
  5. package/dist/engine/cache.d.ts.map +1 -0
  6. package/dist/engine/cache.js +164 -0
  7. package/dist/engine/cache.js.map +1 -0
  8. package/dist/engine/rule-engine.d.ts +135 -0
  9. package/dist/engine/rule-engine.d.ts.map +1 -0
  10. package/dist/engine/rule-engine.js +716 -0
  11. package/dist/engine/rule-engine.js.map +1 -0
  12. package/dist/formatters/github-annotation.d.ts +36 -0
  13. package/dist/formatters/github-annotation.d.ts.map +1 -0
  14. package/dist/formatters/github-annotation.js +122 -0
  15. package/dist/formatters/github-annotation.js.map +1 -0
  16. package/dist/formatters/pr-comment.d.ts +43 -0
  17. package/dist/formatters/pr-comment.d.ts.map +1 -0
  18. package/dist/formatters/pr-comment.js +171 -0
  19. package/dist/formatters/pr-comment.js.map +1 -0
  20. package/dist/formatters/sarif.d.ts +104 -0
  21. package/dist/formatters/sarif.d.ts.map +1 -0
  22. package/dist/formatters/sarif.js +130 -0
  23. package/dist/formatters/sarif.js.map +1 -0
  24. package/dist/index.d.ts +46 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +108 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/integrations/base.d.ts +44 -0
  29. package/dist/integrations/base.d.ts.map +1 -0
  30. package/dist/integrations/base.js +104 -0
  31. package/dist/integrations/base.js.map +1 -0
  32. package/dist/integrations/eslint.d.ts +8 -0
  33. package/dist/integrations/eslint.d.ts.map +1 -0
  34. package/dist/integrations/eslint.js +67 -0
  35. package/dist/integrations/eslint.js.map +1 -0
  36. package/dist/integrations/formatter.d.ts +35 -0
  37. package/dist/integrations/formatter.d.ts.map +1 -0
  38. package/dist/integrations/formatter.js +182 -0
  39. package/dist/integrations/formatter.js.map +1 -0
  40. package/dist/integrations/index.d.ts +17 -0
  41. package/dist/integrations/index.d.ts.map +1 -0
  42. package/dist/integrations/index.js +25 -0
  43. package/dist/integrations/index.js.map +1 -0
  44. package/dist/integrations/stylelint.d.ts +8 -0
  45. package/dist/integrations/stylelint.d.ts.map +1 -0
  46. package/dist/integrations/stylelint.js +59 -0
  47. package/dist/integrations/stylelint.js.map +1 -0
  48. package/dist/integrations/typescript.d.ts +8 -0
  49. package/dist/integrations/typescript.d.ts.map +1 -0
  50. package/dist/integrations/typescript.js +92 -0
  51. package/dist/integrations/typescript.js.map +1 -0
  52. package/dist/rules/registry.d.ts +83 -0
  53. package/dist/rules/registry.d.ts.map +1 -0
  54. package/dist/rules/registry.js +205 -0
  55. package/dist/rules/registry.js.map +1 -0
  56. package/dist/scanners/a11y-scanner.d.ts +14 -0
  57. package/dist/scanners/a11y-scanner.d.ts.map +1 -0
  58. package/dist/scanners/a11y-scanner.js +781 -0
  59. package/dist/scanners/a11y-scanner.js.map +1 -0
  60. package/dist/scanners/component-scanner.d.ts +12 -0
  61. package/dist/scanners/component-scanner.d.ts.map +1 -0
  62. package/dist/scanners/component-scanner.js +304 -0
  63. package/dist/scanners/component-scanner.js.map +1 -0
  64. package/dist/scanners/cross-file-scanner.d.ts +18 -0
  65. package/dist/scanners/cross-file-scanner.d.ts.map +1 -0
  66. package/dist/scanners/cross-file-scanner.js +684 -0
  67. package/dist/scanners/cross-file-scanner.js.map +1 -0
  68. package/dist/scanners/hooks-scanner.d.ts +15 -0
  69. package/dist/scanners/hooks-scanner.d.ts.map +1 -0
  70. package/dist/scanners/hooks-scanner.js +670 -0
  71. package/dist/scanners/hooks-scanner.js.map +1 -0
  72. package/dist/scanners/i18n-scanner.d.ts +13 -0
  73. package/dist/scanners/i18n-scanner.d.ts.map +1 -0
  74. package/dist/scanners/i18n-scanner.js +535 -0
  75. package/dist/scanners/i18n-scanner.js.map +1 -0
  76. package/dist/scanners/naming-scanner.d.ts +19 -0
  77. package/dist/scanners/naming-scanner.d.ts.map +1 -0
  78. package/dist/scanners/naming-scanner.js +746 -0
  79. package/dist/scanners/naming-scanner.js.map +1 -0
  80. package/dist/scanners/performance-scanner.d.ts +7 -0
  81. package/dist/scanners/performance-scanner.d.ts.map +1 -0
  82. package/dist/scanners/performance-scanner.js +402 -0
  83. package/dist/scanners/performance-scanner.js.map +1 -0
  84. package/dist/scanners/platform-scanner.d.ts +15 -0
  85. package/dist/scanners/platform-scanner.d.ts.map +1 -0
  86. package/dist/scanners/platform-scanner.js +320 -0
  87. package/dist/scanners/platform-scanner.js.map +1 -0
  88. package/dist/scanners/security-scanner.d.ts +7 -0
  89. package/dist/scanners/security-scanner.d.ts.map +1 -0
  90. package/dist/scanners/security-scanner.js +349 -0
  91. package/dist/scanners/security-scanner.js.map +1 -0
  92. package/dist/scanners/svelte-scanner.d.ts +14 -0
  93. package/dist/scanners/svelte-scanner.d.ts.map +1 -0
  94. package/dist/scanners/svelte-scanner.js +228 -0
  95. package/dist/scanners/svelte-scanner.js.map +1 -0
  96. package/dist/types.d.ts +343 -0
  97. package/dist/types.d.ts.map +1 -0
  98. package/dist/types.js +6 -0
  99. package/dist/types.js.map +1 -0
  100. package/dist/utils/ast-parser.d.ts +21 -0
  101. package/dist/utils/ast-parser.d.ts.map +1 -0
  102. package/dist/utils/ast-parser.js +119 -0
  103. package/dist/utils/ast-parser.js.map +1 -0
  104. package/dist/utils/baseline.d.ts +89 -0
  105. package/dist/utils/baseline.d.ts.map +1 -0
  106. package/dist/utils/baseline.js +156 -0
  107. package/dist/utils/baseline.js.map +1 -0
  108. package/dist/utils/ci-generator.d.ts +34 -0
  109. package/dist/utils/ci-generator.d.ts.map +1 -0
  110. package/dist/utils/ci-generator.js +194 -0
  111. package/dist/utils/ci-generator.js.map +1 -0
  112. package/dist/utils/common.d.ts +8 -0
  113. package/dist/utils/common.d.ts.map +1 -0
  114. package/dist/utils/common.js +38 -0
  115. package/dist/utils/common.js.map +1 -0
  116. package/dist/utils/concurrent.d.ts +16 -0
  117. package/dist/utils/concurrent.d.ts.map +1 -0
  118. package/dist/utils/concurrent.js +49 -0
  119. package/dist/utils/concurrent.js.map +1 -0
  120. package/dist/utils/config-loader.d.ts +8 -0
  121. package/dist/utils/config-loader.d.ts.map +1 -0
  122. package/dist/utils/config-loader.js +154 -0
  123. package/dist/utils/config-loader.js.map +1 -0
  124. package/dist/utils/fix-bot.d.ts +36 -0
  125. package/dist/utils/fix-bot.d.ts.map +1 -0
  126. package/dist/utils/fix-bot.js +274 -0
  127. package/dist/utils/fix-bot.js.map +1 -0
  128. package/dist/utils/git-hooks.d.ts +55 -0
  129. package/dist/utils/git-hooks.d.ts.map +1 -0
  130. package/dist/utils/git-hooks.js +318 -0
  131. package/dist/utils/git-hooks.js.map +1 -0
  132. package/dist/utils/history-report.d.ts +72 -0
  133. package/dist/utils/history-report.d.ts.map +1 -0
  134. package/dist/utils/history-report.js +144 -0
  135. package/dist/utils/history-report.js.map +1 -0
  136. package/dist/utils/init-config.d.ts +23 -0
  137. package/dist/utils/init-config.d.ts.map +1 -0
  138. package/dist/utils/init-config.js +146 -0
  139. package/dist/utils/init-config.js.map +1 -0
  140. package/dist/utils/pr-publisher.d.ts +64 -0
  141. package/dist/utils/pr-publisher.d.ts.map +1 -0
  142. package/dist/utils/pr-publisher.js +265 -0
  143. package/dist/utils/pr-publisher.js.map +1 -0
  144. package/dist/utils/project-detector.d.ts +20 -0
  145. package/dist/utils/project-detector.d.ts.map +1 -0
  146. package/dist/utils/project-detector.js +342 -0
  147. package/dist/utils/project-detector.js.map +1 -0
  148. package/dist/utils/report-uploader.d.ts +35 -0
  149. package/dist/utils/report-uploader.d.ts.map +1 -0
  150. package/dist/utils/report-uploader.js +106 -0
  151. package/dist/utils/report-uploader.js.map +1 -0
  152. package/package.json +78 -0
@@ -0,0 +1,746 @@
1
+ "use strict";
2
+ /**
3
+ * 命名规范 Scanner
4
+ *
5
+ * 检测范围:
6
+ * 1. class 名 — PascalCase
7
+ * 2. interface 名 — PascalCase
8
+ * 3. type 别名 — PascalCase
9
+ * 4. enum 名 — PascalCase, enum 成员 — UPPER_SNAKE_CASE
10
+ * 5. function/method — camelCase(React 组件允许 PascalCase)
11
+ * 6. variable (let/var) — camelCase
12
+ * 7. constant (const + 字面量) — UPPER_SNAKE_CASE
13
+ * 8. 私有成员前缀 — _ (JS) 或 # (TS private)
14
+ * 9. 文件名 — kebab-case 推荐
15
+ * 10. 文件夹名 — kebab-case 推荐
16
+ */
17
+ var __importDefault = (this && this.__importDefault) || function (mod) {
18
+ return (mod && mod.__esModule) ? mod : { "default": mod };
19
+ };
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.namingRules = void 0;
22
+ const traverse_1 = __importDefault(require("@babel/traverse"));
23
+ const node_path_1 = require("node:path");
24
+ const common_js_1 = require("../utils/common.js");
25
+ /** 默认命名规范配置 */
26
+ const DEFAULT_NAMING = {
27
+ classCase: "PascalCase",
28
+ interfaceCase: "PascalCase",
29
+ typeAliasCase: "PascalCase",
30
+ functionCase: "camelCase",
31
+ variableCase: "camelCase",
32
+ constantCase: "UPPER_SNAKE_CASE",
33
+ enumCase: "PascalCase",
34
+ enumMemberCase: "UPPER_SNAKE_CASE",
35
+ privatePrefix: "underscore",
36
+ fileNameCase: "kebab-case",
37
+ folderNameCase: "kebab-case",
38
+ allowSingleLetter: true,
39
+ allowPascalCaseComponents: true,
40
+ ignorePatterns: [],
41
+ };
42
+ /** 命名规范规则 */
43
+ exports.namingRules = [
44
+ {
45
+ id: "naming-class",
46
+ name: "类名应使用 PascalCase",
47
+ description: "class 声明应使用 PascalCase 命名",
48
+ severity: "warning",
49
+ category: "style",
50
+ defaultEnabled: true,
51
+ docsUrl: "https://github.com/wzm111/frontend-guardian/blob/main/docs/rules/naming-class.md",
52
+ execute(context) {
53
+ return checkIdentifierCase(context, "ClassDeclaration", "class", "PascalCase");
54
+ },
55
+ },
56
+ {
57
+ id: "naming-interface",
58
+ name: "接口名应使用 PascalCase",
59
+ description: "TS interface 声明应使用 PascalCase 命名",
60
+ severity: "warning",
61
+ category: "style",
62
+ defaultEnabled: true,
63
+ docsUrl: "https://github.com/wzm111/frontend-guardian/blob/main/docs/rules/naming-interface.md",
64
+ execute(context) {
65
+ return checkIdentifierCase(context, "TSInterfaceDeclaration", "interface", "PascalCase");
66
+ },
67
+ },
68
+ {
69
+ id: "naming-type-alias",
70
+ name: "类型别名应使用 PascalCase",
71
+ description: "type 别名声明应使用 PascalCase 命名",
72
+ severity: "warning",
73
+ category: "style",
74
+ defaultEnabled: true,
75
+ docsUrl: "https://github.com/wzm111/frontend-guardian/blob/main/docs/rules/naming-type-alias.md",
76
+ execute(context) {
77
+ return checkIdentifierCase(context, "TSTypeAliasDeclaration", "type alias", "PascalCase");
78
+ },
79
+ },
80
+ {
81
+ id: "naming-enum",
82
+ name: "枚举名应使用 PascalCase,成员应使用 UPPER_SNAKE_CASE",
83
+ description: "enum 声明使用 PascalCase,成员使用 UPPER_SNAKE_CASE",
84
+ severity: "warning",
85
+ category: "style",
86
+ defaultEnabled: true,
87
+ docsUrl: "https://github.com/wzm111/frontend-guardian/blob/main/docs/rules/naming-enum.md",
88
+ execute(context) {
89
+ const issues = [];
90
+ const config = getNamingConfig(context);
91
+ const ast = context.utils.parseAST(context.source, {
92
+ ext: (0, common_js_1.getFileExt)(context.filePath),
93
+ });
94
+ if (!ast)
95
+ return issues;
96
+ (0, traverse_1.default)(ast, {
97
+ TSEnumDeclaration(path) {
98
+ const name = path.node.id.name;
99
+ const { line, column } = path.node.loc?.start || { line: 0, column: 0 };
100
+ // 检查枚举名
101
+ if (!isPascalCase(name)) {
102
+ issues.push({
103
+ ruleId: "naming-enum",
104
+ title: `枚举名 "${name}" 应使用 PascalCase`,
105
+ description: `枚举声明应使用 PascalCase,建议改为 "${toPascalCase(name)}"`,
106
+ severity: "warning",
107
+ file: context.filePath,
108
+ line,
109
+ column,
110
+ source: `enum ${name}`,
111
+ fix: {
112
+ text: toPascalCase(name),
113
+ start: { line, column: column + 5 }, // after 'enum '
114
+ end: { line, column: column + 5 + name.length },
115
+ },
116
+ });
117
+ }
118
+ // 检查枚举成员
119
+ for (const member of path.node.members) {
120
+ if (member.type !== "TSEnumMember")
121
+ continue;
122
+ const memberName = member.id.type === "Identifier"
123
+ ? member.id.name
124
+ : member.id.type === "StringLiteral"
125
+ ? member.id.value
126
+ : null;
127
+ if (!memberName)
128
+ continue;
129
+ const { line: mLine, column: mColumn } = member.loc?.start || { line: 0, column: 0 };
130
+ if (config.enumMemberCase === "UPPER_SNAKE_CASE" && !isUpperSnakeCase(memberName)) {
131
+ issues.push({
132
+ ruleId: "naming-enum",
133
+ title: `枚举成员 "${memberName}" 应使用 UPPER_SNAKE_CASE`,
134
+ description: `枚举成员建议使用全大写下划线分隔,如 "${toUpperSnakeCase(memberName)}"`,
135
+ severity: "suggestion",
136
+ file: context.filePath,
137
+ line: mLine,
138
+ column: mColumn,
139
+ source: memberName,
140
+ });
141
+ }
142
+ }
143
+ },
144
+ });
145
+ return issues;
146
+ },
147
+ },
148
+ {
149
+ id: "naming-function",
150
+ name: "函数/方法应使用 camelCase",
151
+ description: "普通函数使用 camelCase,React 组件允许 PascalCase",
152
+ severity: "warning",
153
+ category: "style",
154
+ defaultEnabled: true,
155
+ docsUrl: "https://github.com/wzm111/frontend-guardian/blob/main/docs/rules/naming-function.md",
156
+ execute(context) {
157
+ const issues = [];
158
+ const config = getNamingConfig(context);
159
+ const ast = context.utils.parseAST(context.source, {
160
+ ext: (0, common_js_1.getFileExt)(context.filePath),
161
+ });
162
+ if (!ast)
163
+ return issues;
164
+ (0, traverse_1.default)(ast, {
165
+ // 函数声明: function foo() {}
166
+ FunctionDeclaration(path) {
167
+ const name = path.node.id?.name;
168
+ if (!name)
169
+ return;
170
+ if (shouldIgnoreName(name, config))
171
+ return;
172
+ const { line, column } = path.node.loc?.start || { line: 0, column: 0 };
173
+ // 检测是否是 React 组件(返回 JSX 或 JSXElement)
174
+ const isComponent = config.allowPascalCaseComponents && isLikelyReactComponent(path.node, name);
175
+ if (isComponent) {
176
+ // React 组件允许 PascalCase
177
+ if (!isPascalCase(name)) {
178
+ issues.push({
179
+ ruleId: "naming-function",
180
+ title: `React 组件 "${name}" 应使用 PascalCase`,
181
+ description: `React 组件名应使用 PascalCase,建议改为 "${toPascalCase(name)}"`,
182
+ severity: "warning",
183
+ file: context.filePath,
184
+ line,
185
+ column,
186
+ source: `function ${name}`,
187
+ });
188
+ }
189
+ }
190
+ else {
191
+ // 普通函数用 camelCase
192
+ if (!isCamelCase(name)) {
193
+ issues.push({
194
+ ruleId: "naming-function",
195
+ title: `函数名 "${name}" 应使用 camelCase`,
196
+ description: `普通函数应使用 camelCase,建议改为 "${toCamelCase(name)}"`,
197
+ severity: "warning",
198
+ file: context.filePath,
199
+ line,
200
+ column,
201
+ source: `function ${name}`,
202
+ fix: {
203
+ text: toCamelCase(name),
204
+ start: { line, column: column + 9 },
205
+ end: { line, column: column + 9 + name.length },
206
+ },
207
+ });
208
+ }
209
+ }
210
+ },
211
+ // 方法定义: class Foo { bar() {} }
212
+ ClassMethod(path) {
213
+ const name = path.node.key.type === "Identifier" ? path.node.key.name : null;
214
+ if (!name)
215
+ return;
216
+ if (shouldIgnoreName(name, config))
217
+ return;
218
+ // 构造函数、getter、setter 等内置方法不检查
219
+ if (name === "constructor" || name.startsWith("get ") || name.startsWith("set "))
220
+ return;
221
+ const { line, column } = path.node.loc?.start || { line: 0, column: 0 };
222
+ if (!isCamelCase(name)) {
223
+ issues.push({
224
+ ruleId: "naming-function",
225
+ title: `方法名 "${name}" 应使用 camelCase`,
226
+ description: `类方法应使用 camelCase,建议改为 "${toCamelCase(name)}"`,
227
+ severity: "warning",
228
+ file: context.filePath,
229
+ line,
230
+ column,
231
+ source: `${name}()`,
232
+ });
233
+ }
234
+ },
235
+ // 对象方法: { foo() {} }
236
+ ObjectMethod(path) {
237
+ const name = path.node.key.type === "Identifier" ? path.node.key.name : null;
238
+ if (!name)
239
+ return;
240
+ if (shouldIgnoreName(name, config))
241
+ return;
242
+ const { line, column } = path.node.loc?.start || { line: 0, column: 0 };
243
+ if (!isCamelCase(name)) {
244
+ issues.push({
245
+ ruleId: "naming-function",
246
+ title: `对象方法名 "${name}" 应使用 camelCase`,
247
+ description: `对象方法应使用 camelCase,建议改为 "${toCamelCase(name)}"`,
248
+ severity: "suggestion",
249
+ file: context.filePath,
250
+ line,
251
+ column,
252
+ source: `${name}()`,
253
+ });
254
+ }
255
+ },
256
+ // 箭头函数变量: const foo = () => {}
257
+ VariableDeclarator(path) {
258
+ const id = path.node.id;
259
+ if (id.type !== "Identifier")
260
+ return;
261
+ const name = id.name;
262
+ if (shouldIgnoreName(name, config))
263
+ return;
264
+ // 只检查 const 声明的箭头函数
265
+ const parent = path.parentPath;
266
+ if (!parent?.isVariableDeclaration() || parent.node.kind !== "const")
267
+ return;
268
+ const init = path.node.init;
269
+ if (!init || init.type !== "ArrowFunctionExpression")
270
+ return;
271
+ const { line, column } = id.loc?.start || { line: 0, column: 0 };
272
+ // 检测是否是 React 组件
273
+ const isComponent = config.allowPascalCaseComponents && isLikelyReactComponent(init, name);
274
+ if (isComponent) {
275
+ if (!isPascalCase(name)) {
276
+ issues.push({
277
+ ruleId: "naming-function",
278
+ title: `React 组件 "${name}" 应使用 PascalCase`,
279
+ description: `React 组件应使用 PascalCase,建议改为 "${toPascalCase(name)}"`,
280
+ severity: "warning",
281
+ file: context.filePath,
282
+ line,
283
+ column,
284
+ source: `const ${name} = () =>`,
285
+ });
286
+ }
287
+ }
288
+ else {
289
+ if (!isCamelCase(name)) {
290
+ issues.push({
291
+ ruleId: "naming-function",
292
+ title: `函数变量 "${name}" 应使用 camelCase`,
293
+ description: `函数变量应使用 camelCase,建议改为 "${toCamelCase(name)}"`,
294
+ severity: "suggestion",
295
+ file: context.filePath,
296
+ line,
297
+ column,
298
+ source: `const ${name} = () =>`,
299
+ fix: {
300
+ text: toCamelCase(name),
301
+ start: { line, column },
302
+ end: { line, column: column + name.length },
303
+ },
304
+ });
305
+ }
306
+ }
307
+ },
308
+ });
309
+ return issues;
310
+ },
311
+ },
312
+ {
313
+ id: "naming-variable",
314
+ name: "变量应使用 camelCase",
315
+ description: "let/var 声明的变量使用 camelCase,const 常量使用 UPPER_SNAKE_CASE",
316
+ severity: "suggestion",
317
+ category: "style",
318
+ defaultEnabled: true,
319
+ docsUrl: "https://github.com/wzm111/frontend-guardian/blob/main/docs/rules/naming-variable.md",
320
+ execute(context) {
321
+ const issues = [];
322
+ const config = getNamingConfig(context);
323
+ const ast = context.utils.parseAST(context.source, {
324
+ ext: (0, common_js_1.getFileExt)(context.filePath),
325
+ });
326
+ if (!ast)
327
+ return issues;
328
+ (0, traverse_1.default)(ast, {
329
+ VariableDeclarator(path) {
330
+ const id = path.node.id;
331
+ if (id.type !== "Identifier")
332
+ return;
333
+ const name = id.name;
334
+ if (shouldIgnoreName(name, config))
335
+ return;
336
+ const parent = path.parentPath;
337
+ if (!parent?.isVariableDeclaration())
338
+ return;
339
+ const kind = parent.node.kind; // 'const' | 'let' | 'var'
340
+ const init = path.node.init;
341
+ const { line, column } = id.loc?.start || { line: 0, column: 0 };
342
+ if (kind === "const") {
343
+ // const 声明:区分常量和普通变量
344
+ // 如果初始值是字面量(string/number/boolean/array/object),视为常量
345
+ const isLiteral = init &&
346
+ (init.type === "StringLiteral" ||
347
+ init.type === "NumericLiteral" ||
348
+ init.type === "BooleanLiteral" ||
349
+ init.type === "ArrayExpression" ||
350
+ init.type === "ObjectExpression" ||
351
+ init.type === "UnaryExpression"); // -1, !true 等
352
+ // 但 React 组件不是常量(已在上面的规则中处理)
353
+ if (init?.type === "ArrowFunctionExpression" || init?.type === "FunctionExpression") {
354
+ return; // 由 naming-function 规则处理
355
+ }
356
+ if (isLiteral && config.constantCase === "UPPER_SNAKE_CASE") {
357
+ // 常量应 UPPER_SNAKE_CASE(但对象/数组可以有例外)
358
+ if (!isUpperSnakeCase(name) && !isCamelCase(name)) {
359
+ // 如果既不是 UPPER_SNAKE_CASE 也不是 camelCase,报错
360
+ issues.push({
361
+ ruleId: "naming-variable",
362
+ title: `常量 "${name}" 建议使用 UPPER_SNAKE_CASE`,
363
+ description: `const 声明的字面量常量建议使用全大写下划线分隔,如 "${toUpperSnakeCase(name)}"`,
364
+ severity: "suggestion",
365
+ file: context.filePath,
366
+ line,
367
+ column,
368
+ source: `const ${name}`,
369
+ });
370
+ }
371
+ }
372
+ else {
373
+ // 普通 const 变量:camelCase
374
+ if (!isCamelCase(name)) {
375
+ issues.push({
376
+ ruleId: "naming-variable",
377
+ title: `变量 "${name}" 应使用 camelCase`,
378
+ description: `变量应使用 camelCase,建议改为 "${toCamelCase(name)}"`,
379
+ severity: "suggestion",
380
+ file: context.filePath,
381
+ line,
382
+ column,
383
+ source: `const ${name}`,
384
+ });
385
+ }
386
+ }
387
+ }
388
+ else {
389
+ // let/var:camelCase
390
+ if (!isCamelCase(name)) {
391
+ issues.push({
392
+ ruleId: "naming-variable",
393
+ title: `变量 "${name}" 应使用 camelCase`,
394
+ description: `${kind} 声明的变量应使用 camelCase,建议改为 "${toCamelCase(name)}"`,
395
+ severity: "suggestion",
396
+ file: context.filePath,
397
+ line,
398
+ column,
399
+ source: `${kind} ${name}`,
400
+ });
401
+ }
402
+ }
403
+ },
404
+ });
405
+ return issues;
406
+ },
407
+ },
408
+ {
409
+ id: "naming-private-member",
410
+ name: "私有成员应有明确前缀",
411
+ description: "私有属性和方法应使用 _ 前缀或 # 前缀",
412
+ severity: "suggestion",
413
+ category: "style",
414
+ defaultEnabled: true,
415
+ docsUrl: "https://github.com/wzm111/frontend-guardian/blob/main/docs/rules/naming-private-member.md",
416
+ execute(context) {
417
+ const issues = [];
418
+ const config = getNamingConfig(context);
419
+ if (config.privatePrefix === "none")
420
+ return [];
421
+ const ast = context.utils.parseAST(context.source, {
422
+ ext: (0, common_js_1.getFileExt)(context.filePath),
423
+ });
424
+ if (!ast)
425
+ return issues;
426
+ (0, traverse_1.default)(ast, {
427
+ // TypeScript 私有修饰符
428
+ ClassProperty(path) {
429
+ const node = path.node;
430
+ const key = node.key;
431
+ if (key.type !== "Identifier")
432
+ return;
433
+ const name = key.name;
434
+ // 检查 TS private 修饰符
435
+ const isPrivate = node.accessibility === "private" || node.private;
436
+ if (!isPrivate)
437
+ return;
438
+ const { line, column } = key.loc?.start || { line: 0, column: 0 };
439
+ if (config.privatePrefix === "underscore" && !name.startsWith("_")) {
440
+ issues.push({
441
+ ruleId: "naming-private-member",
442
+ title: `私有属性 "${name}" 应使用 _ 前缀`,
443
+ description: `私有属性建议添加 _ 前缀,改为 "_${name}"`,
444
+ severity: "suggestion",
445
+ file: context.filePath,
446
+ line,
447
+ column,
448
+ source: `private ${name}`,
449
+ });
450
+ }
451
+ },
452
+ ClassMethod(path) {
453
+ const node = path.node;
454
+ const key = node.key;
455
+ if (key.type !== "Identifier")
456
+ return;
457
+ const name = key.name;
458
+ if (name === "constructor")
459
+ return;
460
+ const isPrivate = node.accessibility === "private";
461
+ if (!isPrivate)
462
+ return;
463
+ const { line, column } = key.loc?.start || { line: 0, column: 0 };
464
+ if (config.privatePrefix === "underscore" && !name.startsWith("_")) {
465
+ issues.push({
466
+ ruleId: "naming-private-member",
467
+ title: `私有方法 "${name}" 应使用 _ 前缀`,
468
+ description: `私有方法建议添加 _ 前缀,改为 "_${name}"`,
469
+ severity: "suggestion",
470
+ file: context.filePath,
471
+ line,
472
+ column,
473
+ source: `private ${name}()`,
474
+ });
475
+ }
476
+ },
477
+ // JS 私有字段 #foo
478
+ ClassPrivateProperty(path) {
479
+ // # 前缀是 JS 原生私有字段,已经满足 hash 前缀
480
+ // 如果配置要求 underscore,而实际用了 hash,给出建议
481
+ if (config.privatePrefix === "underscore") {
482
+ const node = path.node;
483
+ const name = node.key?.id?.name || "";
484
+ const { line, column } = node.loc?.start || { line: 0, column: 0 };
485
+ issues.push({
486
+ ruleId: "naming-private-member",
487
+ title: `私有字段 "#${name}" 建议改为 _ 前缀`,
488
+ description: `项目约定私有成员使用 _ 前缀,建议改为 "_${name}"`,
489
+ severity: "suggestion",
490
+ file: context.filePath,
491
+ line,
492
+ column,
493
+ source: `#${name}`,
494
+ });
495
+ }
496
+ },
497
+ });
498
+ return issues;
499
+ },
500
+ },
501
+ {
502
+ id: "naming-file-folder",
503
+ name: "文件和文件夹命名规范",
504
+ description: "文件名和文件夹名建议使用 kebab-case",
505
+ severity: "suggestion",
506
+ category: "style",
507
+ defaultEnabled: true,
508
+ docsUrl: "https://github.com/wzm111/frontend-guardian/blob/main/docs/rules/naming-file-folder.md",
509
+ execute(context) {
510
+ const issues = [];
511
+ const config = getNamingConfig(context);
512
+ // 检查文件名
513
+ const fileName = (0, node_path_1.basename)(context.filePath);
514
+ const nameWithoutExt = fileName.replace((0, node_path_1.extname)(fileName), "");
515
+ // 跳过特殊文件
516
+ if (nameWithoutExt.startsWith(".") || // .eslintrc, .babelrc
517
+ nameWithoutExt === "index" ||
518
+ nameWithoutExt === "main" ||
519
+ nameWithoutExt === "App" ||
520
+ nameWithoutExt === "setupTests" ||
521
+ /^(vite|webpack|rollup|babel|jest|eslint|prettier|tsconfig|jsconfig|postcss|tailwind)/i.test(nameWithoutExt)) {
522
+ // 跳过配置文件和入口文件
523
+ }
524
+ else if (config.fileNameCase === "kebab-case" &&
525
+ !isKebabCase(nameWithoutExt) &&
526
+ !isCamelCase(nameWithoutExt)) {
527
+ const { line, column } = { line: 1, column: 1 };
528
+ issues.push({
529
+ ruleId: "naming-file-folder",
530
+ title: `文件名 "${nameWithoutExt}" 建议使用 kebab-case`,
531
+ description: `文件名建议使用短横线连接的小写格式,如 "${toKebabCase(nameWithoutExt)}"`,
532
+ severity: "suggestion",
533
+ file: context.filePath,
534
+ line,
535
+ column,
536
+ source: fileName,
537
+ });
538
+ }
539
+ // 检查文件夹名
540
+ const dirPath = (0, node_path_1.dirname)(context.filePath);
541
+ const dirName = (0, node_path_1.basename)(dirPath);
542
+ // 跳过根目录和特殊目录
543
+ if (dirName.startsWith(".") ||
544
+ dirName === "node_modules" ||
545
+ dirName === "src" ||
546
+ dirName === "dist" ||
547
+ dirName === "build" ||
548
+ dirName === "public" ||
549
+ dirName === "assets" ||
550
+ dirName === "components" ||
551
+ dirName === "pages" ||
552
+ dirName === "utils" ||
553
+ dirName === "hooks" ||
554
+ dirName === "types" ||
555
+ dirName === "constants" ||
556
+ dirName === "services" ||
557
+ dirName === "store" ||
558
+ dirName === "styles" ||
559
+ dirName === "locales" ||
560
+ dirName === "i18n" ||
561
+ dirName === "tests" ||
562
+ dirName === "__tests__" ||
563
+ dirName === "__mocks__") {
564
+ // 跳过标准目录名
565
+ }
566
+ else if (config.folderNameCase === "kebab-case" && !isKebabCase(dirName) && !isCamelCase(dirName)) {
567
+ // 只检查直接父目录(避免检查太深层)
568
+ // 不直接报告,因为无法精确定位到目录的代码位置
569
+ // 记录在 meta 中供汇总报告
570
+ }
571
+ return issues;
572
+ },
573
+ },
574
+ ];
575
+ // ============================================================================
576
+ // 辅助函数
577
+ // ============================================================================
578
+ /** 获取命名规范配置 */
579
+ function getNamingConfig(context) {
580
+ return { ...DEFAULT_NAMING, ...(context.config.naming || {}) };
581
+ }
582
+ /** 通用的标识符大小写检查 */
583
+ function checkIdentifierCase(context, nodeType, kindLabel, expectedCase) {
584
+ const issues = [];
585
+ const ast = context.utils.parseAST(context.source, {
586
+ ext: (0, common_js_1.getFileExt)(context.filePath),
587
+ });
588
+ if (!ast)
589
+ return issues;
590
+ (0, traverse_1.default)(ast, {
591
+ [nodeType](path) {
592
+ const name = path.node.id?.name;
593
+ if (!name)
594
+ return;
595
+ const { line, column } = path.node.loc?.start || { line: 0, column: 0 };
596
+ let isValid = false;
597
+ let suggestion = "";
598
+ switch (expectedCase) {
599
+ case "PascalCase":
600
+ isValid = isPascalCase(name);
601
+ suggestion = toPascalCase(name);
602
+ break;
603
+ case "camelCase":
604
+ isValid = isCamelCase(name);
605
+ suggestion = toCamelCase(name);
606
+ break;
607
+ case "UPPER_SNAKE_CASE":
608
+ isValid = isUpperSnakeCase(name);
609
+ suggestion = toUpperSnakeCase(name);
610
+ break;
611
+ }
612
+ if (!isValid) {
613
+ issues.push({
614
+ ruleId: `naming-${kindLabel}`,
615
+ title: `${kindLabel}名 "${name}" 应使用 ${expectedCase}`,
616
+ description: `${kindLabel}应使用 ${expectedCase},建议改为 "${suggestion}"`,
617
+ severity: "warning",
618
+ file: context.filePath,
619
+ line,
620
+ column,
621
+ source: `${kindLabel} ${name}`,
622
+ fix: {
623
+ text: suggestion,
624
+ start: { line, column: column + kindLabel.length + 1 },
625
+ end: { line, column: column + kindLabel.length + 1 + name.length },
626
+ },
627
+ });
628
+ }
629
+ },
630
+ });
631
+ return issues;
632
+ }
633
+ /** 是否是 PascalCase */
634
+ function isPascalCase(str) {
635
+ return /^[A-Z][a-zA-Z0-9]*$/.test(str) && /[a-z]/.test(str);
636
+ }
637
+ /** 是否是 camelCase */
638
+ function isCamelCase(str) {
639
+ return /^[a-z][a-zA-Z0-9]*$/.test(str);
640
+ }
641
+ /** 是否是 UPPER_SNAKE_CASE */
642
+ function isUpperSnakeCase(str) {
643
+ return /^[A-Z][A-Z0-9_]*$/.test(str);
644
+ }
645
+ /** 是否是 kebab-case */
646
+ function isKebabCase(str) {
647
+ return /^[a-z][a-z0-9-]*$/.test(str) && !str.endsWith("-");
648
+ }
649
+ /** 转换为 PascalCase */
650
+ function toPascalCase(str) {
651
+ // 处理已有的分隔符
652
+ return str
653
+ .replace(/[-_]/g, " ")
654
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
655
+ .split(/\s+/)
656
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
657
+ .join("");
658
+ }
659
+ /** 转换为 camelCase */
660
+ function toCamelCase(str) {
661
+ const pascal = toPascalCase(str);
662
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
663
+ }
664
+ /** 转换为 UPPER_SNAKE_CASE */
665
+ function toUpperSnakeCase(str) {
666
+ return str
667
+ .replace(/[-]/g, "_")
668
+ .replace(/([a-z])([A-Z])/g, "$1_$2")
669
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
670
+ .toUpperCase()
671
+ .replace(/^_+|_+$/g, "");
672
+ }
673
+ /** 转换为 kebab-case */
674
+ function toKebabCase(str) {
675
+ return str
676
+ .replace(/_/g, "-")
677
+ .replace(/([a-z])([A-Z])/g, "$1-$2")
678
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
679
+ .toLowerCase()
680
+ .replace(/^-+|-+$/g, "");
681
+ }
682
+ /** 检测是否是 React 组件 */
683
+ function isLikelyReactComponent(node, name) {
684
+ // 1. 名字以大写字母开头(PascalCase)
685
+ if (!/^[A-Z]/.test(name))
686
+ return false;
687
+ // 2. 检查函数体是否返回 JSX
688
+ let body = node.body;
689
+ if (!body)
690
+ return false;
691
+ // 箭头函数可能直接返回表达式
692
+ if (body.type !== "BlockStatement") {
693
+ return isJSXExpression(body);
694
+ }
695
+ // 遍历函数体找 return 语句
696
+ for (const stmt of body.body || []) {
697
+ if (stmt.type === "ReturnStatement" && stmt.argument) {
698
+ if (isJSXExpression(stmt.argument))
699
+ return true;
700
+ }
701
+ }
702
+ return false;
703
+ }
704
+ /** 是否是 JSX 表达式 */
705
+ function isJSXExpression(node) {
706
+ if (!node)
707
+ return false;
708
+ return (node.type === "JSXElement" ||
709
+ node.type === "JSXFragment" ||
710
+ node.type === "JSXText" ||
711
+ (node.type === "ConditionalExpression" &&
712
+ (isJSXExpression(node.consequent) || isJSXExpression(node.alternate))) ||
713
+ (node.type === "LogicalExpression" && (isJSXExpression(node.left) || isJSXExpression(node.right))) ||
714
+ (node.type === "CallExpression" && isJSXExpression(node.arguments[0])));
715
+ }
716
+ /** 是否应该忽略的名字 */
717
+ function shouldIgnoreName(name, config) {
718
+ // 单字母变量
719
+ if (config.allowSingleLetter && name.length === 1)
720
+ return true;
721
+ // 下划线开头的内部变量
722
+ if (name.startsWith("_"))
723
+ return true;
724
+ // $ 前缀(jQuery, Vue 等)
725
+ if (name.startsWith("$"))
726
+ return true;
727
+ // 双下划线(魔术方法、内部方法)
728
+ if (name.startsWith("__"))
729
+ return true;
730
+ // React/Vue 特殊名
731
+ if (["props", "state", "emit", "slots", "attrs", "refs", "context", "children"].includes(name))
732
+ return true;
733
+ // 配置中的忽略模式
734
+ for (const pattern of config.ignorePatterns || []) {
735
+ try {
736
+ const regex = new RegExp(pattern);
737
+ if (regex.test(name))
738
+ return true;
739
+ }
740
+ catch {
741
+ // 无效正则,跳过
742
+ }
743
+ }
744
+ return false;
745
+ }
746
+ //# sourceMappingURL=naming-scanner.js.map