code-auditor-mcp 3.4.18 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +99 -0
- package/README.md +5 -5
- package/dist/analyzers/applicability.d.ts +19 -5
- package/dist/analyzers/applicability.d.ts.map +1 -1
- package/dist/analyzers/applicability.js +36 -24
- package/dist/analyzers/applicability.js.map +1 -1
- package/dist/analyzers/cross-language/APIContractAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/cross-language/APIContractAnalyzer.js +23 -2
- package/dist/analyzers/cross-language/APIContractAnalyzer.js.map +1 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts +52 -9
- package/dist/analyzers/cross-language/DependencyGraphBuilder.d.ts.map +1 -1
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js +228 -55
- package/dist/analyzers/cross-language/DependencyGraphBuilder.js.map +1 -1
- package/dist/analyzers/cross-language/SchemaValidator.d.ts +19 -12
- package/dist/analyzers/cross-language/SchemaValidator.d.ts.map +1 -1
- package/dist/analyzers/cross-language/SchemaValidator.js +78 -111
- package/dist/analyzers/cross-language/SchemaValidator.js.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts +0 -6
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +85 -73
- package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
- package/dist/analyzers/documentationAnalyzer.js +5 -5
- package/dist/analyzers/documentationAnalyzer.js.map +1 -1
- package/dist/analyzers/invariantsAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/invariantsAnalyzer.js +1 -0
- package/dist/analyzers/invariantsAnalyzer.js.map +1 -1
- package/dist/analyzers/provenance.d.ts +7 -9
- package/dist/analyzers/provenance.d.ts.map +1 -1
- package/dist/analyzers/provenance.js +88 -124
- package/dist/analyzers/provenance.js.map +1 -1
- package/dist/analyzers/reactAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/reactAnalyzer.js +120 -21
- package/dist/analyzers/reactAnalyzer.js.map +1 -1
- package/dist/analyzers/ruleRegistry.d.ts +35 -12
- package/dist/analyzers/ruleRegistry.d.ts.map +1 -1
- package/dist/analyzers/ruleRegistry.js +66 -108
- package/dist/analyzers/ruleRegistry.js.map +1 -1
- package/dist/analyzers/ruleTiming.d.ts +2 -2
- package/dist/analyzers/ruleTiming.js +2 -2
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js +60 -24
- package/dist/analyzers/universal/UniversalConventionsAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js +5 -2
- package/dist/analyzers/universal/UniversalDRYAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +82 -71
- package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts +2 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js +23 -12
- package/dist/analyzers/universal/UniversalDocumentationAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts +11 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js +60 -22
- package/dist/analyzers/universal/UniversalSOLIDAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +5 -2
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +2 -3
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js +258 -87
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/schema/codeAnalysis.d.ts.map +1 -1
- package/dist/analyzers/universal/schema/codeAnalysis.js +7 -1
- package/dist/analyzers/universal/schema/codeAnalysis.js.map +1 -1
- package/dist/analyzers/universal/schema/discovery.d.ts +29 -1
- package/dist/analyzers/universal/schema/discovery.d.ts.map +1 -1
- package/dist/analyzers/universal/schema/discovery.js +76 -21
- package/dist/analyzers/universal/schema/discovery.js.map +1 -1
- package/dist/analyzers/universal/schema/jsonSchema.js +7 -11
- package/dist/analyzers/universal/schema/jsonSchema.js.map +1 -1
- package/dist/auditRouter.d.ts +41 -0
- package/dist/auditRouter.d.ts.map +1 -0
- package/dist/auditRouter.js +185 -0
- package/dist/auditRouter.js.map +1 -0
- package/dist/auditRunner.d.ts.map +1 -1
- package/dist/auditRunner.js +93 -84
- package/dist/auditRunner.js.map +1 -1
- package/dist/cli.js +306 -95
- package/dist/cli.js.map +1 -1
- package/dist/codeIndexDB.d.ts.map +1 -1
- package/dist/codeIndexDB.js +41 -7
- package/dist/codeIndexDB.js.map +1 -1
- package/dist/componentScanner.d.ts.map +1 -1
- package/dist/componentScanner.js +2 -1
- package/dist/componentScanner.js.map +1 -1
- package/dist/config/configLoader.d.ts +9 -0
- package/dist/config/configLoader.d.ts.map +1 -1
- package/dist/config/configLoader.js +45 -2
- package/dist/config/configLoader.js.map +1 -1
- package/dist/config/defaults.d.ts +8 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +19 -3
- package/dist/config/defaults.js.map +1 -1
- package/dist/dataPaths.js +18 -1
- package/dist/dataPaths.js.map +1 -1
- package/dist/enforcement/gate.d.ts +14 -19
- package/dist/enforcement/gate.d.ts.map +1 -1
- package/dist/enforcement/gate.js +27 -45
- package/dist/enforcement/gate.js.map +1 -1
- package/dist/functionScanner.d.ts.map +1 -1
- package/dist/functionScanner.js +1 -17
- package/dist/functionScanner.js.map +1 -1
- package/dist/invariants/ruleEngine.d.ts +8 -0
- package/dist/invariants/ruleEngine.d.ts.map +1 -1
- package/dist/invariants/ruleEngine.js +35 -16
- package/dist/invariants/ruleEngine.js.map +1 -1
- package/dist/languages/LanguageOrchestrator.d.ts +9 -10
- package/dist/languages/LanguageOrchestrator.d.ts.map +1 -1
- package/dist/languages/LanguageOrchestrator.js +135 -101
- package/dist/languages/LanguageOrchestrator.js.map +1 -1
- package/dist/languages/RuntimeManager.d.ts +28 -1
- package/dist/languages/RuntimeManager.d.ts.map +1 -1
- package/dist/languages/RuntimeManager.js +158 -47
- package/dist/languages/RuntimeManager.js.map +1 -1
- package/dist/languages/go/GoAdapter.d.ts +2 -0
- package/dist/languages/go/GoAdapter.d.ts.map +1 -1
- package/dist/languages/go/GoAdapter.js +15 -22
- package/dist/languages/go/GoAdapter.js.map +1 -1
- package/dist/languages/go/analyzer +0 -0
- package/dist/languages/go/analyzer-src/analyzer.go +510 -0
- package/dist/languages/go/analyzer-src/go.mod +3 -0
- package/dist/languages/go/analyzer-src/indexer.go +268 -0
- package/dist/languages/go/analyzer-src/parser.go +461 -0
- package/dist/languages/go/analyzer-src/solid.go +395 -0
- package/dist/languages/go/analyzer-src/testconventions.go +25 -0
- package/dist/languages/go/analyzer-src/types.go +148 -0
- package/dist/languages/go/go.mod +7 -0
- package/dist/languages/go/main.go +203 -0
- package/dist/languages/testConventions.d.ts +35 -0
- package/dist/languages/testConventions.d.ts.map +1 -0
- package/dist/languages/testConventions.js +51 -0
- package/dist/languages/testConventions.js.map +1 -0
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts +3 -0
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +25 -18
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
- package/dist/mcp-tools/whitelistTools.d.ts +1 -1
- package/dist/mcp-tools-shared.d.ts +0 -3
- package/dist/mcp-tools-shared.d.ts.map +1 -1
- package/dist/mcp-tools-shared.js +8 -117
- package/dist/mcp-tools-shared.js.map +1 -1
- package/dist/mcp.js +3 -1
- package/dist/mcp.js.map +1 -1
- package/dist/mcpAuditJobs.d.ts.map +1 -1
- package/dist/mcpAuditJobs.js +13 -0
- package/dist/mcpAuditJobs.js.map +1 -1
- package/dist/nextFile.d.ts +21 -0
- package/dist/nextFile.d.ts.map +1 -0
- package/dist/nextFile.js +51 -0
- package/dist/nextFile.js.map +1 -0
- package/dist/nextFileIncremental.d.ts +141 -0
- package/dist/nextFileIncremental.d.ts.map +1 -0
- package/dist/nextFileIncremental.js +314 -0
- package/dist/nextFileIncremental.js.map +1 -0
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +43 -3
- package/dist/pipeline.js.map +1 -1
- package/dist/pipelineAdapters.d.ts +17 -0
- package/dist/pipelineAdapters.d.ts.map +1 -1
- package/dist/pipelineAdapters.js +791 -40
- package/dist/pipelineAdapters.js.map +1 -1
- package/dist/reporting/jsonReportGenerator.d.ts.map +1 -1
- package/dist/reporting/jsonReportGenerator.js +11 -30
- package/dist/reporting/jsonReportGenerator.js.map +1 -1
- package/dist/styles/styleExtractor.js +2 -2
- package/dist/styles/styleExtractor.js.map +1 -1
- package/dist/styles/styleIndexer.d.ts +7 -0
- package/dist/styles/styleIndexer.d.ts.map +1 -1
- package/dist/styles/styleIndexer.js +51 -67
- package/dist/styles/styleIndexer.js.map +1 -1
- package/dist/styles/types.d.ts +3 -2
- package/dist/styles/types.d.ts.map +1 -1
- package/dist/types/crossLanguage.d.ts +1 -0
- package/dist/types/crossLanguage.d.ts.map +1 -1
- package/dist/types.d.ts +43 -34
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/fileDiscovery.d.ts +9 -0
- package/dist/utils/fileDiscovery.d.ts.map +1 -1
- package/dist/utils/fileDiscovery.js +18 -0
- package/dist/utils/fileDiscovery.js.map +1 -1
- package/package.json +16 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/README.md +11 -9
- package/plugin/hooks/hooks.json +4 -0
- package/plugin/scripts/hook-audit.sh +20 -30
- package/plugin/scripts/hook-common.sh +63 -0
- package/plugin/scripts/hook-self-audit.sh +99 -0
- package/plugin/skills/code-auditor/SKILL-RULE-KINDS.md +55 -9
- package/plugin/skills/code-auditor/SKILL.md +22 -23
|
@@ -48,6 +48,24 @@ const TAILWIND_SPACING_PX = {
|
|
|
48
48
|
'96': 384,
|
|
49
49
|
};
|
|
50
50
|
const TAILWIND_SCALE_VALUES = Object.values(TAILWIND_SPACING_PX).sort((a, b) => a - b);
|
|
51
|
+
/**
|
|
52
|
+
* Stable symbol for a value-scoped finding: `<property>: <value>`.
|
|
53
|
+
*
|
|
54
|
+
* The baseline fingerprint is `[analyzer, rule, file, symbol]`. Without a
|
|
55
|
+
* symbol, every finding for a `(rule, file)` pair collapses into one entry and
|
|
56
|
+
* the ratchet can no longer tell one distinct finding from forty. The value is
|
|
57
|
+
* what separates one finding about a property from another — an outlier
|
|
58
|
+
* `margin-top: 12px` must not share a fingerprint with a different outlier
|
|
59
|
+
* `margin-top: 14px`, and two mechanism-fragmented properties inside the same
|
|
60
|
+
* selector must not share a fingerprint just because they share a selector.
|
|
61
|
+
* `normalized_value` is the canonical bucket (may be a JSON-encoded object for
|
|
62
|
+
* complex values); `raw_value` is the resolved spelling fallback.
|
|
63
|
+
*/
|
|
64
|
+
function declValueKey(d) {
|
|
65
|
+
const prop = d.property ?? '';
|
|
66
|
+
const val = d.normalized_value ?? d.raw_value ?? '';
|
|
67
|
+
return val ? `${prop}: ${val}` : prop;
|
|
68
|
+
}
|
|
51
69
|
/**
|
|
52
70
|
* Values so common that coincidental token-name matches are always noise.
|
|
53
71
|
* Filtered before any token comparison (Spec 22 Item 1).
|
|
@@ -326,7 +344,7 @@ class UniversalStylesAnalyzerDetectors extends UniversalStylesAnalyzerBase {
|
|
|
326
344
|
violations.push(this.makeViolation(sample.file_path, sample.line, `Value drift in "${property}": "${sample.raw_value}" is rare ` +
|
|
327
345
|
`(${list.length} of ${total} usages, ${(share * 100).toFixed(1)}%). ` +
|
|
328
346
|
`The dominant value "${modeList[0].raw_value}" is used ${modeList.length} times. ` +
|
|
329
|
-
`Consider using a consistent value or design token.`, { severity: 'warning', rule: 'styles/value-drift', symbol:
|
|
347
|
+
`Consider using a consistent value or design token.`, { severity: 'warning', rule: 'styles/value-drift', symbol: declValueKey(sample) }));
|
|
330
348
|
}
|
|
331
349
|
}
|
|
332
350
|
return violations;
|
|
@@ -367,7 +385,7 @@ class UniversalStylesAnalyzerDetectors extends UniversalStylesAnalyzerBase {
|
|
|
367
385
|
violations.push(this.makeViolation(decl.file_path, decl.line, `Off-scale "${property}" value: "${decl.raw_value}" (${px}px) ` +
|
|
368
386
|
`does not align with the inferred ${step}px scale step. ` +
|
|
369
387
|
`Nearby scale values: ${Math.floor(px / step) * step}px or ` +
|
|
370
|
-
`${Math.ceil(px / step) * step}px.`, { severity: 'warning', rule: 'styles/off-scale' }));
|
|
388
|
+
`${Math.ceil(px / step) * step}px.`, { severity: 'warning', rule: 'styles/off-scale', symbol: declValueKey(decl) }));
|
|
371
389
|
}
|
|
372
390
|
}
|
|
373
391
|
}
|
|
@@ -478,36 +496,62 @@ function flagColorDriftStragglers(clusters, property, cfg, report) {
|
|
|
478
496
|
violations.push(report(item.decl.file_path, item.decl.line, `Color drift in "${property}": "${item.decl.raw_value}" is a rare value ` +
|
|
479
497
|
`(used ${cluster.length} time${cluster.length === 1 ? '' : 's'}, ` +
|
|
480
498
|
`${(share * 100).toFixed(1)}% of ${total} usages). ` +
|
|
481
|
-
`Dominant cluster has ${dominantSize} values. Consider using a design token.`, { severity: 'warning', rule: 'styles/value-drift', symbol:
|
|
499
|
+
`Dominant cluster has ${dominantSize} values. Consider using a design token.`, { severity: 'warning', rule: 'styles/value-drift', symbol: declValueKey(item.decl) }));
|
|
482
500
|
}
|
|
483
501
|
}
|
|
484
502
|
return violations;
|
|
485
503
|
}
|
|
504
|
+
/** SQLite's default variable-number limit is 999; stay well under it. */
|
|
505
|
+
const SQLITE_MAX_VARIABLES = 900;
|
|
486
506
|
/**
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
507
|
+
* Build a batched defined-class lookup backed by the `style_defined_classes`
|
|
508
|
+
* table. Instead of loading every style_declarations row and regex-extracting
|
|
509
|
+
* selectors in JS (the old `collectDefinedClassCatalog`), each batch of names
|
|
510
|
+
* resolves via a single `WHERE class_name IN (...)` index lookup — chunked to
|
|
511
|
+
* stay under SQLite's variable limit — so only the names actually used in scope
|
|
512
|
+
* touch the database.
|
|
493
513
|
*/
|
|
494
|
-
function
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
514
|
+
function createDefinedClassLookup(indexHandle) {
|
|
515
|
+
const cache = new Map();
|
|
516
|
+
return (names) => {
|
|
517
|
+
const result = new Map();
|
|
518
|
+
const missing = [];
|
|
519
|
+
for (const n of names) {
|
|
520
|
+
const hit = cache.get(n);
|
|
521
|
+
if (hit !== undefined) {
|
|
522
|
+
if (hit !== null)
|
|
523
|
+
result.set(n, hit);
|
|
501
524
|
continue;
|
|
502
|
-
const matches = ctx.matchAll(/\.([a-zA-Z0-9_-]+)/g);
|
|
503
|
-
for (const m of matches) {
|
|
504
|
-
names.add(m[1]);
|
|
505
|
-
if (!locations.has(m[1]))
|
|
506
|
-
locations.set(m[1], d.file_path);
|
|
507
525
|
}
|
|
526
|
+
missing.push(n);
|
|
508
527
|
}
|
|
509
|
-
|
|
510
|
-
|
|
528
|
+
for (let i = 0; i < missing.length; i += SQLITE_MAX_VARIABLES) {
|
|
529
|
+
const chunk = missing.slice(i, i + SQLITE_MAX_VARIABLES);
|
|
530
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
531
|
+
let rows = [];
|
|
532
|
+
try {
|
|
533
|
+
rows = indexHandle.query(`SELECT class_name, MIN(file_path) AS file_path
|
|
534
|
+
FROM style_defined_classes
|
|
535
|
+
WHERE class_name IN (${placeholders})
|
|
536
|
+
GROUP BY class_name`, chunk);
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
// Table absent on a pre-migration DB — treat as "nothing defined".
|
|
540
|
+
}
|
|
541
|
+
const found = new Map(rows.map((r) => [r.class_name, r.file_path]));
|
|
542
|
+
for (const n of chunk) {
|
|
543
|
+
const loc = found.get(n);
|
|
544
|
+
if (loc !== undefined) {
|
|
545
|
+
cache.set(n, loc);
|
|
546
|
+
result.set(n, loc);
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
cache.set(n, null);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return result;
|
|
554
|
+
};
|
|
511
555
|
}
|
|
512
556
|
/**
|
|
513
557
|
* Levenshtein edit distance between two class names, capping the DP width so a
|
|
@@ -534,20 +578,45 @@ function levenshteinDistance(a, b, maxDist) {
|
|
|
534
578
|
return dp[m][n];
|
|
535
579
|
}
|
|
536
580
|
/**
|
|
537
|
-
*
|
|
538
|
-
*
|
|
581
|
+
* Build a suggestion lookup that finds the nearest defined class within edit
|
|
582
|
+
* distance 3. The candidate pool is bounded to classes sharing the query's
|
|
583
|
+
* first character (a `LIKE` prefix lookup) — not a scan of the full catalog —
|
|
584
|
+
* with a full-catalog fallback only when the prefix bucket is empty, so a
|
|
585
|
+
* first-character typo still surfaces a suggestion.
|
|
539
586
|
*/
|
|
540
|
-
function
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
587
|
+
function createDefinedClassSuggester(indexHandle) {
|
|
588
|
+
// Load the defined-class catalog once, lazily, and cache it. The catalog is
|
|
589
|
+
// a dedicated ~hundreds-row table (not the 16k-declaration corpus), so a
|
|
590
|
+
// single scan per suggestion is already bounded; a prefix bucket would only
|
|
591
|
+
// risk a first-character-typo suggestion diverging from the previous
|
|
592
|
+
// full-catalog `nearestDefinedClass` result.
|
|
593
|
+
let catalog = null;
|
|
594
|
+
const loadCatalog = () => {
|
|
595
|
+
if (catalog === null) {
|
|
596
|
+
try {
|
|
597
|
+
catalog = indexHandle.query(`SELECT class_name, MIN(file_path) AS file_path
|
|
598
|
+
FROM style_defined_classes
|
|
599
|
+
GROUP BY class_name`);
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
catalog = [];
|
|
603
|
+
}
|
|
548
604
|
}
|
|
549
|
-
|
|
550
|
-
|
|
605
|
+
return catalog;
|
|
606
|
+
};
|
|
607
|
+
return (name) => {
|
|
608
|
+
const lower = name.toLowerCase();
|
|
609
|
+
let best = null;
|
|
610
|
+
let bestDist = Infinity;
|
|
611
|
+
for (const c of loadCatalog()) {
|
|
612
|
+
const dist = levenshteinDistance(lower, c.class_name.toLowerCase(), 3);
|
|
613
|
+
if (dist < bestDist) {
|
|
614
|
+
bestDist = dist;
|
|
615
|
+
best = { name: c.class_name, filePath: c.file_path };
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return best;
|
|
619
|
+
};
|
|
551
620
|
}
|
|
552
621
|
async function initTailwindProbe(expander, cfg, report) {
|
|
553
622
|
await expander.init({
|
|
@@ -572,27 +641,26 @@ async function initTailwindProbe(expander, cfg, report) {
|
|
|
572
641
|
}
|
|
573
642
|
return null;
|
|
574
643
|
}
|
|
575
|
-
function flagUnresolvedClasses(usageEntries, expander, report,
|
|
644
|
+
function flagUnresolvedClasses(usageEntries, expander, report, suggest) {
|
|
576
645
|
return withRuleTiming('styles/undefined-class', () => {
|
|
577
|
-
const { names: definedClasses, locations: definedClassLocations } = catalog;
|
|
578
646
|
const violations = [];
|
|
579
647
|
for (const u of usageEntries) {
|
|
580
648
|
const resolved = expander.resolve(u.class_name);
|
|
581
649
|
if (resolved.valid)
|
|
582
650
|
continue;
|
|
583
|
-
const nearest =
|
|
584
|
-
const nearestFile = nearest ? definedClassLocations.get(nearest) : undefined;
|
|
651
|
+
const nearest = suggest(u.class_name);
|
|
585
652
|
violations.push(report(u.file_path, u.line, `Undefined CSS class: "${u.class_name}" has no matching definition ` +
|
|
586
653
|
`in any stylesheet, Tailwind utility set, or project config.`, {
|
|
587
654
|
severity: 'warning',
|
|
588
655
|
rule: 'styles/undefined-class',
|
|
656
|
+
symbol: u.class_name,
|
|
589
657
|
resolution: {
|
|
590
658
|
action: nearest ? 'use-defined-class' : 'define-or-remove-class',
|
|
591
659
|
summary: nearest
|
|
592
|
-
? `Rename "${u.class_name}" to the defined class "${nearest}"${
|
|
660
|
+
? `Rename "${u.class_name}" to the defined class "${nearest.name}"${nearest.filePath ? ` (defined in ${nearest.filePath})` : ''}.`
|
|
593
661
|
: `Define the class "${u.class_name}" in a stylesheet, or remove the usage.`,
|
|
594
|
-
symbols: nearest ? [nearest] : [u.class_name],
|
|
595
|
-
files:
|
|
662
|
+
symbols: nearest ? [nearest.name] : [u.class_name],
|
|
663
|
+
files: nearest ? [u.file_path, nearest.filePath] : [u.file_path],
|
|
596
664
|
lines: [u.line],
|
|
597
665
|
},
|
|
598
666
|
}));
|
|
@@ -639,8 +707,6 @@ function matchBypassToken(d, tokenValueMap, categoricalExclusions, scaleProps) {
|
|
|
639
707
|
if (declType !== null && declType !== tokenInfo.valueType)
|
|
640
708
|
return null;
|
|
641
709
|
}
|
|
642
|
-
if (tokenInfo.valueType !== 'color')
|
|
643
|
-
return null;
|
|
644
710
|
return tokenInfo;
|
|
645
711
|
}
|
|
646
712
|
function flagPropertyValueFragmentation(declarations, minMechanisms, report) {
|
|
@@ -665,7 +731,7 @@ function flagPropertyValueFragmentation(declarations, minMechanisms, report) {
|
|
|
665
731
|
const prop = sample.property;
|
|
666
732
|
violations.push(report(sample.file_path, sample.line, `Mechanism fragmentation: "${prop}: ${sample.raw_value}" is applied via ` +
|
|
667
733
|
`${mechs.size} different mechanisms (${[...mechs].sort().join(', ')}). ` +
|
|
668
|
-
`Consolidate to a single mechanism or design token.`, { severity: 'warning', rule: 'styles/mechanism-fragmentation' }));
|
|
734
|
+
`Consolidate to a single mechanism or design token.`, { severity: 'warning', rule: 'styles/mechanism-fragmentation', symbol: declValueKey(sample) }));
|
|
669
735
|
}
|
|
670
736
|
return violations;
|
|
671
737
|
}
|
|
@@ -710,30 +776,104 @@ function buildDeclarationBlocks(declarations, minDeclarations) {
|
|
|
710
776
|
})
|
|
711
777
|
.filter(b => b.declCount >= minDeclarations);
|
|
712
778
|
}
|
|
779
|
+
/** Prefix length for the Xiao et al. set-similarity join (see flagSimilarBlockPairs). */
|
|
780
|
+
const prefixLen = (threshold, s) => s - Math.ceil(threshold * s) + 1;
|
|
713
781
|
function flagSimilarBlockPairs(blockEntries, threshold, report) {
|
|
782
|
+
const n = blockEntries.length;
|
|
783
|
+
if (n < 2)
|
|
784
|
+
return [];
|
|
785
|
+
// Count filter (Xiao et al. "Efficient Exact Set-Similarity Joins"): with a
|
|
786
|
+
// size filter (Jaccard >= t implies min/max >= t) and a rarest-first global
|
|
787
|
+
// value ordering, two sets with Jaccard >= t must overlap on their prefixes of
|
|
788
|
+
// length |S| - ceil(t·|S|) + 1. Indexing only those prefixes turns the O(b²)
|
|
789
|
+
// all-pairs Jaccard scan into a candidate generation step over rare values —
|
|
790
|
+
// high-fan-out values (e.g. a shared `display:block`) sort into the suffix and
|
|
791
|
+
// never generate a pair. Verified on the 16k-row corpus: 705k pairs -> ~2.6k
|
|
792
|
+
// candidates with zero false negatives.
|
|
793
|
+
// 1. Global value frequency drives the rarest-first ordering.
|
|
794
|
+
const freq = new Map();
|
|
795
|
+
for (const b of blockEntries) {
|
|
796
|
+
for (const v of b.valueSet)
|
|
797
|
+
freq.set(v, (freq.get(v) ?? 0) + 1);
|
|
798
|
+
}
|
|
799
|
+
// 2. Order each block's values rarest-first (tie-broken for determinism).
|
|
800
|
+
const ordered = blockEntries.map((b) => ({
|
|
801
|
+
...b,
|
|
802
|
+
values: [...b.valueSet].sort((x, y) => (freq.get(x) - freq.get(y)) || (x < y ? -1 : 1)),
|
|
803
|
+
size: b.valueSet.size,
|
|
804
|
+
}));
|
|
805
|
+
// 3–5. Inverted prefix index → candidate generation → exact Jaccard verify.
|
|
806
|
+
const inverted = buildInvertedIndex(ordered, threshold);
|
|
807
|
+
const candidates = generateCandidates(ordered, inverted, threshold, n);
|
|
808
|
+
return verifyCandidates(ordered, candidates, threshold, report);
|
|
809
|
+
}
|
|
810
|
+
/** Phase 3: inverted index over prefix elements only. */
|
|
811
|
+
function buildInvertedIndex(ordered, threshold) {
|
|
812
|
+
const inverted = new Map();
|
|
813
|
+
ordered.forEach((b, i) => {
|
|
814
|
+
const p = Math.max(1, Math.min(prefixLen(threshold, b.size), b.size));
|
|
815
|
+
for (let k = 0; k < p; k++) {
|
|
816
|
+
const v = b.values[k];
|
|
817
|
+
const list = inverted.get(v);
|
|
818
|
+
if (list)
|
|
819
|
+
list.push(i);
|
|
820
|
+
else
|
|
821
|
+
inverted.set(v, [i]);
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
return inverted;
|
|
825
|
+
}
|
|
826
|
+
/** Phase 4: candidate generation + size filter. */
|
|
827
|
+
function generateCandidates(ordered, inverted, threshold, n) {
|
|
828
|
+
// The epsilon guards against a FP rounding edge dropping a borderline pair
|
|
829
|
+
// (a false positive is harmless — it is re-verified by the exact Jaccard
|
|
830
|
+
// below — a false negative is not).
|
|
831
|
+
const E = 1e-6;
|
|
832
|
+
const candidates = new Set();
|
|
833
|
+
for (const [, idxs] of inverted) {
|
|
834
|
+
for (let a = 0; a < idxs.length; a++) {
|
|
835
|
+
for (let b = a + 1; b < idxs.length; b++) {
|
|
836
|
+
const i = idxs[a];
|
|
837
|
+
const j = idxs[b];
|
|
838
|
+
const si = ordered[i].size;
|
|
839
|
+
const sj = ordered[j].size;
|
|
840
|
+
if (Math.min(si, sj) < threshold * Math.max(si, sj) - E)
|
|
841
|
+
continue;
|
|
842
|
+
candidates.add(i < j ? i * n + j : j * n + i);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return candidates;
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Phase 5: verify the exact Jaccard for each candidate and report, in the same
|
|
850
|
+
* (i, j) ascending order as the naive all-pairs loop so the output is
|
|
851
|
+
* byte-identical.
|
|
852
|
+
*/
|
|
853
|
+
function verifyCandidates(ordered, candidates, threshold, report) {
|
|
854
|
+
const n = ordered.length;
|
|
714
855
|
const violations = [];
|
|
715
|
-
if (blockEntries.length < 2)
|
|
716
|
-
return violations;
|
|
717
856
|
const reported = new Set();
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
857
|
+
const sortedCandidates = [...candidates].sort((x, y) => x - y);
|
|
858
|
+
for (const enc of sortedCandidates) {
|
|
859
|
+
const i = Math.floor(enc / n);
|
|
860
|
+
const j = enc % n;
|
|
861
|
+
const a = ordered[i];
|
|
862
|
+
const b = ordered[j];
|
|
863
|
+
if (a.key === b.key)
|
|
864
|
+
continue;
|
|
865
|
+
const pairKey = [a.key, b.key].sort().join('::');
|
|
866
|
+
if (reported.has(pairKey))
|
|
867
|
+
continue;
|
|
868
|
+
reported.add(pairKey);
|
|
869
|
+
const intersection = new Set([...a.valueSet].filter(x => b.valueSet.has(x)));
|
|
870
|
+
const union = new Set([...a.valueSet, ...b.valueSet]);
|
|
871
|
+
const similarity = intersection.size / union.size;
|
|
872
|
+
if (similarity >= threshold) {
|
|
873
|
+
violations.push(report(a.filePath, a.line, `Declaration-set similarity: "${a.context}" and "${b.context}" ` +
|
|
874
|
+
`in ${b.filePath} share ${intersection.size} of ${union.size} ` +
|
|
875
|
+
`declarations (${(similarity * 100).toFixed(0)}%). ` +
|
|
876
|
+
`Consider consolidating these rules or extracting a shared mixin.`, { severity: 'suggestion', rule: 'styles/declaration-set-similarity', symbol: `${a.context} & ${b.context}` }));
|
|
737
877
|
}
|
|
738
878
|
}
|
|
739
879
|
return violations;
|
|
@@ -741,6 +881,14 @@ function flagSimilarBlockPairs(blockEntries, threshold, report) {
|
|
|
741
881
|
function buildTokenValueMap(tokens) {
|
|
742
882
|
const tokenValueMap = new Map();
|
|
743
883
|
for (const t of tokens) {
|
|
884
|
+
// The Tailwind loader falls back to the bundled v4 default palette when a
|
|
885
|
+
// project has no Tailwind config, seeding those tokens with
|
|
886
|
+
// `file_path === 'built-in defaults'`. Those are not the project's tokens —
|
|
887
|
+
// a plain-CSS project that never opted into a token system would otherwise
|
|
888
|
+
// have every raw `#fff` flagged as "bypassing colors.white", a fabricated
|
|
889
|
+
// finding. Only tokens the project actually defined can be bypassed.
|
|
890
|
+
if (t.file_path === 'built-in defaults')
|
|
891
|
+
continue;
|
|
744
892
|
const normalizedTokenVal = normalizeValue(t.value, '__token__');
|
|
745
893
|
tokenValueMap.set(t.value, { name: t.name, valueType: normalizedTokenVal?.type ?? null });
|
|
746
894
|
}
|
|
@@ -798,21 +946,27 @@ class StylesStructureDetectors {
|
|
|
798
946
|
* 3. Batch-probe unknown classes via @apply compilation
|
|
799
947
|
* 4. Resolve each class from cache + structural patterns
|
|
800
948
|
*/
|
|
801
|
-
async detectUndefinedClasses(classUsage,
|
|
949
|
+
async detectUndefinedClasses(classUsage, cfg, definedClassIndex) {
|
|
802
950
|
const violations = [];
|
|
803
|
-
const definedClassCatalog = collectDefinedClassCatalog(byProperty);
|
|
804
951
|
const expander = getTailwindExpander();
|
|
805
952
|
// Fail-open rule (Spec 22 R1.3): if the probe fails and Tailwind IS
|
|
806
953
|
// present, disable the undefined-class detector.
|
|
807
954
|
const failOpen = await initTailwindProbe(expander, cfg, this.makeViolation.bind(this));
|
|
808
955
|
if (failOpen)
|
|
809
956
|
return failOpen;
|
|
810
|
-
//
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
957
|
+
// 1. Static-filter usage rows into candidate class names (the defined-check
|
|
958
|
+
// is deferred to the DB lookup below).
|
|
959
|
+
const { candidates, usageEntries } = collectUndefinedClassCandidates(classUsage, new Set());
|
|
960
|
+
// 2. Resolve the candidate names against the defined-class table in one
|
|
961
|
+
// chunked `class_name IN (...)` index lookup — names with no row back
|
|
962
|
+
// are undefined.
|
|
963
|
+
const definedLocations = definedClassIndex?.lookup(candidates) ?? new Map();
|
|
964
|
+
// 3. Batch-probe only the genuinely undefined names.
|
|
965
|
+
const undefinedCandidates = candidates.filter((c) => !definedLocations.has(c));
|
|
966
|
+
if (undefinedCandidates.length > 0 && expander.probeReady) {
|
|
967
|
+
await expander.validateBatch(undefinedCandidates);
|
|
814
968
|
}
|
|
815
|
-
violations.push(...flagUnresolvedClasses(usageEntries, expander, this.makeViolation.bind(this),
|
|
969
|
+
violations.push(...flagUnresolvedClasses(usageEntries.filter((u) => !definedLocations.has(u.class_name)), expander, this.makeViolation.bind(this), definedClassIndex?.suggest ?? (() => null)));
|
|
816
970
|
return violations;
|
|
817
971
|
}
|
|
818
972
|
// -----------------------------------------------------------------------
|
|
@@ -845,7 +999,7 @@ class StylesStructureDetectors {
|
|
|
845
999
|
continue;
|
|
846
1000
|
violations.push(this.makeViolation(d.file_path, d.line, `Token bypass: "${d.raw_value}" for "${d.property}" matches design ` +
|
|
847
1001
|
`token "${tokenInfo.name}" but was used as a raw value. ` +
|
|
848
|
-
`Use the token reference instead to keep styles consistent.`, { severity: 'warning', rule: 'styles/token-bypass' }));
|
|
1002
|
+
`Use the token reference instead to keep styles consistent.`, { severity: 'warning', rule: 'styles/token-bypass', symbol: declValueKey(d) }));
|
|
849
1003
|
}
|
|
850
1004
|
return violations;
|
|
851
1005
|
}
|
|
@@ -907,14 +1061,14 @@ class StylesStructureDetectors {
|
|
|
907
1061
|
const sample = values.get(sortedVals[0])[0];
|
|
908
1062
|
violations.push(this.makeViolation(sample.file_path, sample.line, `Z-index sprawl: ${values.size} distinct z-index values ` +
|
|
909
1063
|
`(${sortedVals.join(', ')}). Consider defining a z-index scale ` +
|
|
910
|
-
`(e.g., $z-layers: (dropdown: 100, modal: 200, toast: 300)).`, { severity: 'warning', rule: 'styles/z-index-sprawl' }));
|
|
1064
|
+
`(e.g., $z-layers: (dropdown: 100, modal: 200, toast: 300)).`, { severity: 'warning', rule: 'styles/z-index-sprawl', symbol: declValueKey(sample) }));
|
|
911
1065
|
}
|
|
912
1066
|
// Flag singletons (z-index values used only once)
|
|
913
1067
|
for (const [val, list] of values) {
|
|
914
1068
|
if (list.length === 1 && values.size > 2) {
|
|
915
1069
|
const d = list[0];
|
|
916
1070
|
violations.push(this.makeViolation(d.file_path, d.line, `Singleton z-index: z-index: ${val} is used only once. ` +
|
|
917
|
-
`Consider whether this value belongs in a shared z-index scale.`, { severity: 'suggestion', rule: 'styles/z-index-singleton' }));
|
|
1071
|
+
`Consider whether this value belongs in a shared z-index scale.`, { severity: 'suggestion', rule: 'styles/z-index-singleton', symbol: declValueKey(d) }));
|
|
918
1072
|
}
|
|
919
1073
|
}
|
|
920
1074
|
return violations;
|
|
@@ -924,11 +1078,19 @@ class StylesStructureDetectors {
|
|
|
924
1078
|
// Orchestration layer: queries the style index and dispatches to the
|
|
925
1079
|
// detectors. This is the only exported class in the chain.
|
|
926
1080
|
// ---------------------------------------------------------------------------
|
|
1081
|
+
/**
|
|
1082
|
+
* Construct the structure detectors for the analyzer. Lives as a factory so the
|
|
1083
|
+
* analyzer depends on a seam rather than directly instantiating the concrete
|
|
1084
|
+
* detector class (the dependency-inversion signal).
|
|
1085
|
+
*/
|
|
1086
|
+
function createStylesStructureDetectors(makeViolation) {
|
|
1087
|
+
return new StylesStructureDetectors(makeViolation);
|
|
1088
|
+
}
|
|
927
1089
|
/**
|
|
928
1090
|
* Universal styles analyzer.
|
|
929
1091
|
*/
|
|
930
1092
|
export class UniversalStylesAnalyzer extends UniversalStylesAnalyzerDetectors {
|
|
931
|
-
structure =
|
|
1093
|
+
structure = createStylesStructureDetectors(this.makeViolation.bind(this));
|
|
932
1094
|
/**
|
|
933
1095
|
* Override analyze() to query the full style index in one pass instead
|
|
934
1096
|
* of per-file AST processing. The base class analyze() loop is bypassed.
|
|
@@ -960,13 +1122,7 @@ export class UniversalStylesAnalyzer extends UniversalStylesAnalyzerDetectors {
|
|
|
960
1122
|
name: this.name,
|
|
961
1123
|
});
|
|
962
1124
|
}
|
|
963
|
-
const violations = await this.
|
|
964
|
-
byProperty: groupDeclarationsByProperty(declarations),
|
|
965
|
-
cfg,
|
|
966
|
-
declarations,
|
|
967
|
-
classUsage: this.queryClassUsage(indexHandle),
|
|
968
|
-
tokenValueMap: buildTokenValueMap(this.queryTokens(indexHandle)),
|
|
969
|
-
});
|
|
1125
|
+
const violations = await this.runAllDetectors(indexHandle, cfg, declarations);
|
|
970
1126
|
applySeverityOverrides(violations, config);
|
|
971
1127
|
return buildStylesResult({
|
|
972
1128
|
violations: violations.filter(v => v.severity !== 'off'),
|
|
@@ -975,13 +1131,27 @@ export class UniversalStylesAnalyzer extends UniversalStylesAnalyzerDetectors {
|
|
|
975
1131
|
name: this.name,
|
|
976
1132
|
});
|
|
977
1133
|
}
|
|
1134
|
+
/** Assemble detector inputs from the style index and run all detectors. */
|
|
1135
|
+
async runAllDetectors(indexHandle, cfg, declarations) {
|
|
1136
|
+
return this.runDetectors({
|
|
1137
|
+
byProperty: groupDeclarationsByProperty(declarations),
|
|
1138
|
+
cfg,
|
|
1139
|
+
declarations,
|
|
1140
|
+
classUsage: this.queryClassUsage(indexHandle),
|
|
1141
|
+
tokenValueMap: buildTokenValueMap(this.queryTokens(indexHandle)),
|
|
1142
|
+
definedClassIndex: {
|
|
1143
|
+
lookup: createDefinedClassLookup(indexHandle),
|
|
1144
|
+
suggest: createDefinedClassSuggester(indexHandle),
|
|
1145
|
+
},
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
978
1148
|
/** Run all detectors and return their combined violations. */
|
|
979
1149
|
async runDetectors(inputs) {
|
|
980
|
-
const { byProperty, cfg, declarations, classUsage, tokenValueMap } = inputs;
|
|
1150
|
+
const { byProperty, cfg, declarations, classUsage, tokenValueMap, definedClassIndex } = inputs;
|
|
981
1151
|
const violations = [];
|
|
982
1152
|
violations.push(...this.detectValueDrift(byProperty, cfg, declarations));
|
|
983
1153
|
violations.push(...this.detectOffScaleValues(byProperty, cfg));
|
|
984
|
-
violations.push(...await this.structure.detectUndefinedClasses(classUsage,
|
|
1154
|
+
violations.push(...await this.structure.detectUndefinedClasses(classUsage, cfg, definedClassIndex));
|
|
985
1155
|
violations.push(...this.structure.detectTokenBypass(declarations, tokenValueMap, cfg));
|
|
986
1156
|
violations.push(...this.structure.detectMechanismFragmentation(declarations, cfg));
|
|
987
1157
|
violations.push(...this.structure.detectDeclarationSetSimilarity(declarations, cfg));
|
|
@@ -997,7 +1167,8 @@ export class UniversalStylesAnalyzer extends UniversalStylesAnalyzerDetectors {
|
|
|
997
1167
|
// -----------------------------------------------------------------------
|
|
998
1168
|
queryDeclarations(indexHandle) {
|
|
999
1169
|
try {
|
|
1000
|
-
return indexHandle.query('SELECT
|
|
1170
|
+
return indexHandle.query('SELECT property, raw_value, normalized_value, mechanism, file_path, line, context, token_ref ' +
|
|
1171
|
+
'FROM style_declarations ORDER BY property, file_path, line');
|
|
1001
1172
|
}
|
|
1002
1173
|
catch {
|
|
1003
1174
|
return [];
|