king-design-analyzer 1.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.
Files changed (92) hide show
  1. package/README.md +151 -0
  2. package/components/affix.json +178 -0
  3. package/components/az.json +86 -0
  4. package/components/badge.json +162 -0
  5. package/components/billtypes.json +90 -0
  6. package/components/breadcrumb.json +220 -0
  7. package/components/button.json +511 -0
  8. package/components/card.json +334 -0
  9. package/components/cardcontent.json +144 -0
  10. package/components/carousel.json +207 -0
  11. package/components/cascader.json +373 -0
  12. package/components/checkbox.json +298 -0
  13. package/components/code.json +318 -0
  14. package/components/collapse.json +358 -0
  15. package/components/copy.json +220 -0
  16. package/components/datepicker.json +529 -0
  17. package/components/descriptions.json +309 -0
  18. package/components/dialog.json +460 -0
  19. package/components/divider.json +287 -0
  20. package/components/drawer.json +563 -0
  21. package/components/dropdown.json +407 -0
  22. package/components/editable.json +331 -0
  23. package/components/ellipsis.json +218 -0
  24. package/components/form.json +638 -0
  25. package/components/grid.json +377 -0
  26. package/components/icon.json +442 -0
  27. package/components/input.json +821 -0
  28. package/components/layoutcontent.json +210 -0
  29. package/components/menu.json +482 -0
  30. package/components/message.json +345 -0
  31. package/components/pagination.json +444 -0
  32. package/components/paginationplus.json +74 -0
  33. package/components/popover.json +456 -0
  34. package/components/progress.json +354 -0
  35. package/components/protable.json +132 -0
  36. package/components/radio.json +246 -0
  37. package/components/region.json +115 -0
  38. package/components/select.json +676 -0
  39. package/components/slider.json +422 -0
  40. package/components/spin.json +232 -0
  41. package/components/spinner.json +441 -0
  42. package/components/status.json +75 -0
  43. package/components/steps.json +292 -0
  44. package/components/switch.json +357 -0
  45. package/components/table.json +1057 -0
  46. package/components/tablecolumnid.json +217 -0
  47. package/components/tabs.json +363 -0
  48. package/components/tag.json +504 -0
  49. package/components/timepicker.json +447 -0
  50. package/components/tip.json +322 -0
  51. package/components/tooltip.json +416 -0
  52. package/components/tour.json +395 -0
  53. package/components/transfer.json +414 -0
  54. package/components/tree.json +480 -0
  55. package/components/treeselect.json +478 -0
  56. package/components/upload.json +499 -0
  57. package/components/virtuallist.json +178 -0
  58. package/dist/ast/index.d.mts +71 -0
  59. package/dist/ast/index.d.ts +71 -0
  60. package/dist/ast/index.js +15 -0
  61. package/dist/ast/index.mjs +2 -0
  62. package/dist/chunk-4BUGNH4F.mjs +62 -0
  63. package/dist/chunk-5H7N2A5X.mjs +1 -0
  64. package/dist/chunk-5IF32MBB.js +603 -0
  65. package/dist/chunk-ARWRNWDW.js +219 -0
  66. package/dist/chunk-C3L4IXJC.mjs +577 -0
  67. package/dist/chunk-GNVCC37B.js +102 -0
  68. package/dist/chunk-N4UIA6DN.js +68 -0
  69. package/dist/chunk-RAMIBZAU.mjs +216 -0
  70. package/dist/chunk-RNQHI7YG.js +64 -0
  71. package/dist/chunk-TB6BF5TJ.mjs +99 -0
  72. package/dist/chunk-UX7KGX45.mjs +66 -0
  73. package/dist/chunk-YTEYDSDW.js +2 -0
  74. package/dist/full/index.d.mts +29 -0
  75. package/dist/full/index.d.ts +29 -0
  76. package/dist/full/index.js +16 -0
  77. package/dist/full/index.mjs +3 -0
  78. package/dist/index.d.mts +5 -0
  79. package/dist/index.d.ts +5 -0
  80. package/dist/index.js +43 -0
  81. package/dist/index.mjs +6 -0
  82. package/dist/runtime/index.d.mts +14 -0
  83. package/dist/runtime/index.d.ts +14 -0
  84. package/dist/runtime/index.js +15 -0
  85. package/dist/runtime/index.mjs +2 -0
  86. package/dist/sfcCompiler-m51JOfWs.d.mts +22 -0
  87. package/dist/sfcCompiler-m51JOfWs.d.ts +22 -0
  88. package/dist/static/index.d.mts +14 -0
  89. package/dist/static/index.d.ts +14 -0
  90. package/dist/static/index.js +19 -0
  91. package/dist/static/index.mjs +2 -0
  92. package/package.json +88 -0
