king-design-analyzer 2.1.6 → 2.1.9

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.
@@ -1,1027 +0,0 @@
1
- import * as fs from 'fs/promises';
2
- import * as fsSync from 'fs';
3
- import * as path from 'path';
4
- import { fileURLToPath } from 'url';
5
- import { parse } from '@vue/compiler-sfc';
6
- import * as ts from 'typescript';
7
-
8
- // src/analysis/componentRegistry.ts
9
- var __filename$1 = fileURLToPath(import.meta.url);
10
- var __dirname$1 = path.dirname(__filename$1);
11
- function resolveComponentsPath() {
12
- const prodPath = path.join(__dirname$1, "../components");
13
- if (fsSync.existsSync(prodPath)) return prodPath;
14
- const devPath = path.join(__dirname$1, "../../components");
15
- if (fsSync.existsSync(devPath)) return devPath;
16
- return prodPath;
17
- }
18
- var ComponentRegistry = class {
19
- constructor() {
20
- this.components = /* @__PURE__ */ new Map();
21
- this.loaded = false;
22
- this.watcher = null;
23
- // Path to components directory (resolved dynamically for dev/prod compatibility)
24
- this.metadataPath = resolveComponentsPath();
25
- }
26
- async load() {
27
- if (this.loaded) return;
28
- await this.reload();
29
- this.loaded = true;
30
- this.startWatching();
31
- }
32
- /**
33
- * 重新加载所有组件元数据
34
- */
35
- async reload() {
36
- const newComponents = /* @__PURE__ */ new Map();
37
- try {
38
- const files = await fs.readdir(this.metadataPath);
39
- for (const file of files) {
40
- if (!file.endsWith(".json")) continue;
41
- const content = await fs.readFile(path.join(this.metadataPath, file), "utf-8");
42
- const data = JSON.parse(content);
43
- newComponents.set(data.name, data);
44
- if (data.subComponents) {
45
- for (const sub of data.subComponents) {
46
- newComponents.set(sub.name, sub);
47
- }
48
- }
49
- }
50
- this.components = newComponents;
51
- console.log(`[ComponentRegistry] ${this.loaded ? "Reloaded" : "Loaded"} ${this.components.size} components.`);
52
- } catch (error) {
53
- console.error("[ComponentRegistry] Failed to load metadata:", error);
54
- }
55
- }
56
- /**
57
- * 开始监听文件变化
58
- */
59
- startWatching() {
60
- if (this.watcher) return;
61
- try {
62
- let reloadTimeout = null;
63
- this.watcher = fsSync.watch(this.metadataPath, (eventType, filename) => {
64
- if (!filename?.endsWith(".json")) return;
65
- console.log(`[ComponentRegistry] Detected change: ${filename} (${eventType})`);
66
- if (reloadTimeout) {
67
- clearTimeout(reloadTimeout);
68
- }
69
- reloadTimeout = setTimeout(() => {
70
- this.reload();
71
- }, 500);
72
- });
73
- console.log(`[ComponentRegistry] Hot reload enabled for: ${this.metadataPath}`);
74
- } catch (error) {
75
- console.warn("[ComponentRegistry] Could not enable hot reload:", error);
76
- }
77
- }
78
- /**
79
- * 停止监听
80
- */
81
- stopWatching() {
82
- if (this.watcher) {
83
- this.watcher.close();
84
- this.watcher = null;
85
- console.log("[ComponentRegistry] Hot reload disabled.");
86
- }
87
- }
88
- getComponent(name) {
89
- return this.components.get(name);
90
- }
91
- getAllComponentNames() {
92
- return Array.from(this.components.keys());
93
- }
94
- isKnownComponent(name) {
95
- return this.components.has(name);
96
- }
97
- };
98
- var componentRegistry = new ComponentRegistry();
99
- var __filename2 = fileURLToPath(import.meta.url);
100
- var __dirname2 = path.dirname(__filename2);
101
- function resolveHooksPath() {
102
- const prodPath = path.join(__dirname2, "../hooks");
103
- if (fsSync.existsSync(prodPath)) return prodPath;
104
- const devPath = path.join(__dirname2, "../../hooks");
105
- if (fsSync.existsSync(devPath)) return devPath;
106
- return prodPath;
107
- }
108
- var HooksRegistry = class {
109
- constructor() {
110
- this.hooks = /* @__PURE__ */ new Map();
111
- this.loaded = false;
112
- this.watcher = null;
113
- this.metadataPath = resolveHooksPath();
114
- }
115
- async load() {
116
- if (this.loaded) return;
117
- await this.reload();
118
- this.loaded = true;
119
- this.startWatching();
120
- }
121
- /**
122
- * 重新加载所有 hooks 元数据
123
- */
124
- async reload() {
125
- const newHooks = /* @__PURE__ */ new Map();
126
- try {
127
- const files = await fs.readdir(this.metadataPath);
128
- for (const file of files) {
129
- if (!file.endsWith(".json")) continue;
130
- const content = await fs.readFile(path.join(this.metadataPath, file), "utf-8");
131
- const data = JSON.parse(content);
132
- newHooks.set(data.name, data);
133
- }
134
- this.hooks = newHooks;
135
- if (this.hooks.size > 0) {
136
- console.log(`[HooksRegistry] ${this.loaded ? "Reloaded" : "Loaded"} ${this.hooks.size} hooks.`);
137
- }
138
- } catch (error) {
139
- if (error.code !== "ENOENT") {
140
- console.error("[HooksRegistry] Failed to load metadata:", error);
141
- }
142
- }
143
- }
144
- /**
145
- * 开始监听文件变化
146
- */
147
- startWatching() {
148
- if (this.watcher) return;
149
- try {
150
- let reloadTimeout = null;
151
- this.watcher = fsSync.watch(this.metadataPath, (eventType, filename) => {
152
- if (!filename?.endsWith(".json")) return;
153
- console.log(`[HooksRegistry] Detected change: ${filename} (${eventType})`);
154
- if (reloadTimeout) {
155
- clearTimeout(reloadTimeout);
156
- }
157
- reloadTimeout = setTimeout(() => {
158
- this.reload();
159
- }, 500);
160
- });
161
- } catch (error) {
162
- }
163
- }
164
- /**
165
- * 停止监听
166
- */
167
- stopWatching() {
168
- if (this.watcher) {
169
- this.watcher.close();
170
- this.watcher = null;
171
- }
172
- }
173
- getHook(name) {
174
- return this.hooks.get(name);
175
- }
176
- getAllHookNames() {
177
- return Array.from(this.hooks.keys());
178
- }
179
- isKnownHook(name) {
180
- return this.hooks.has(name);
181
- }
182
- /**
183
- * 获取 hook 的必填参数数量
184
- */
185
- getRequiredParamCount(name) {
186
- const hook = this.hooks.get(name);
187
- if (!hook) return 0;
188
- return hook.params.filter((p) => p.required).length;
189
- }
190
- /**
191
- * 获取 hook 的总参数数量
192
- */
193
- getTotalParamCount(name) {
194
- const hook = this.hooks.get(name);
195
- if (!hook) return 0;
196
- return hook.params.length;
197
- }
198
- };
199
- var hooksRegistry = new HooksRegistry();
200
-
201
- // src/analysis/astReviewer.ts
202
- var VUE_BUILTINS = /* @__PURE__ */ new Set([
203
- // Reactivity
204
- "ref",
205
- "reactive",
206
- "computed",
207
- "watch",
208
- "watchEffect",
209
- "watchPostEffect",
210
- "watchSyncEffect",
211
- "toRef",
212
- "toRefs",
213
- "toRaw",
214
- "markRaw",
215
- "isRef",
216
- "isReactive",
217
- "isReadonly",
218
- "isProxy",
219
- "shallowRef",
220
- "shallowReactive",
221
- "shallowReadonly",
222
- "triggerRef",
223
- "customRef",
224
- "readonly",
225
- "effectScope",
226
- "getCurrentScope",
227
- "onScopeDispose",
228
- // Lifecycle
229
- "onMounted",
230
- "onUnmounted",
231
- "onBeforeMount",
232
- "onBeforeUnmount",
233
- "onUpdated",
234
- "onBeforeUpdate",
235
- "onActivated",
236
- "onDeactivated",
237
- "onErrorCaptured",
238
- "onRenderTracked",
239
- "onRenderTriggered",
240
- "onServerPrefetch",
241
- // Composition API
242
- "defineProps",
243
- "defineEmits",
244
- "defineExpose",
245
- "defineOptions",
246
- "defineSlots",
247
- "defineModel",
248
- "withDefaults",
249
- "useSlots",
250
- "useAttrs",
251
- // Others
252
- "nextTick",
253
- "inject",
254
- "provide",
255
- "getCurrentInstance",
256
- "h",
257
- "createVNode",
258
- "resolveComponent",
259
- "resolveDirective",
260
- "withDirectives"
261
- ]);
262
- var JS_GLOBALS = /* @__PURE__ */ new Set([
263
- "console",
264
- "window",
265
- "document",
266
- "globalThis",
267
- "self",
268
- "Math",
269
- "JSON",
270
- "Date",
271
- "Array",
272
- "Object",
273
- "String",
274
- "Number",
275
- "Boolean",
276
- "Promise",
277
- "Error",
278
- "TypeError",
279
- "RangeError",
280
- "SyntaxError",
281
- "RegExp",
282
- "Map",
283
- "Set",
284
- "WeakMap",
285
- "WeakSet",
286
- "Symbol",
287
- "Proxy",
288
- "Reflect",
289
- "parseInt",
290
- "parseFloat",
291
- "isNaN",
292
- "isFinite",
293
- "encodeURI",
294
- "decodeURI",
295
- "encodeURIComponent",
296
- "decodeURIComponent",
297
- "atob",
298
- "btoa",
299
- "setTimeout",
300
- "setInterval",
301
- "clearTimeout",
302
- "clearInterval",
303
- "requestAnimationFrame",
304
- "cancelAnimationFrame",
305
- "fetch",
306
- "URL",
307
- "URLSearchParams",
308
- "FormData",
309
- "Blob",
310
- "File",
311
- "FileReader",
312
- "undefined",
313
- "null",
314
- "NaN",
315
- "Infinity",
316
- "true",
317
- "false"
318
- ]);
319
- var JS_KEYWORDS = /* @__PURE__ */ new Set([
320
- "if",
321
- "else",
322
- "for",
323
- "while",
324
- "do",
325
- "switch",
326
- "case",
327
- "break",
328
- "continue",
329
- "return",
330
- "try",
331
- "catch",
332
- "finally",
333
- "throw",
334
- "new",
335
- "delete",
336
- "typeof",
337
- "instanceof",
338
- "void",
339
- "in",
340
- "of",
341
- "this",
342
- "class",
343
- "extends",
344
- "super",
345
- "import",
346
- "export",
347
- "default",
348
- "const",
349
- "let",
350
- "var",
351
- "function",
352
- "async",
353
- "await",
354
- "yield",
355
- "true",
356
- "false",
357
- "null",
358
- "undefined",
359
- "NaN",
360
- "Infinity"
361
- ]);
362
- var TEMPLATE_BUILTINS = /* @__PURE__ */ new Set([
363
- "$event",
364
- "$refs",
365
- "$slots",
366
- "$attrs",
367
- "$emit",
368
- "$el",
369
- "$parent",
370
- "$root",
371
- "$data",
372
- "$options",
373
- "$route",
374
- "$router",
375
- "$t",
376
- "$i18n",
377
- "$n",
378
- "$d",
379
- "$te"
380
- // Vue Router & i18n
381
- ]);
382
- var NATIVE_EVENTS = /* @__PURE__ */ new Set([
383
- // Mouse
384
- "click",
385
- "dblclick",
386
- "mousedown",
387
- "mouseup",
388
- "mousemove",
389
- "mouseenter",
390
- "mouseleave",
391
- "mouseover",
392
- "mouseout",
393
- // Keyboard
394
- "keydown",
395
- "keyup",
396
- "keypress",
397
- // Focus
398
- "focus",
399
- "blur",
400
- "focusin",
401
- "focusout",
402
- // Form
403
- "input",
404
- "change",
405
- "submit",
406
- "reset",
407
- "invalid",
408
- // Drag
409
- "drag",
410
- "dragstart",
411
- "dragend",
412
- "dragenter",
413
- "dragleave",
414
- "dragover",
415
- "drop",
416
- // Clipboard
417
- "copy",
418
- "cut",
419
- "paste",
420
- // Touch
421
- "touchstart",
422
- "touchend",
423
- "touchmove",
424
- "touchcancel",
425
- // Scroll & Resize
426
- "scroll",
427
- "resize",
428
- "wheel",
429
- // Other
430
- "contextmenu",
431
- "pointerdown",
432
- "pointerup",
433
- "pointermove",
434
- "pointerenter",
435
- "pointerleave"
436
- ]);
437
- var NATIVE_HTML_ATTRIBUTES = /* @__PURE__ */ new Set([
438
- // Form attributes
439
- "maxlength",
440
- "minlength",
441
- "max",
442
- "min",
443
- "step",
444
- "pattern",
445
- "autocomplete",
446
- "autofocus",
447
- "required",
448
- "readonly",
449
- "disabled",
450
- "checked",
451
- "selected",
452
- "multiple",
453
- "accept",
454
- "name",
455
- "value",
456
- "placeholder",
457
- "form",
458
- "formaction",
459
- "formmethod",
460
- "formtarget",
461
- // Link/Navigation attributes
462
- "href",
463
- "target",
464
- "rel",
465
- "download",
466
- "hreflang",
467
- "ping",
468
- "referrerpolicy",
469
- // Media attributes
470
- "src",
471
- "alt",
472
- "width",
473
- "height",
474
- "loading",
475
- "decoding",
476
- "crossorigin",
477
- "controls",
478
- "autoplay",
479
- "loop",
480
- "muted",
481
- "poster",
482
- "preload",
483
- // Table attributes
484
- "colspan",
485
- "rowspan",
486
- "headers",
487
- "scope",
488
- // Global attributes
489
- "id",
490
- "title",
491
- "lang",
492
- "dir",
493
- "hidden",
494
- "tabindex",
495
- "accesskey",
496
- "draggable",
497
- "contenteditable",
498
- "spellcheck",
499
- "translate",
500
- "role",
501
- // ARIA attributes (common ones)
502
- "aria-label",
503
- "aria-labelledby",
504
- "aria-describedby",
505
- "aria-hidden",
506
- "aria-expanded",
507
- "aria-selected",
508
- "aria-checked",
509
- "aria-disabled",
510
- "aria-controls",
511
- "aria-haspopup"
512
- ]);
513
- var VERSATILE_COMPONENTS = /* @__PURE__ */ new Set([
514
- "BillTypes",
515
- "Region",
516
- "AZ",
517
- "LayoutContent",
518
- "CardContent",
519
- "ProTable",
520
- "PaginationPlus",
521
- "Status",
522
- "TableColumnId",
523
- "IconTooltip",
524
- "ButtonLink"
525
- ]);
526
- var DUAL_EXPORT_COMPONENTS = /* @__PURE__ */ new Set([
527
- "TableColumn"
528
- // 可与 ProTable 配合从 versatile 导入,也可与 Table 配合从 king-design 导入
529
- ]);
530
- var NESTING_RULES = {
531
- "DropdownItem": ["DropdownMenu"],
532
- "DropdownMenu": ["Dropdown"],
533
- "TableColumn": ["Table", "ProTable"],
534
- "FormItem": ["Form"],
535
- "Tab": ["Tabs"],
536
- "Step": ["Steps"],
537
- "MenuItem": ["Menu", "SubMenu"],
538
- "SubMenu": ["Menu"],
539
- "BreadcrumbItem": ["Breadcrumb"],
540
- "CollapseItem": ["Collapse"],
541
- "CarouselItem": ["Carousel"],
542
- "DescriptionItem": ["Descriptions"]
543
- };
544
- async function analyzeCodeWithAST(code) {
545
- await componentRegistry.load();
546
- await hooksRegistry.load();
547
- const violations = [];
548
- const { descriptor, errors } = parse(code);
549
- if (errors.length > 0) {
550
- return violations;
551
- }
552
- const scriptContent = descriptor.scriptSetup?.content || descriptor.script?.content || "";
553
- if (scriptContent) {
554
- const sourceFile = ts.createSourceFile(
555
- "temp.ts",
556
- scriptContent,
557
- ts.ScriptTarget.Latest,
558
- true
559
- );
560
- ts.forEachChild(sourceFile, (node) => {
561
- if (ts.isImportDeclaration(node)) {
562
- checkImport(node, violations, sourceFile);
563
- }
564
- });
565
- }
566
- if (descriptor.template?.ast) {
567
- checkTemplate(descriptor.template.ast, violations, []);
568
- }
569
- if (scriptContent && descriptor.template) {
570
- const scriptBindings = extractScriptBindings(scriptContent);
571
- checkTemplateVariables(descriptor.template, scriptBindings, violations);
572
- }
573
- if (scriptContent) {
574
- const scriptBindings = extractScriptBindings(scriptContent);
575
- checkScriptFunctionCalls(scriptContent, scriptBindings, violations);
576
- }
577
- return violations;
578
- }
579
- function extractScriptBindings(scriptContent) {
580
- const bindings = /* @__PURE__ */ new Set();
581
- VUE_BUILTINS.forEach((b) => bindings.add(b));
582
- JS_GLOBALS.forEach((b) => bindings.add(b));
583
- try {
584
- let extractNames2 = function(name) {
585
- if (ts.isIdentifier(name)) {
586
- bindings.add(name.text);
587
- } else if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
588
- name.elements.forEach((element) => {
589
- if (ts.isBindingElement(element)) {
590
- extractNames2(element.name);
591
- }
592
- });
593
- }
594
- }, visit2 = function(node) {
595
- if (ts.isVariableStatement(node)) {
596
- node.declarationList.declarations.forEach((decl) => extractNames2(decl.name));
597
- } else if (ts.isFunctionDeclaration(node) && node.name) {
598
- bindings.add(node.name.text);
599
- } else if (ts.isClassDeclaration(node) && node.name) {
600
- bindings.add(node.name.text);
601
- } else if (ts.isImportDeclaration(node)) {
602
- const namedBindings = node.importClause?.namedBindings;
603
- if (namedBindings && ts.isNamedImports(namedBindings)) {
604
- namedBindings.elements.forEach((element) => {
605
- bindings.add(element.name.text);
606
- });
607
- }
608
- if (node.importClause?.name) {
609
- bindings.add(node.importClause.name.text);
610
- }
611
- }
612
- };
613
- var extractNames = extractNames2, visit = visit2;
614
- const sourceFile = ts.createSourceFile(
615
- "temp.ts",
616
- scriptContent,
617
- ts.ScriptTarget.Latest,
618
- true
619
- );
620
- sourceFile.statements.forEach(visit2);
621
- } catch (err) {
622
- return bindings;
623
- }
624
- return bindings;
625
- }
626
- function checkScriptFunctionCalls(scriptContent, bindings, violations) {
627
- try {
628
- let visit2 = function(node) {
629
- if (ts.isCallExpression(node)) {
630
- const callee = node.expression;
631
- if (ts.isIdentifier(callee)) {
632
- const funcName = callee.text;
633
- const isHookCall = funcName.startsWith("use") && funcName.length > 3 && funcName[3] === funcName[3].toUpperCase();
634
- if (isHookCall) {
635
- if (!bindings.has(funcName)) {
636
- violations.push({
637
- rule: "\u4F7F\u7528\u4E86\u672A\u5BFC\u5165\u7684 Hook",
638
- match: `${funcName}()`,
639
- suggestion: `\u8BF7\u5148\u5BFC\u5165 ${funcName}: import { ${funcName} } from '@ksyun-internal/versatile'`
640
- });
641
- }
642
- }
643
- }
644
- }
645
- ts.forEachChild(node, visit2);
646
- };
647
- var visit = visit2;
648
- const sourceFile = ts.createSourceFile(
649
- "temp.ts",
650
- scriptContent,
651
- ts.ScriptTarget.Latest,
652
- true
653
- );
654
- ts.forEachChild(sourceFile, visit2);
655
- } catch (err) {
656
- }
657
- }
658
- function checkTemplateVariables(template, bindings, violations) {
659
- const templateContent = template.content;
660
- const templateAst = template.ast;
661
- const localVariables = /* @__PURE__ */ new Set();
662
- if (templateAst) {
663
- let walk2 = function(node) {
664
- if (node.type === 0 || node.type === 1) {
665
- const vFor = node.props?.find((p) => p.type === 7 && p.name === "for");
666
- if (vFor && vFor.exp) {
667
- if (vFor.parseResult) {
668
- const { value, key, index } = vFor.parseResult;
669
- if (value) extractIdentifiersFromTemplateAST(value, localVariables);
670
- if (key) extractIdentifiersFromTemplateAST(key, localVariables);
671
- if (index) extractIdentifiersFromTemplateAST(index, localVariables);
672
- } else if (vFor.exp.content) {
673
- extractIdentifiersFromTemplateAST(vFor.exp, localVariables);
674
- }
675
- }
676
- const vSlot = node.props?.find((p) => p.type === 7 && p.name === "slot");
677
- if (vSlot && vSlot.exp) {
678
- extractIdentifiersFromTemplateAST(vSlot.exp, localVariables);
679
- }
680
- if (node.children) {
681
- node.children.forEach(walk2);
682
- }
683
- }
684
- };
685
- walk2(templateAst);
686
- }
687
- const allBindings = /* @__PURE__ */ new Set([...bindings, ...localVariables]);
688
- const interpolationRegex = /\{\{\s*([^}]+)\s*\}\}/g;
689
- let match;
690
- while ((match = interpolationRegex.exec(templateContent)) !== null) {
691
- const expression = match[1].trim();
692
- const { identifiers, locals } = extractIdentifiersFromExpression(expression);
693
- const combinedBindings = /* @__PURE__ */ new Set([...allBindings, ...locals]);
694
- for (const id of identifiers) {
695
- if (!combinedBindings.has(id) && !isTemplateBuiltin(id)) {
696
- violations.push({
697
- rule: `\u6A21\u677F\u5F15\u7528\u4E86\u672A\u5B9A\u4E49\u7684\u53D8\u91CF: ${id}`,
698
- match: `{{ ${expression} }}`,
699
- suggestion: `\u8BF7\u786E\u4FDD\u5728 <script setup> \u4E2D\u5B9A\u4E49\u53D8\u91CF ${id}\uFF0C\u6216\u68C0\u67E5\u62FC\u5199\u662F\u5426\u6B63\u786E`
700
- });
701
- }
702
- }
703
- }
704
- const bindRegex = /(?::|v-bind:)[\w.-]+="([^"]+)"/g;
705
- while ((match = bindRegex.exec(templateContent)) !== null) {
706
- const expression = match[1].trim();
707
- const { identifiers, locals } = extractIdentifiersFromExpression(expression);
708
- const combinedBindings = /* @__PURE__ */ new Set([...allBindings, ...locals]);
709
- for (const id of identifiers) {
710
- if (!combinedBindings.has(id) && !isTemplateBuiltin(id)) {
711
- violations.push({
712
- rule: `\u7ED1\u5B9A\u5C5E\u6027\u5F15\u7528\u4E86\u672A\u5B9A\u4E49\u7684\u53D8\u91CF: ${id}`,
713
- match: match[0],
714
- suggestion: `\u8BF7\u786E\u4FDD\u5728 <script setup> \u4E2D\u5B9A\u4E49\u53D8\u91CF ${id}`
715
- });
716
- }
717
- }
718
- }
719
- const eventRegex = /(?:@|v-on:)[\w.-]+="([^"]+)"/g;
720
- while ((match = eventRegex.exec(templateContent)) !== null) {
721
- const expression = match[1].trim();
722
- const { identifiers, locals } = extractIdentifiersFromExpression(expression);
723
- const combinedBindings = /* @__PURE__ */ new Set([...allBindings, ...locals]);
724
- for (const id of identifiers) {
725
- if (!combinedBindings.has(id) && !isTemplateBuiltin(id)) {
726
- violations.push({
727
- rule: `\u4E8B\u4EF6\u5904\u7406\u5668\u5F15\u7528\u4E86\u672A\u5B9A\u4E49\u7684\u51FD\u6570/\u53D8\u91CF: ${id}`,
728
- match: match[0],
729
- suggestion: `\u8BF7\u786E\u4FDD\u5728 <script setup> \u4E2D\u5B9A\u4E49 ${id}`
730
- });
731
- }
732
- }
733
- }
734
- }
735
- function extractIdentifiersFromTemplateAST(node, set) {
736
- if (!node || typeof node !== "object") return;
737
- const n = node;
738
- if (n.type === 4 && n.content) {
739
- const matches = n.content.match(/\b([a-zA-Z_$][a-zA-Z0-9_$]*)\b/g);
740
- if (matches) {
741
- matches.forEach((m) => {
742
- if (!isJsKeyword(m)) set.add(m);
743
- });
744
- }
745
- } else if (n.type === 8 && n.children) {
746
- n.children.forEach((c) => extractIdentifiersFromTemplateAST(c, set));
747
- }
748
- }
749
- function extractIdentifiersFromExpression(expression) {
750
- const identifiers = [];
751
- const locals = [];
752
- const arrowFuncRegex = /(?:(?:\(([^)]+)\))|([a-zA-Z_$][\w$]*))\s*=>/g;
753
- let arrowMatch;
754
- while ((arrowMatch = arrowFuncRegex.exec(expression)) !== null) {
755
- const params = arrowMatch[1] || arrowMatch[2];
756
- if (params) {
757
- const paramNames = params.split(",").map((p) => p.trim().split(":")[0].trim());
758
- paramNames.forEach((p) => {
759
- const subMatches = p.match(/\b([a-zA-Z_$][\w$]*)\b/g);
760
- if (subMatches) subMatches.forEach((sm) => {
761
- if (!isJsKeyword(sm)) locals.push(sm);
762
- });
763
- });
764
- }
765
- }
766
- const cleanExpr = expression.replace(/'[^']*'/g, "").replace(/"[^"]*"/g, "").replace(/`[^`]*`/g, "");
767
- const idRegex = /(?:^|[^.\w$])([a-zA-Z_$][\w$]*)\b(?!\s*:)/g;
768
- let match;
769
- while ((match = idRegex.exec(cleanExpr)) !== null) {
770
- const id = match[1];
771
- if (!isJsKeyword(id) && !locals.includes(id)) {
772
- identifiers.push(id);
773
- }
774
- }
775
- return { identifiers: [...new Set(identifiers)], locals: [...new Set(locals)] };
776
- }
777
- function isTemplateBuiltin(id) {
778
- return TEMPLATE_BUILTINS.has(id);
779
- }
780
- function isJsKeyword(id) {
781
- return JS_KEYWORDS.has(id);
782
- }
783
- function checkImport(node, violations, sourceFile) {
784
- const moduleSpecifier = node.moduleSpecifier.getText(sourceFile).replace(/['\"]/g, "");
785
- if (moduleSpecifier === "@king-design/vue" || moduleSpecifier === "@ksyun-internal/versatile") {
786
- const namedBindings = node.importClause?.namedBindings;
787
- if (namedBindings && ts.isNamedImports(namedBindings)) {
788
- namedBindings.elements.forEach((element) => {
789
- const originalName = element.propertyName ? element.propertyName.text : element.name.text;
790
- const localName = element.name.text;
791
- const isAlias = !!element.propertyName;
792
- const isHook = originalName.startsWith("use") && originalName.length > 3 && originalName[3] === originalName[3].toUpperCase();
793
- if (isHook) {
794
- if (moduleSpecifier !== "@ksyun-internal/versatile") {
795
- violations.push({
796
- rule: `Hook ${originalName} \u5BFC\u5165\u6E90\u9519\u8BEF`,
797
- match: `import { ${originalName} } from '${moduleSpecifier}'`,
798
- suggestion: `\u5E94\u4ECE '@ksyun-internal/versatile' \u5BFC\u5165`
799
- });
800
- return;
801
- }
802
- if (!hooksRegistry.isKnownHook(originalName)) {
803
- violations.push({
804
- rule: "\u5F15\u7528\u4E86\u4E0D\u5B58\u5728\u7684 Hook",
805
- match: originalName,
806
- suggestion: `Hook ${originalName} \u4E0D\u5B58\u5728\u4E8E @ksyun-internal/versatile\u3002\u53EF\u7528\u7684 Hooks: ${hooksRegistry.getAllHookNames().join(", ") || "\u6682\u65E0"}`
807
- });
808
- return;
809
- }
810
- if (isAlias) {
811
- violations.push({
812
- rule: `\u7981\u6B62\u5BF9 Hook ${originalName} \u4F7F\u7528\u522B\u540D\u5BFC\u5165`,
813
- match: `${originalName} as ${localName}`,
814
- suggestion: `\u76F4\u63A5\u4F7F\u7528\u539F\u540D: import { ${originalName} } from '${moduleSpecifier}'`
815
- });
816
- }
817
- return;
818
- }
819
- if (isAlias) {
820
- violations.push({
821
- rule: `\u7981\u6B62\u5BF9\u7EC4\u4EF6 ${originalName} \u4F7F\u7528\u522B\u540D\u5BFC\u5165`,
822
- match: `${originalName} as ${localName}`,
823
- suggestion: `\u76F4\u63A5\u4F7F\u7528\u539F\u540D: import { ${originalName} } from '${moduleSpecifier}'`
824
- });
825
- }
826
- if (componentRegistry.isKnownComponent(originalName)) {
827
- if (DUAL_EXPORT_COMPONENTS.has(originalName)) {
828
- return;
829
- }
830
- const isVersatile = VERSATILE_COMPONENTS.has(originalName);
831
- const expectedPackage = isVersatile ? "@ksyun-internal/versatile" : "@king-design/vue";
832
- if (expectedPackage !== moduleSpecifier) {
833
- violations.push({
834
- rule: `\u7EC4\u4EF6 ${originalName} \u5BFC\u5165\u6E90\u9519\u8BEF`,
835
- match: `import { ... } from '${moduleSpecifier}'`,
836
- suggestion: `\u5E94\u4ECE '${expectedPackage}' \u5BFC\u5165`
837
- });
838
- }
839
- } else {
840
- violations.push({
841
- rule: "\u5F15\u7528\u4E86\u4E0D\u5B58\u5728\u7684\u7EC4\u4EF6/\u5BFC\u51FA",
842
- match: originalName,
843
- suggestion: `\u8BF7\u786E\u8BA4 ${originalName} \u662F\u5426\u5B58\u5728\u4E8E ${moduleSpecifier}\u3002\u5EFA\u8BAE\u67E5\u770B\u6587\u6863\u3002`
844
- });
845
- }
846
- });
847
- }
848
- }
849
- }
850
- function checkNestingRules(tagName, metadata, ancestors, violations) {
851
- const requiredParents = metadata?.requiredParent ? [metadata.requiredParent] : NESTING_RULES[tagName];
852
- if (!requiredParents || requiredParents.length === 0) return;
853
- const hasValidAncestor = ancestors.some(
854
- (ancestor) => requiredParents.includes(ancestor) || ancestor === "template"
855
- );
856
- if (!hasValidAncestor && ancestors.length > 0) {
857
- violations.push({
858
- rule: `${tagName} \u5FC5\u987B\u653E\u5728 ${requiredParents.join(" \u6216 ")} \u7EC4\u4EF6\u5185`,
859
- match: tagName,
860
- suggestion: `\u8BF7\u5C06 ${tagName} \u79FB\u52A8\u5230 ${requiredParents[0]} \u7EC4\u4EF6\u4E2D`
861
- });
862
- }
863
- }
864
- function checkComponentSpecificRules(tagName, node, violations) {
865
- if (tagName === "Dropdown") {
866
- node.children?.forEach((child) => {
867
- if (child.type === 1 && child.tag === "template") {
868
- const slotProp = child.props?.find(
869
- (p) => p.type === 7 && p.name === "slot" && p.arg?.content === "menu"
870
- );
871
- if (slotProp) {
872
- violations.push({
873
- rule: "DropdownMenu \u4E0D\u80FD\u653E\u5728 #menu \u63D2\u69FD\u4E2D",
874
- match: "<template #menu>",
875
- suggestion: "\u8BF7\u79FB\u9664 <template #menu>\uFF0C\u5C06 DropdownMenu \u76F4\u63A5\u4F5C\u4E3A Dropdown \u7684\u7B2C\u4E8C\u4E2A\u5B50\u5143\u7D20"
876
- });
877
- }
878
- }
879
- });
880
- }
881
- if (tagName === "Table") {
882
- node.props.forEach((prop) => {
883
- if (prop.type === 6 && (prop.name === "rowKey" || prop.name === "row-key")) {
884
- violations.push({
885
- rule: "Table\u7EC4\u4EF6 rowKey \u5C5E\u6027 usage \u9519\u8BEF",
886
- match: `rowKey="${prop.value?.content || ""}"`,
887
- 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"'
888
- });
889
- }
890
- });
891
- }
892
- if (tagName === "Icon") {
893
- node.props.forEach((prop) => {
894
- if (prop.type === 6 && prop.name === "name") {
895
- violations.push({
896
- rule: "Icon \u7EC4\u4EF6\u7981\u6B62\u4F7F\u7528 name \u5C5E\u6027",
897
- match: `name="${prop.value?.content || ""}"`,
898
- suggestion: '\u8BF7\u4F7F\u7528 class \u5C5E\u6027\u6307\u5B9A\u56FE\u6807\uFF0C\u4F8B\u5982: class="k-icon-search"'
899
- });
900
- }
901
- });
902
- }
903
- }
904
- function checkProps(tagName, node, metadata, violations) {
905
- let customTagName = null;
906
- const tagNameProp = node.props.find(
907
- (p) => p.type === 6 && p.name === "tagName" || p.type === 7 && p.name === "bind" && p.arg?.content === "tagName"
908
- );
909
- if (tagNameProp?.type === 6 && tagNameProp.value?.content) {
910
- customTagName = tagNameProp.value.content;
911
- }
912
- const dynamicAllowedProps = [];
913
- if (customTagName === "a") {
914
- dynamicAllowedProps.push("href", "target", "rel", "download");
915
- }
916
- node.props.forEach((prop) => {
917
- if (prop.type !== 6) return;
918
- const propName = prop.name;
919
- const camelCaseProp = propName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
920
- const isPropValid = metadata.props?.some(
921
- (p) => p.name === propName || p.name === camelCaseProp || p.name.toLowerCase() === propName.toLowerCase()
922
- );
923
- const isDynamicallyAllowed = dynamicAllowedProps.includes(propName);
924
- const isBuiltinProp = ["class", "style", "key", "ref"].includes(propName) || propName.startsWith("data-");
925
- const isNativeHtmlAttr = NATIVE_HTML_ATTRIBUTES.has(propName) || propName.startsWith("aria-");
926
- if (!isPropValid && !isDynamicallyAllowed && !isBuiltinProp && !isNativeHtmlAttr) {
927
- if (metadata.props && metadata.props.length > 0) {
928
- violations.push({
929
- rule: `\u5C5E\u6027 ${propName} \u4E0D\u5B58\u5728\u4E8E ${tagName}`,
930
- match: propName,
931
- suggestion: `\u53EF\u7528\u5C5E\u6027: ${metadata.props.map((p) => p.name).join(", ")}`
932
- });
933
- }
934
- }
935
- if (isPropValid && metadata.props) {
936
- const targetProp = metadata.props.find(
937
- (p) => p.name === propName || p.name === camelCaseProp || p.name.toLowerCase() === propName.toLowerCase()
938
- );
939
- if (targetProp) {
940
- const attrValue = prop.value?.content;
941
- if (attrValue) {
942
- let cleanAllowedValues = [];
943
- if (targetProp.allowedValues && targetProp.allowedValues.length > 0) {
944
- cleanAllowedValues = targetProp.allowedValues.map((av) => String(av.value).replace(/['"]/g, ""));
945
- } else if (targetProp.type?.kind === "union" && targetProp.type.unionTypes) {
946
- cleanAllowedValues = targetProp.type.unionTypes.map((v) => String(v).replace(/['"]/g, ""));
947
- }
948
- if (cleanAllowedValues.length > 0 && !cleanAllowedValues.includes(attrValue)) {
949
- violations.push({
950
- rule: `${tagName} \u7684\u5C5E\u6027 ${propName} \u7684\u503C "${attrValue}" \u65E0\u6548`,
951
- match: `${propName}="${attrValue}"`,
952
- suggestion: `\u5141\u8BB8\u7684\u503C: ${cleanAllowedValues.join(" | ")}`
953
- });
954
- }
955
- }
956
- }
957
- }
958
- });
959
- }
960
- function checkEvents(tagName, node, metadata, violations) {
961
- node.props.forEach((prop) => {
962
- if (prop.type !== 7) return;
963
- if (prop.name !== "on") return;
964
- const arg = prop.arg;
965
- const eventName = arg && "content" in arg ? arg.content : null;
966
- if (!eventName) return;
967
- if (eventName.startsWith("$change:") || eventName.startsWith("update:")) {
968
- return;
969
- }
970
- const camelCaseEvent = eventName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
971
- const isEventValid = metadata.events?.some(
972
- (e) => e.name === eventName || e.name === camelCaseEvent || e.name.toLowerCase() === eventName.toLowerCase()
973
- );
974
- const isNativeEvent = NATIVE_EVENTS.has(eventName);
975
- if (!isEventValid && !isNativeEvent && metadata.events && metadata.events.length > 0) {
976
- violations.push({
977
- rule: `\u4E8B\u4EF6 @${eventName} \u4E0D\u5B58\u5728\u4E8E ${tagName}`,
978
- match: `@${eventName}`,
979
- suggestion: `\u53EF\u7528\u4E8B\u4EF6: ${metadata.events.map((e) => e.name).join(", ") || "\u65E0\u81EA\u5B9A\u4E49\u4E8B\u4EF6"}`
980
- });
981
- }
982
- });
983
- }
984
- function checkSlots(tagName, node, metadata, violations) {
985
- if (!node.children || !metadata.slots?.length) return;
986
- node.children.forEach((child) => {
987
- if (child.type !== 1 || child.tag !== "template") return;
988
- child.props.forEach((prop) => {
989
- if (prop.type !== 7 || prop.name !== "slot") return;
990
- const slotName = prop.arg?.content || "default";
991
- const isSlotValid = metadata.slots?.some((s) => s.name === slotName);
992
- if (!isSlotValid) {
993
- violations.push({
994
- rule: `\u63D2\u69FD #${slotName} \u4E0D\u5B58\u5728\u4E8E ${tagName}`,
995
- match: `#${slotName}`,
996
- suggestion: `\u53EF\u7528\u63D2\u69FD: ${metadata.slots.map((s) => s.name).join(", ")}`
997
- });
998
- }
999
- });
1000
- });
1001
- }
1002
- function checkTemplate(node, violations, ancestors) {
1003
- if (node.type === 1) {
1004
- const elementNode = node;
1005
- const tagName = elementNode.tag;
1006
- const metadata = componentRegistry.isKnownComponent(tagName) ? componentRegistry.getComponent(tagName) : null;
1007
- if (metadata) {
1008
- checkNestingRules(tagName, metadata, ancestors, violations);
1009
- }
1010
- checkComponentSpecificRules(tagName, elementNode, violations);
1011
- if (metadata) {
1012
- checkProps(tagName, elementNode, metadata, violations);
1013
- checkEvents(tagName, elementNode, metadata, violations);
1014
- checkSlots(tagName, elementNode, metadata, violations);
1015
- }
1016
- }
1017
- if ("children" in node && Array.isArray(node.children)) {
1018
- const newAncestors = node.type === 1 && "tag" in node ? [...ancestors, node.tag] : ancestors;
1019
- node.children.forEach((child) => {
1020
- if (typeof child === "object" && child !== null && "type" in child) {
1021
- checkTemplate(child, violations, newAncestors);
1022
- }
1023
- });
1024
- }
1025
- }
1026
-
1027
- export { analyzeCodeWithAST, componentRegistry };