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