@@ -0,0 +1,603 @@
1
+ 'use strict';
2
+
3
+ var fs = require('fs/promises');
4
+ var fsSync = require('fs');
5
+ var path = require('path');
6
+ var compilerSfc = require('@vue/compiler-sfc');
7
+ var ts = require('typescript');
8
+
9
+ function _interopNamespace(e) {
10
+ if (e && e.__esModule) return e;
11
+ var n = Object.create(null);
12
+ if (e) {
13
+ Object.keys(e).forEach(function (k) {
14
+ if (k !== 'default') {
15
+ var d = Object.getOwnPropertyDescriptor(e, k);
16
+ Object.defineProperty(n, k, d.get ? d : {
17
+ enumerable: true,
18
+ get: function () { return e[k]; }
19
+ });
20
+ }
21
+ });
22
+ }
23
+ n.default = e;
24
+ return Object.freeze(n);
25
+ }
26
+
27
+ var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
28
+ var fsSync__namespace = /*#__PURE__*/_interopNamespace(fsSync);
29
+ var path__namespace = /*#__PURE__*/_interopNamespace(path);
30
+ var ts__namespace = /*#__PURE__*/_interopNamespace(ts);
31
+
32
+ // src/analysis/componentRegistry.ts
33
+ var ComponentRegistry = class {
34
+ constructor() {
35
+ this.components = /* @__PURE__ */ new Map();
36
+ this.loaded = false;
37
+ this.watcher = null;
38
+ // Hardcoded path to metadata in this specific workspace environment
39
+ this.metadataPath = path__namespace.join(process.cwd(), "components");
40
+ }
41
+ async load() {
42
+ if (this.loaded) return;
43
+ await this.reload();
44
+ this.loaded = true;
45
+ this.startWatching();
46
+ }
47
+ /**
48
+ * 重新加载所有组件元数据
49
+ */
50
+ async reload() {
51
+ const newComponents = /* @__PURE__ */ new Map();
52
+ try {
53
+ const files = await fs__namespace.readdir(this.metadataPath);
54
+ for (const file of files) {
55
+ if (!file.endsWith(".json")) continue;
56
+ const content = await fs__namespace.readFile(path__namespace.join(this.metadataPath, file), "utf-8");
57
+ const data = JSON.parse(content);
58
+ newComponents.set(data.name, data);
59
+ if (data.subComponents) {
60
+ for (const sub of data.subComponents) {
61
+ newComponents.set(sub.name, sub);
62
+ }
63
+ }
64
+ }
65
+ this.components = newComponents;
66
+ console.log(`[ComponentRegistry] ${this.loaded ? "Reloaded" : "Loaded"} ${this.components.size} components.`);
67
+ } catch (error) {
68
+ console.error("[ComponentRegistry] Failed to load metadata:", error);
69
+ }
70
+ }
71
+ /**
72
+ * 开始监听文件变化
73
+ */
74
+ startWatching() {
75
+ if (this.watcher) return;
76
+ try {
77
+ let reloadTimeout = null;
78
+ this.watcher = fsSync__namespace.watch(this.metadataPath, (eventType, filename) => {
79
+ if (!filename?.endsWith(".json")) return;
80
+ console.log(`[ComponentRegistry] Detected change: ${filename} (${eventType})`);
81
+ if (reloadTimeout) {
82
+ clearTimeout(reloadTimeout);
83
+ }
84
+ reloadTimeout = setTimeout(() => {
85
+ this.reload();
86
+ }, 500);
87
+ });
88
+ console.log(`[ComponentRegistry] Hot reload enabled for: ${this.metadataPath}`);
89
+ } catch (error) {
90
+ console.warn("[ComponentRegistry] Could not enable hot reload:", error);
91
+ }
92
+ }
93
+ /**
94
+ * 停止监听
95
+ */
96
+ stopWatching() {
97
+ if (this.watcher) {
98
+ this.watcher.close();
99
+ this.watcher = null;
100
+ console.log("[ComponentRegistry] Hot reload disabled.");
101
+ }
102
+ }
103
+ getComponent(name) {
104
+ return this.components.get(name);
105
+ }
106
+ getAllComponentNames() {
107
+ return Array.from(this.components.keys());
108
+ }
109
+ isKnownComponent(name) {
110
+ return this.components.has(name);
111
+ }
112
+ };
113
+ var componentRegistry = new ComponentRegistry();
114
+ async function analyzeCodeWithAST(code) {
115
+ await componentRegistry.load();
116
+ const violations = [];
117
+ const { descriptor, errors } = compilerSfc.parse(code);
118
+ if (errors.length > 0) {
119
+ return violations;
120
+ }
121
+ const scriptContent = descriptor.scriptSetup?.content || descriptor.script?.content || "";
122
+ if (scriptContent) {
123
+ const sourceFile = ts__namespace.createSourceFile(
124
+ "temp.ts",
125
+ scriptContent,
126
+ ts__namespace.ScriptTarget.Latest,
127
+ true
128
+ );
129
+ ts__namespace.forEachChild(sourceFile, (node) => {
130
+ if (ts__namespace.isImportDeclaration(node)) {
131
+ checkImport(node, violations, sourceFile);
132
+ }
133
+ });
134
+ }
135
+ if (descriptor.template?.ast) {
136
+ checkTemplate(descriptor.template.ast, violations, null);
137
+ }
138
+ if (/<Dropdown[\s\S]*?<template\s+(#menu|v-slot:menu)>/i.test(code)) {
139
+ violations.push({
140
+ rule: "DropdownMenu \u4E0D\u80FD\u653E\u5728 #menu \u63D2\u69FD\u4E2D",
141
+ match: "<template #menu>...<DropdownMenu>",
142
+ suggestion: "\u8BF7\u79FB\u9664 <template #menu>\uFF0C\u5C06 DropdownMenu \u76F4\u63A5\u4F5C\u4E3A Dropdown \u7684\u7B2C\u4E8C\u4E2A\u5B50\u5143\u7D20"
143
+ });
144
+ }
145
+ if (scriptContent && descriptor.template?.content) {
146
+ const scriptBindings = extractScriptBindings(scriptContent);
147
+ checkTemplateVariables(descriptor.template.content, scriptBindings, violations);
148
+ }
149
+ return violations;
150
+ }
151
+ function extractScriptBindings(scriptContent) {
152
+ const bindings = /* @__PURE__ */ new Set();
153
+ const vueBuiltins = [
154
+ "ref",
155
+ "reactive",
156
+ "computed",
157
+ "watch",
158
+ "watchEffect",
159
+ "onMounted",
160
+ "onUnmounted",
161
+ "onBeforeMount",
162
+ "onBeforeUnmount",
163
+ "defineProps",
164
+ "defineEmits",
165
+ "defineExpose",
166
+ "withDefaults",
167
+ "toRef",
168
+ "toRefs",
169
+ "nextTick",
170
+ "inject",
171
+ "provide",
172
+ "readonly",
173
+ "shallowRef",
174
+ "shallowReactive",
175
+ "triggerRef",
176
+ "customRef",
177
+ "h",
178
+ "createVNode",
179
+ "resolveComponent",
180
+ "getCurrentInstance"
181
+ ];
182
+ vueBuiltins.forEach((b) => bindings.add(b));
183
+ [
184
+ "console",
185
+ "window",
186
+ "document",
187
+ "Math",
188
+ "JSON",
189
+ "Date",
190
+ "Array",
191
+ "Object",
192
+ "String",
193
+ "Number",
194
+ "Boolean",
195
+ "Promise",
196
+ "Error",
197
+ "RegExp",
198
+ "Map",
199
+ "Set",
200
+ "parseInt",
201
+ "parseFloat",
202
+ "isNaN",
203
+ "isFinite",
204
+ "setTimeout",
205
+ "setInterval",
206
+ "clearTimeout",
207
+ "clearInterval",
208
+ "fetch",
209
+ "undefined",
210
+ "null",
211
+ "true",
212
+ "false"
213
+ ].forEach((b) => bindings.add(b));
214
+ try {
215
+ let visit2 = function(node) {
216
+ if (ts__namespace.isVariableDeclaration(node) && ts__namespace.isIdentifier(node.name)) {
217
+ bindings.add(node.name.text);
218
+ }
219
+ if (ts__namespace.isFunctionDeclaration(node) && node.name) {
220
+ bindings.add(node.name.text);
221
+ }
222
+ if (ts__namespace.isImportDeclaration(node)) {
223
+ const namedBindings = node.importClause?.namedBindings;
224
+ if (namedBindings && ts__namespace.isNamedImports(namedBindings)) {
225
+ namedBindings.elements.forEach((element) => {
226
+ bindings.add(element.name.text);
227
+ });
228
+ }
229
+ if (node.importClause?.name) {
230
+ bindings.add(node.importClause.name.text);
231
+ }
232
+ }
233
+ if (ts__namespace.isCallExpression(node)) {
234
+ const expr = node.expression;
235
+ if (ts__namespace.isIdentifier(expr) && expr.text === "defineProps") {
236
+ }
237
+ }
238
+ ts__namespace.forEachChild(node, visit2);
239
+ };
240
+ var visit = visit2;
241
+ const sourceFile = ts__namespace.createSourceFile(
242
+ "temp.ts",
243
+ scriptContent,
244
+ ts__namespace.ScriptTarget.Latest,
245
+ true
246
+ );
247
+ visit2(sourceFile);
248
+ } catch {
249
+ }
250
+ return bindings;
251
+ }
252
+ function checkTemplateVariables(templateContent, bindings, violations) {
253
+ const interpolationRegex = /\{\{\s*([^}]+)\s*\}\}/g;
254
+ let match;
255
+ while ((match = interpolationRegex.exec(templateContent)) !== null) {
256
+ const expression = match[1].trim();
257
+ const identifiers = extractIdentifiersFromExpression(expression);
258
+ for (const id of identifiers) {
259
+ if (!bindings.has(id) && !isTemplateBuiltin(id)) {
260
+ violations.push({
261
+ rule: `\u6A21\u677F\u5F15\u7528\u4E86\u672A\u5B9A\u4E49\u7684\u53D8\u91CF: ${id}`,
262
+ match: `{{ ${expression} }}`,
263
+ suggestion: `\u8BF7\u786E\u4FDD\u5728 <script setup> \u4E2D\u5B9A\u4E49\u53D8\u91CF ${id}\uFF0C\u6216\u68C0\u67E5\u62FC\u5199\u662F\u5426\u6B63\u786E`
264
+ });
265
+ }
266
+ }
267
+ }
268
+ const bindRegex = /(?::|v-bind:)[\w.-]+="([^"]+)"/g;
269
+ while ((match = bindRegex.exec(templateContent)) !== null) {
270
+ const expression = match[1].trim();
271
+ const identifiers = extractIdentifiersFromExpression(expression);
272
+ for (const id of identifiers) {
273
+ if (!bindings.has(id) && !isTemplateBuiltin(id)) {
274
+ violations.push({
275
+ rule: `\u7ED1\u5B9A\u5C5E\u6027\u5F15\u7528\u4E86\u672A\u5B9A\u4E49\u7684\u53D8\u91CF: ${id}`,
276
+ match: match[0],
277
+ suggestion: `\u8BF7\u786E\u4FDD\u5728 <script setup> \u4E2D\u5B9A\u4E49\u53D8\u91CF ${id}`
278
+ });
279
+ }
280
+ }
281
+ }
282
+ const eventRegex = /(?:@|v-on:)[\w.-]+="([^"]+)"/g;
283
+ while ((match = eventRegex.exec(templateContent)) !== null) {
284
+ const expression = match[1].trim();
285
+ if (expression.includes("(") || expression.includes("=")) continue;
286
+ const identifiers = extractIdentifiersFromExpression(expression);
287
+ for (const id of identifiers) {
288
+ if (!bindings.has(id) && !isTemplateBuiltin(id)) {
289
+ violations.push({
290
+ rule: `\u4E8B\u4EF6\u5904\u7406\u5668\u5F15\u7528\u4E86\u672A\u5B9A\u4E49\u7684\u51FD\u6570: ${id}`,
291
+ match: match[0],
292
+ suggestion: `\u8BF7\u786E\u4FDD\u5728 <script setup> \u4E2D\u5B9A\u4E49\u51FD\u6570 ${id}`
293
+ });
294
+ }
295
+ }
296
+ }
297
+ }
298
+ function extractIdentifiersFromExpression(expression) {
299
+ expression = expression.replace(/'[^']*'/g, "").replace(/"[^"]*"/g, "").replace(/`[^`]*`/g, "");
300
+ const identifiers = [];
301
+ const idRegex = /\b([a-zA-Z_$][a-zA-Z0-9_$]*)\b/g;
302
+ let match;
303
+ while ((match = idRegex.exec(expression)) !== null) {
304
+ const id = match[1];
305
+ if (!isJsKeyword(id)) {
306
+ identifiers.push(id);
307
+ }
308
+ }
309
+ return identifiers;
310
+ }
311
+ function isTemplateBuiltin(id) {
312
+ const builtins = [
313
+ "$event",
314
+ "$refs",
315
+ "$slots",
316
+ "$attrs",
317
+ "$emit",
318
+ "$route",
319
+ "$router",
320
+ "$t",
321
+ "$i18n",
322
+ "item",
323
+ "index",
324
+ "key",
325
+ "value"
326
+ ];
327
+ return builtins.includes(id);
328
+ }
329
+ function isJsKeyword(id) {
330
+ const keywords = [
331
+ "if",
332
+ "else",
333
+ "for",
334
+ "while",
335
+ "do",
336
+ "switch",
337
+ "case",
338
+ "break",
339
+ "continue",
340
+ "return",
341
+ "try",
342
+ "catch",
343
+ "finally",
344
+ "throw",
345
+ "new",
346
+ "delete",
347
+ "typeof",
348
+ "instanceof",
349
+ "void",
350
+ "in",
351
+ "of",
352
+ "this",
353
+ "class",
354
+ "extends",
355
+ "super",
356
+ "import",
357
+ "export",
358
+ "default",
359
+ "const",
360
+ "let",
361
+ "var",
362
+ "function",
363
+ "async",
364
+ "await",
365
+ "yield",
366
+ "true",
367
+ "false",
368
+ "null",
369
+ "undefined",
370
+ "NaN",
371
+ "Infinity"
372
+ ];
373
+ return keywords.includes(id);
374
+ }
375
+ function checkImport(node, violations, sourceFile) {
376
+ const moduleSpecifier = node.moduleSpecifier.getText(sourceFile).replace(/['"]/g, "");
377
+ if (moduleSpecifier === "@king-design/vue" || moduleSpecifier === "@ksyun-internal/versatile") {
378
+ const namedBindings = node.importClause?.namedBindings;
379
+ if (namedBindings && ts__namespace.isNamedImports(namedBindings)) {
380
+ namedBindings.elements.forEach((element) => {
381
+ const originalName = element.propertyName ? element.propertyName.text : element.name.text;
382
+ const localName = element.name.text;
383
+ const isAlias = !!element.propertyName;
384
+ if (isAlias) {
385
+ violations.push({
386
+ rule: `\u7981\u6B62\u5BF9\u7EC4\u4EF6 ${originalName} \u4F7F\u7528\u522B\u540D\u5BFC\u5165`,
387
+ match: `${originalName} as ${localName}`,
388
+ suggestion: `\u76F4\u63A5\u4F7F\u7528\u539F\u540D: import { ${originalName} } from '${moduleSpecifier}'`
389
+ });
390
+ }
391
+ if (componentRegistry.isKnownComponent(originalName)) {
392
+ const metadata = componentRegistry.getComponent(originalName);
393
+ const expectedPackageMatch = metadata?.importStatement?.match(/from\s+['"]([^'"]+)['"]/);
394
+ if (expectedPackageMatch) {
395
+ const expectedPackage = expectedPackageMatch[1];
396
+ if (expectedPackage !== moduleSpecifier) {
397
+ violations.push({
398
+ rule: `\u7EC4\u4EF6 ${originalName} \u5BFC\u5165\u6E90\u9519\u8BEF`,
399
+ match: `import { ... } from '${moduleSpecifier}'`,
400
+ suggestion: `\u5E94\u4ECE '${expectedPackage}' \u5BFC\u5165`
401
+ });
402
+ }
403
+ }
404
+ } else {
405
+ violations.push({
406
+ rule: "\u5F15\u7528\u4E86\u4E0D\u5B58\u5728\u7684\u7EC4\u4EF6/\u5BFC\u51FA",
407
+ match: originalName,
408
+ suggestion: `\u8BF7\u786E\u8BA4 ${originalName} \u662F\u5426\u5B58\u5728\u4E8E ${moduleSpecifier}\u3002\u5EFA\u8BAE\u67E5\u770B\u6587\u6863\u3002`
409
+ });
410
+ }
411
+ });
412
+ }
413
+ }
414
+ }
415
+ var NESTING_RULES = {
416
+ "DropdownItem": ["DropdownMenu"],
417
+ "DropdownMenu": ["Dropdown"],
418
+ "TableColumn": ["Table"],
419
+ "FormItem": ["Form"],
420
+ "Tab": ["Tabs"],
421
+ "Step": ["Steps"],
422
+ "MenuItem": ["Menu", "SubMenu"],
423
+ "SubMenu": ["Menu"],
424
+ "BreadcrumbItem": ["Breadcrumb"],
425
+ "CollapseItem": ["Collapse"],
426
+ "CarouselItem": ["Carousel"],
427
+ "DescriptionItem": ["Descriptions"]
428
+ };
429
+ function checkTemplate(node, violations, parentTagName) {
430
+ if (node.type === 1) {
431
+ const tagName = node.tag;
432
+ if (componentRegistry.isKnownComponent(tagName)) {
433
+ const metadata = componentRegistry.getComponent(tagName);
434
+ const requiredParents = metadata?.requiredParent ? [metadata.requiredParent] : NESTING_RULES[tagName];
435
+ if (requiredParents && requiredParents.length > 0) {
436
+ if (parentTagName && !requiredParents.includes(parentTagName)) {
437
+ const isInSlotTemplate = parentTagName === "template";
438
+ if (!isInSlotTemplate) {
439
+ violations.push({
440
+ rule: `${tagName} \u5FC5\u987B\u653E\u5728 ${requiredParents.join(" \u6216 ")} \u7EC4\u4EF6\u5185`,
441
+ match: tagName,
442
+ suggestion: `\u8BF7\u5C06 ${tagName} \u79FB\u52A8\u5230 ${requiredParents[0]} \u7EC4\u4EF6\u4E2D`
443
+ });
444
+ }
445
+ }
446
+ }
447
+ }
448
+ if (tagName === "Table") {
449
+ node.props.forEach((prop) => {
450
+ if (prop.type === 6 && (prop.name === "rowKey" || prop.name === "row-key")) {
451
+ violations.push({
452
+ rule: "Table\u7EC4\u4EF6 rowKey \u5C5E\u6027 usage \u9519\u8BEF",
453
+ match: `rowKey="${prop.value?.content || ""}"`,
454
+ suggestion: 'rowKey \u5FC5\u987B\u662F\u4E00\u4E2A\u8FD4\u56DE\u552F\u4E00\u503C\u7684\u51FD\u6570\u3002\u8BF7\u4F7F\u7528\u7ED1\u5B9A\u8BED\u6CD5\uFF0C\u4F8B\u5982 :rowKey="(row) => row.id"'
455
+ });
456
+ }
457
+ });
458
+ }
459
+ if (tagName === "Icon") {
460
+ node.props.forEach((prop) => {
461
+ if (prop.type === 6 && prop.name === "name") {
462
+ violations.push({
463
+ rule: "Icon \u7EC4\u4EF6\u7981\u6B62\u4F7F\u7528 name \u5C5E\u6027",
464
+ match: `name="${prop.value?.content || ""}"`,
465
+ suggestion: '\u8BF7\u4F7F\u7528 class \u5C5E\u6027\u6307\u5B9A\u56FE\u6807\uFF0C\u4F8B\u5982: class="k-icon-search"'
466
+ });
467
+ }
468
+ });
469
+ }
470
+ if (componentRegistry.isKnownComponent(tagName)) {
471
+ const metadata = componentRegistry.getComponent(tagName);
472
+ if (metadata) {
473
+ let customTagName = null;
474
+ const tagNameProp = node.props.find(
475
+ (p) => p.type === 6 && p.name === "tagName" || p.type === 7 && p.name === "bind" && p.arg?.content === "tagName"
476
+ );
477
+ if (tagNameProp?.type === 6 && tagNameProp.value?.content) {
478
+ customTagName = tagNameProp.value.content;
479
+ }
480
+ const dynamicAllowedProps = [];
481
+ if (customTagName === "a") {
482
+ dynamicAllowedProps.push("href", "target", "rel", "download");
483
+ }
484
+ node.props.forEach((prop) => {
485
+ if (prop.type === 6) {
486
+ const propName = prop.name;
487
+ const camelCaseProp = propName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
488
+ const isPropValid = metadata.props.some(
489
+ (p) => p.name === propName || p.name === camelCaseProp || p.name.toLowerCase() === propName.toLowerCase()
490
+ );
491
+ const isDynamicallyAllowed = dynamicAllowedProps.includes(propName);
492
+ if (!isPropValid && !isDynamicallyAllowed && !["class", "style", "key", "ref"].includes(propName) && !propName.startsWith("data-")) {
493
+ if (metadata.props.length > 0) {
494
+ violations.push({
495
+ rule: `\u5C5E\u6027 ${propName} \u4E0D\u5B58\u5728\u4E8E ${tagName}`,
496
+ match: propName,
497
+ suggestion: `\u53EF\u7528\u5C5E\u6027: ${metadata.props.map((p) => p.name).join(", ")}`
498
+ });
499
+ }
500
+ }
501
+ if (isPropValid) {
502
+ const targetProp = metadata.props.find(
503
+ (p) => p.name === propName || p.name === camelCaseProp || p.name.toLowerCase() === propName.toLowerCase()
504
+ );
505
+ if (targetProp) {
506
+ const attrValue = prop.value?.content;
507
+ if (attrValue) {
508
+ let cleanAllowedValues = [];
509
+ if (targetProp.allowedValues && targetProp.allowedValues.length > 0) {
510
+ cleanAllowedValues = targetProp.allowedValues.map((av) => String(av.value).replace(/['"]/g, ""));
511
+ } else if (targetProp.type.kind === "union" && targetProp.type.unionTypes) {
512
+ cleanAllowedValues = targetProp.type.unionTypes.map((v) => String(v).replace(/['"]/g, ""));
513
+ }
514
+ if (cleanAllowedValues.length > 0) {
515
+ const isValueValid = cleanAllowedValues.includes(attrValue);
516
+ if (!isValueValid) {
517
+ console.log(`\u274C [AST Reviewer] \u5C5E\u6027\u503C\u6821\u9A8C\u5931\u8D25: ${tagName}.${propName}="${attrValue}" (\u5141\u8BB8\u503C: ${cleanAllowedValues.join(" | ")})`);
518
+ violations.push({
519
+ rule: `${tagName} \u7684\u5C5E\u6027 ${propName} \u7684\u503C "${attrValue}" \u65E0\u6548`,
520
+ match: `${propName}="${attrValue}"`,
521
+ suggestion: `\u5141\u8BB8\u7684\u503C: ${cleanAllowedValues.join(" | ")}`
522
+ });
523
+ }
524
+ }
525
+ }
526
+ }
527
+ }
528
+ } else if (prop.type === 7) {
529
+ const dirName = prop.name;
530
+ const arg = prop.arg?.content;
531
+ if (dirName === "on" && arg) {
532
+ const eventName = arg;
533
+ if (eventName.startsWith("$change:") || eventName.startsWith("update:")) {
534
+ return;
535
+ }
536
+ const camelCaseEvent = eventName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
537
+ const isEventValid = metadata.events.some(
538
+ (e) => e.name === eventName || e.name === camelCaseEvent || e.name.toLowerCase() === eventName.toLowerCase()
539
+ );
540
+ const nativeEvents = [
541
+ "click",
542
+ "dblclick",
543
+ "mousedown",
544
+ "mouseup",
545
+ "mousemove",
546
+ "mouseenter",
547
+ "mouseleave",
548
+ "keydown",
549
+ "keyup",
550
+ "keypress",
551
+ "focus",
552
+ "blur",
553
+ "input",
554
+ "submit",
555
+ "scroll",
556
+ "resize",
557
+ "contextmenu",
558
+ "wheel",
559
+ "touchstart",
560
+ "touchend",
561
+ "touchmove"
562
+ ];
563
+ const isNativeEvent = nativeEvents.includes(eventName);
564
+ if (!isEventValid && !isNativeEvent && metadata.events.length > 0) {
565
+ violations.push({
566
+ rule: `\u4E8B\u4EF6 @${eventName} \u4E0D\u5B58\u5728\u4E8E ${tagName}`,
567
+ match: `@${eventName}`,
568
+ suggestion: `\u53EF\u7528\u4E8B\u4EF6: ${metadata.events.map((e) => e.name).join(", ") || "\u65E0\u81EA\u5B9A\u4E49\u4E8B\u4EF6"}`
569
+ });
570
+ }
571
+ }
572
+ }
573
+ });
574
+ if (node.children && metadata.slots && metadata.slots.length > 0) {
575
+ node.children.forEach((child) => {
576
+ if (child.type === 1 && child.tag === "template") {
577
+ child.props.forEach((prop) => {
578
+ if (prop.type === 7 && prop.name === "slot") {
579
+ const slotName = prop.arg?.content || "default";
580
+ const isSlotValid = metadata.slots.some((s) => s.name === slotName);
581
+ if (!isSlotValid) {
582
+ violations.push({
583
+ rule: `\u63D2\u69FD #${slotName} \u4E0D\u5B58\u5728\u4E8E ${tagName}`,
584
+ match: `#${slotName}`,
585
+ suggestion: `\u53EF\u7528\u63D2\u69FD: ${metadata.slots.map((s) => s.name).join(", ")}`
586
+ });
587
+ }
588
+ }
589
+ });
590
+ }
591
+ });
592
+ }
593
+ }
594
+ }
595
+ }
596
+ if (node.children) {
597
+ const currentTag = node.type === 1 ? node.tag : null;
598
+ node.children.forEach((child) => checkTemplate(child, violations, currentTag));
599
+ }
600
+ }
601
+
602
+ exports.analyzeCodeWithAST = analyzeCodeWithAST;
603
+ exports.componentRegistry = componentRegistry;