code-auditor-mcp 3.4.14 → 3.4.16
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 +83 -0
- package/dist/analyzers/applicability.d.ts +41 -0
- package/dist/analyzers/applicability.d.ts.map +1 -0
- package/dist/analyzers/applicability.js +96 -0
- package/dist/analyzers/applicability.js.map +1 -0
- package/dist/analyzers/ruleRegistry.js +1 -1
- package/dist/analyzers/ruleRegistry.js.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts +2 -2
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +2 -2
- package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js +20 -9
- package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
- package/dist/analyzers/universal/schema/migrations.d.ts +11 -0
- package/dist/analyzers/universal/schema/migrations.d.ts.map +1 -1
- package/dist/analyzers/universal/schema/migrations.js +127 -4
- package/dist/analyzers/universal/schema/migrations.js.map +1 -1
- package/dist/auditRunner.d.ts.map +1 -1
- package/dist/auditRunner.js +62 -23
- package/dist/auditRunner.js.map +1 -1
- package/dist/cli.js +406 -1
- package/dist/cli.js.map +1 -1
- package/dist/codeIndexDB.d.ts +40 -1
- package/dist/codeIndexDB.d.ts.map +1 -1
- package/dist/codeIndexDB.js +222 -50
- package/dist/codeIndexDB.js.map +1 -1
- package/dist/config/defaults.js +1 -1
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/pathProfiles.d.ts +7 -0
- package/dist/config/pathProfiles.d.ts.map +1 -1
- package/dist/config/pathProfiles.js +7 -1
- package/dist/config/pathProfiles.js.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.d.ts.map +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js +1 -1
- package/dist/languages/typescript/TreeSitterTypeScriptAdapter.js.map +1 -1
- package/dist/ledger.d.ts +163 -2
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +528 -7
- package/dist/ledger.js.map +1 -1
- package/dist/mcp.js +3 -3
- package/dist/mcp.js.map +1 -1
- package/dist/mcpAuditJobs.d.ts +24 -2
- package/dist/mcpAuditJobs.d.ts.map +1 -1
- package/dist/mcpAuditJobs.js +364 -58
- package/dist/mcpAuditJobs.js.map +1 -1
- package/dist/pipeline.d.ts +9 -2
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +165 -18
- package/dist/pipeline.js.map +1 -1
- package/dist/pipelineAdapters.d.ts.map +1 -1
- package/dist/pipelineAdapters.js +45 -10
- package/dist/pipelineAdapters.js.map +1 -1
- package/dist/scripts/benchmarkWorkers.js +1 -1
- package/dist/scripts/benchmarkWorkers.js.map +1 -1
- package/dist/services/auditJobService.d.ts +22 -5
- package/dist/services/auditJobService.d.ts.map +1 -1
- package/dist/services/auditJobService.js +67 -26
- package/dist/services/auditJobService.js.map +1 -1
- package/dist/services/fileAccounting.d.ts +83 -0
- package/dist/services/fileAccounting.d.ts.map +1 -0
- package/dist/services/fileAccounting.js +215 -0
- package/dist/services/fileAccounting.js.map +1 -0
- package/dist/styles/styleExtractor.d.ts +2 -2
- package/dist/styles/styleExtractor.d.ts.map +1 -1
- package/dist/styles/styleExtractor.js +83 -11
- package/dist/styles/styleExtractor.js.map +1 -1
- package/dist/styles/styleIndexer.d.ts +8 -0
- package/dist/styles/styleIndexer.d.ts.map +1 -1
- package/dist/styles/styleIndexer.js +103 -15
- package/dist/styles/styleIndexer.js.map +1 -1
- package/dist/styles/tailwindUtilityExpander.d.ts +3 -0
- package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
- package/dist/styles/tailwindUtilityExpander.js +35 -1
- package/dist/styles/tailwindUtilityExpander.js.map +1 -1
- package/dist/styles/types.d.ts +11 -0
- package/dist/styles/types.d.ts.map +1 -1
- package/dist/types.d.ts +133 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/fileDiscovery.d.ts +98 -0
- package/dist/utils/fileDiscovery.d.ts.map +1 -1
- package/dist/utils/fileDiscovery.js +246 -29
- package/dist/utils/fileDiscovery.js.map +1 -1
- package/dist/workers/auditJobRunner.d.ts +2 -0
- package/dist/workers/auditJobRunner.d.ts.map +1 -0
- package/dist/workers/auditJobRunner.js +37 -0
- package/dist/workers/auditJobRunner.js.map +1 -0
- package/dist/workers/auditWorker.js +26 -0
- package/dist/workers/auditWorker.js.map +1 -1
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
|
@@ -1,36 +1,77 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* DB-backed audit job model.
|
|
3
|
+
*
|
|
4
|
+
* A job is a view over a `findings_ledger_runs` row: `jobId` IS `run_id`, and
|
|
5
|
+
* lifecycle state (`queued → running → completed/failed`, progress, error,
|
|
6
|
+
* resultId) lives on the run row. There is no second job model and no
|
|
7
|
+
* in-memory Map — this delegates to `ledger.ts`, so synchronous and detached
|
|
8
|
+
* audits share one write path.
|
|
9
|
+
*
|
|
10
|
+
* The database handle is passed in explicitly (matching `ledger.ts`); callers
|
|
11
|
+
* resolve the per-project DB via `CodeIndexDB.getInstance(undefined, projectRoot)`.
|
|
3
12
|
*/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { createLedgerRun, detectRunInput, getLedgerRun, patchLedgerRun, } from '../ledger.js';
|
|
17
|
+
// Package version — read once at module load (mirrors auditRunner.ts).
|
|
18
|
+
const __svcDirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const _pkg = JSON.parse(readFileSync(path.join(__svcDirname, '..', '..', 'package.json'), 'utf-8'));
|
|
20
|
+
const TOOL_VERSION = String(_pkg.version || '0.0.0');
|
|
21
|
+
function toAuditJobRecord(detail) {
|
|
22
|
+
let progress;
|
|
23
|
+
if (detail.progressJson) {
|
|
24
|
+
try {
|
|
25
|
+
progress = JSON.parse(detail.progressJson);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
progress = undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
jobId: detail.runId,
|
|
33
|
+
status: detail.status,
|
|
34
|
+
path: detail.projectRoot ?? detail.target ?? '',
|
|
35
|
+
createdAt: detail.timestamp,
|
|
36
|
+
...(detail.startedAt ? { startedAt: detail.startedAt } : {}),
|
|
37
|
+
...(detail.finishedAt ? { finishedAt: detail.finishedAt } : {}),
|
|
38
|
+
...(detail.resultId ? { resultId: detail.resultId } : {}),
|
|
39
|
+
...(detail.error ? { error: detail.error } : {}),
|
|
40
|
+
...(progress ? { progress } : {}),
|
|
15
41
|
};
|
|
16
|
-
jobs.set(jobId, rec);
|
|
17
|
-
return rec;
|
|
18
42
|
}
|
|
19
|
-
export function
|
|
20
|
-
|
|
43
|
+
export function createAuditJob(db, projectRoot, meta = {}) {
|
|
44
|
+
const runInput = detectRunInput(meta.command ?? 'audit.start', meta.surface ?? 'mcp', meta.scope ?? 'all', projectRoot, TOOL_VERSION);
|
|
45
|
+
const runId = createLedgerRun(db, runInput, { status: 'queued', projectRoot });
|
|
46
|
+
// getLedgerRun cannot miss a row we just created; the non-null assertion is safe.
|
|
47
|
+
return toAuditJobRecord(getLedgerRun(db, runId));
|
|
48
|
+
}
|
|
49
|
+
export function getAuditJob(db, jobId) {
|
|
50
|
+
const detail = getLedgerRun(db, jobId);
|
|
51
|
+
return detail ? toAuditJobRecord(detail) : undefined;
|
|
21
52
|
}
|
|
22
|
-
export function patchAuditJob(jobId, patch) {
|
|
23
|
-
|
|
24
|
-
if (!cur)
|
|
53
|
+
export function patchAuditJob(db, jobId, patch) {
|
|
54
|
+
if (!getLedgerRun(db, jobId))
|
|
25
55
|
return undefined;
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
56
|
+
const ledgerPatch = {};
|
|
57
|
+
if (patch.status !== undefined)
|
|
58
|
+
ledgerPatch.status = patch.status;
|
|
59
|
+
if (patch.startedAt !== undefined)
|
|
60
|
+
ledgerPatch.startedAt = patch.startedAt ?? null;
|
|
61
|
+
if (patch.finishedAt !== undefined)
|
|
62
|
+
ledgerPatch.finishedAt = patch.finishedAt ?? null;
|
|
63
|
+
if (patch.error !== undefined)
|
|
64
|
+
ledgerPatch.error = patch.error ?? null;
|
|
65
|
+
if (patch.resultId !== undefined)
|
|
66
|
+
ledgerPatch.resultId = patch.resultId ?? null;
|
|
67
|
+
if (patch.progress !== undefined)
|
|
68
|
+
ledgerPatch.progressJson = JSON.stringify(patch.progress);
|
|
69
|
+
patchLedgerRun(db, jobId, ledgerPatch);
|
|
70
|
+
return getAuditJob(db, jobId);
|
|
29
71
|
}
|
|
30
|
-
export function setAuditJobProgress(jobId, progress) {
|
|
31
|
-
|
|
32
|
-
if (!cur)
|
|
72
|
+
export function setAuditJobProgress(db, jobId, progress) {
|
|
73
|
+
if (!getLedgerRun(db, jobId))
|
|
33
74
|
return;
|
|
34
|
-
|
|
75
|
+
patchLedgerRun(db, jobId, { progressJson: JSON.stringify(progress) });
|
|
35
76
|
}
|
|
36
77
|
//# sourceMappingURL=auditJobService.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auditJobService.js","sourceRoot":"","sources":["../../src/services/auditJobService.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"auditJobService.js","sourceRoot":"","sources":["../../src/services/auditJobService.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EACL,eAAe,EACf,cAAc,EACd,YAAY,EACZ,cAAc,GACf,MAAM,cAAc,CAAC;AAsCtB,uEAAuE;AACvE,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAClE,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;AACpG,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,CAAC;AAErD,SAAS,gBAAgB,CAAC,MAAuB;IAC/C,IAAI,QAAsC,CAAC;IAC3C,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;QACxB,IAAI,CAAC;YACH,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,YAAY,CAAqB,CAAC;QACjE,CAAC;QAAC,MAAM,CAAC;YACP,QAAQ,GAAG,SAAS,CAAC;QACvB,CAAC;IACH,CAAC;IACD,OAAO;QACL,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,MAAM,EAAE,MAAM,CAAC,MAAwB;QACvC,IAAI,EAAE,MAAM,CAAC,WAAW,IAAI,MAAM,CAAC,MAAM,IAAI,EAAE;QAC/C,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5D,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/D,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzD,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChD,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAClC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,EAAqB,EACrB,WAAmB,EACnB,IAAI,GAAuB,EAAE;IAE7B,MAAM,QAAQ,GAAG,cAAc,CAC7B,IAAI,CAAC,OAAO,IAAI,aAAa,EAC7B,IAAI,CAAC,OAAO,IAAI,KAAK,EACrB,IAAI,CAAC,KAAK,IAAI,KAAK,EACnB,WAAW,EACX,YAAY,CACb,CAAC;IACF,MAAM,KAAK,GAAG,eAAe,CAAC,EAAE,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC,CAAC;IAC/E,kFAAkF;IAClF,OAAO,gBAAgB,CAAC,YAAY,CAAC,EAAE,EAAE,KAAK,CAAE,CAAC,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,EAAqB,EAAE,KAAa;IAC9D,MAAM,MAAM,GAAG,YAAY,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;IACvC,OAAO,MAAM,CAAC,CAAC,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACvD,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,EAAqB,EACrB,KAAa,EACb,KAA8B;IAE9B,IAAI,CAAC,YAAY,CAAC,EAAE,EAAE,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAE/C,MAAM,WAAW,GAAmB,EAAE,CAAC;IACvC,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS;QAAE,WAAW,CAAC,MAAM,GAAG,KAAK,CAAC,MAAyB,CAAC;IACrF,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS;QAAE,WAAW,CAAC,SAAS,GAAG,KAAK,CAAC,SAAS,IAAI,IAAI,CAAC;IACnF,IAAI,KAAK,CAAC,UAAU,KAAK,SAAS;QAAE,WAAW,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,IAAI,IAAI,CAAC;IACtF,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;QAAE,WAAW,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,IAAI,CAAC;IACvE,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS;QAAE,WAAW,CAAC,QAAQ,GAAG,KAAK,CAAC,QAAQ,IAAI,IAAI,CAAC;IAChF,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS;QAAE,WAAW,CAAC,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAE5F,cAAc,CAAC,EAAE,EAAE,KAAK,EAAE,WAAW,CAAC,CAAC;IACvC,OAAO,WAAW,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,EAAqB,EAAE,KAAa,EAAE,QAA0B;IAClG,IAAI,CAAC,YAAY,CAAC,EAAE,EAAE,KAAK,CAAC;QAAE,OAAO;IACrC,cAAc,CAAC,EAAE,EAAE,KAAK,EAAE,EAAE,YAAY,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AACxE,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File accounting accumulator (Spec 44).
|
|
3
|
+
*
|
|
4
|
+
* Makes file handling *total*: every file the discovery walk touches lands in
|
|
5
|
+
* exactly one terminal state — `analyzed` (reached >= 1 stage-2 visitor),
|
|
6
|
+
* `partially analyzed` (dropped at stage 2 as `no adapter` / `no visitor
|
|
7
|
+
* matched` yet still consumed/read by a later layer — the style indexer or a
|
|
8
|
+
* stage-3 `readSource`), or `dropped` (with one of eight named reasons) — and
|
|
9
|
+
* the accounting must balance
|
|
10
|
+
* (`analyzed + partiallyAnalyzed + dropped === touched`), asserted at the end
|
|
11
|
+
* of the run.
|
|
12
|
+
*
|
|
13
|
+
* The accumulator is owned by the run and threaded discovery -> stage 1 ->
|
|
14
|
+
* stage 2. Files inside infrastructure directories (node_modules, .git, …) are
|
|
15
|
+
* NOT in the universe — they are recorded as an aggregate `infraPruned` line,
|
|
16
|
+
* never per-file. Reason 8 (`unsupported dialect`) is a sub-layer: style-level
|
|
17
|
+
* unread sources that do NOT affect the file-level balance, reported under the
|
|
18
|
+
* reason in metadata but excluded from `dropped`.
|
|
19
|
+
*/
|
|
20
|
+
import type { FileDropReason, FileAccountingSummary, FileAccountingFileEntry } from '../types.js';
|
|
21
|
+
/** Detail attached to a drop (everything except the filePath, which is the key). */
|
|
22
|
+
export type FileDropDetail = Omit<FileAccountingFileEntry, 'filePath'>;
|
|
23
|
+
/**
|
|
24
|
+
* Thrown when the accounting does not balance: a file was touched but never
|
|
25
|
+
* classified (a silent-drop leak), or classified more than once. Surfaced by
|
|
26
|
+
* `assertBalanced()` at the end of the run and converted to a non-zero exit.
|
|
27
|
+
*/
|
|
28
|
+
export declare class AccountingBalanceError extends Error {
|
|
29
|
+
readonly name = "AccountingBalanceError";
|
|
30
|
+
constructor(message: string);
|
|
31
|
+
}
|
|
32
|
+
export declare class FileAccounting {
|
|
33
|
+
private readonly entries;
|
|
34
|
+
private readonly infraPrunedMap;
|
|
35
|
+
private readonly unsupportedDialects;
|
|
36
|
+
private readonly conflicts;
|
|
37
|
+
/** Mark a candidate file as part of the touched universe. */
|
|
38
|
+
recordTouched(filePath: string): void;
|
|
39
|
+
/** Mark a file as analyzed (its tuple reached >= 1 stage-2 visitor). */
|
|
40
|
+
recordAnalyzed(filePath: string): void;
|
|
41
|
+
/** Mark a file as dropped with exactly one of the eight reasons. */
|
|
42
|
+
recordDropped(reason: FileDropReason, filePath: string, detail?: FileDropDetail): void;
|
|
43
|
+
/**
|
|
44
|
+
* Sanctioned transition used only for the size-threshold path: an oversized
|
|
45
|
+
* `.sql` orphan reaches the schema-sql visitor (and is therefore first
|
|
46
|
+
* recorded `analyzed`) before being skipped. Reclassify it as dropped.
|
|
47
|
+
*/
|
|
48
|
+
reclassifyAnalyzedToDropped(reason: FileDropReason, filePath: string, detail?: FileDropDetail): void;
|
|
49
|
+
/**
|
|
50
|
+
* Reclassify a file dropped at stage 2 as "partially analyzed": it reached
|
|
51
|
+
* zero stage-2 visitors (hence `no adapter` / `no visitor matched`) yet was
|
|
52
|
+
* still *consumed* (read) by a later layer — the style indexer readFileSyncs
|
|
53
|
+
* every discovered file, stage-3 reducers pull source via `readSource`. The
|
|
54
|
+
* trigger is consumption, not output: a clean file the indexer read is just as
|
|
55
|
+
* "partially analyzed" as one that happened to produce findings. The original
|
|
56
|
+
* reason is retained — it still explains why the file wasn't *fully* analyzed.
|
|
57
|
+
*
|
|
58
|
+
* Only `no adapter` and `no visitor matched` are reclassifiable: the other
|
|
59
|
+
* drop reasons mean the file never reached (directory pruned, extension not
|
|
60
|
+
* known, parse failed) or was deliberately removed from (size threshold, path
|
|
61
|
+
* profile excluded) every analysis layer, so a read from one of them would be
|
|
62
|
+
* a genuine accounting bug, not a partial analysis. No-ops otherwise, so the
|
|
63
|
+
* pipeline can call it blindly over every consumed file path.
|
|
64
|
+
*/
|
|
65
|
+
reclassifyDroppedToPartiallyAnalyzed(filePath: string): void;
|
|
66
|
+
/** Record an infrastructure directory prune (aggregate, not per-file). */
|
|
67
|
+
recordInfraPruned(directory: string, rule: string): void;
|
|
68
|
+
/**
|
|
69
|
+
* Reason 8 sub-layer: a style-level unread source (Spec 42 R2). Recorded in
|
|
70
|
+
* the summary under `unsupported dialect` but does NOT touch the file-level
|
|
71
|
+
* `analyzed + dropped === touched` balance.
|
|
72
|
+
*/
|
|
73
|
+
recordUnsupportedDialect(filePath: string, reason?: string): void;
|
|
74
|
+
/**
|
|
75
|
+
* Verify the balance. Throws `AccountingBalanceError` if any file was touched
|
|
76
|
+
* but never classified (a leak) or classified more than once. Called at the
|
|
77
|
+
* end of the run before metadata assembly.
|
|
78
|
+
*/
|
|
79
|
+
assertBalanced(): void;
|
|
80
|
+
/** Produce the summary (touched/analyzed/partiallyAnalyzed/dropped counts + per-reason lists). */
|
|
81
|
+
summary(): FileAccountingSummary;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=fileAccounting.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileAccounting.d.ts","sourceRoot":"","sources":["../../src/services/fileAccounting.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAElG,oFAAoF;AACpF,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,uBAAuB,EAAE,UAAU,CAAC,CAAC;AAUvE;;;;GAIG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,SAAkB,IAAI,4BAA4B;IAClD,YAAY,OAAO,EAAE,MAAM,EAE1B;CACF;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;IACpD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA+E;IAC9G,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAoD;IACxF,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;IAE1C,6DAA6D;IAC7D,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAIpC;IAED,wEAAwE;IACxE,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAYrC;IAED,oEAAoE;IACpE,aAAa,CAAC,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,cAAc,GAAG,IAAI,CAmBrF;IAED;;;;OAIG;IACH,2BAA2B,CAAC,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,cAAc,GAAG,IAAI,CASnG;IAED;;;;;;;;;;;;;;;OAeG;IACH,oCAAoC,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAK3D;IAED,0EAA0E;IAC1E,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAKvD;IAED;;;;OAIG;IACH,wBAAwB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAEhE;IAED;;;;OAIG;IACH,cAAc,IAAI,IAAI,CAqBrB;IAED,kGAAkG;IAClG,OAAO,IAAI,qBAAqB,CAmD/B;CACF"}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File accounting accumulator (Spec 44).
|
|
3
|
+
*
|
|
4
|
+
* Makes file handling *total*: every file the discovery walk touches lands in
|
|
5
|
+
* exactly one terminal state — `analyzed` (reached >= 1 stage-2 visitor),
|
|
6
|
+
* `partially analyzed` (dropped at stage 2 as `no adapter` / `no visitor
|
|
7
|
+
* matched` yet still consumed/read by a later layer — the style indexer or a
|
|
8
|
+
* stage-3 `readSource`), or `dropped` (with one of eight named reasons) — and
|
|
9
|
+
* the accounting must balance
|
|
10
|
+
* (`analyzed + partiallyAnalyzed + dropped === touched`), asserted at the end
|
|
11
|
+
* of the run.
|
|
12
|
+
*
|
|
13
|
+
* The accumulator is owned by the run and threaded discovery -> stage 1 ->
|
|
14
|
+
* stage 2. Files inside infrastructure directories (node_modules, .git, …) are
|
|
15
|
+
* NOT in the universe — they are recorded as an aggregate `infraPruned` line,
|
|
16
|
+
* never per-file. Reason 8 (`unsupported dialect`) is a sub-layer: style-level
|
|
17
|
+
* unread sources that do NOT affect the file-level balance, reported under the
|
|
18
|
+
* reason in metadata but excluded from `dropped`.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Thrown when the accounting does not balance: a file was touched but never
|
|
22
|
+
* classified (a silent-drop leak), or classified more than once. Surfaced by
|
|
23
|
+
* `assertBalanced()` at the end of the run and converted to a non-zero exit.
|
|
24
|
+
*/
|
|
25
|
+
export class AccountingBalanceError extends Error {
|
|
26
|
+
name = 'AccountingBalanceError';
|
|
27
|
+
constructor(message) {
|
|
28
|
+
super(message);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export class FileAccounting {
|
|
32
|
+
entries = new Map();
|
|
33
|
+
infraPrunedMap = new Map();
|
|
34
|
+
unsupportedDialects = [];
|
|
35
|
+
conflicts = [];
|
|
36
|
+
/** Mark a candidate file as part of the touched universe. */
|
|
37
|
+
recordTouched(filePath) {
|
|
38
|
+
if (!this.entries.has(filePath)) {
|
|
39
|
+
this.entries.set(filePath, { state: 'touched' });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Mark a file as analyzed (its tuple reached >= 1 stage-2 visitor). */
|
|
43
|
+
recordAnalyzed(filePath) {
|
|
44
|
+
const entry = this.entries.get(filePath);
|
|
45
|
+
if (!entry) {
|
|
46
|
+
this.entries.set(filePath, { state: 'analyzed' });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (entry.state === 'analyzed')
|
|
50
|
+
return; // idempotent — a file can reach several visitors
|
|
51
|
+
if (entry.state === 'dropped') {
|
|
52
|
+
this.conflicts.push(`${filePath}: analyzed after dropped(${entry.reason})`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
entry.state = 'analyzed';
|
|
56
|
+
}
|
|
57
|
+
/** Mark a file as dropped with exactly one of the eight reasons. */
|
|
58
|
+
recordDropped(reason, filePath, detail) {
|
|
59
|
+
const entry = this.entries.get(filePath);
|
|
60
|
+
if (!entry) {
|
|
61
|
+
this.entries.set(filePath, { state: 'dropped', reason, detail });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (entry.state === 'dropped') {
|
|
65
|
+
if (entry.reason !== reason) {
|
|
66
|
+
this.conflicts.push(`${filePath}: dropped(${reason}) after dropped(${entry.reason})`);
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (entry.state === 'analyzed') {
|
|
71
|
+
this.conflicts.push(`${filePath}: dropped(${reason}) after analyzed`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
entry.state = 'dropped';
|
|
75
|
+
entry.reason = reason;
|
|
76
|
+
entry.detail = detail;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Sanctioned transition used only for the size-threshold path: an oversized
|
|
80
|
+
* `.sql` orphan reaches the schema-sql visitor (and is therefore first
|
|
81
|
+
* recorded `analyzed`) before being skipped. Reclassify it as dropped.
|
|
82
|
+
*/
|
|
83
|
+
reclassifyAnalyzedToDropped(reason, filePath, detail) {
|
|
84
|
+
const entry = this.entries.get(filePath);
|
|
85
|
+
if (!entry || entry.state !== 'analyzed') {
|
|
86
|
+
this.conflicts.push(`${filePath}: reclassify(${reason}) when state=${entry?.state ?? 'absent'}`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
entry.state = 'dropped';
|
|
90
|
+
entry.reason = reason;
|
|
91
|
+
entry.detail = detail;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Reclassify a file dropped at stage 2 as "partially analyzed": it reached
|
|
95
|
+
* zero stage-2 visitors (hence `no adapter` / `no visitor matched`) yet was
|
|
96
|
+
* still *consumed* (read) by a later layer — the style indexer readFileSyncs
|
|
97
|
+
* every discovered file, stage-3 reducers pull source via `readSource`. The
|
|
98
|
+
* trigger is consumption, not output: a clean file the indexer read is just as
|
|
99
|
+
* "partially analyzed" as one that happened to produce findings. The original
|
|
100
|
+
* reason is retained — it still explains why the file wasn't *fully* analyzed.
|
|
101
|
+
*
|
|
102
|
+
* Only `no adapter` and `no visitor matched` are reclassifiable: the other
|
|
103
|
+
* drop reasons mean the file never reached (directory pruned, extension not
|
|
104
|
+
* known, parse failed) or was deliberately removed from (size threshold, path
|
|
105
|
+
* profile excluded) every analysis layer, so a read from one of them would be
|
|
106
|
+
* a genuine accounting bug, not a partial analysis. No-ops otherwise, so the
|
|
107
|
+
* pipeline can call it blindly over every consumed file path.
|
|
108
|
+
*/
|
|
109
|
+
reclassifyDroppedToPartiallyAnalyzed(filePath) {
|
|
110
|
+
const entry = this.entries.get(filePath);
|
|
111
|
+
if (!entry || entry.state !== 'dropped')
|
|
112
|
+
return;
|
|
113
|
+
if (entry.reason !== 'no adapter' && entry.reason !== 'no visitor matched')
|
|
114
|
+
return;
|
|
115
|
+
entry.state = 'partially analyzed';
|
|
116
|
+
}
|
|
117
|
+
/** Record an infrastructure directory prune (aggregate, not per-file). */
|
|
118
|
+
recordInfraPruned(directory, rule) {
|
|
119
|
+
const key = `${directory}::${rule}`;
|
|
120
|
+
const existing = this.infraPrunedMap.get(key);
|
|
121
|
+
if (existing)
|
|
122
|
+
existing.directories++;
|
|
123
|
+
else
|
|
124
|
+
this.infraPrunedMap.set(key, { directory, rule, directories: 1 });
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Reason 8 sub-layer: a style-level unread source (Spec 42 R2). Recorded in
|
|
128
|
+
* the summary under `unsupported dialect` but does NOT touch the file-level
|
|
129
|
+
* `analyzed + dropped === touched` balance.
|
|
130
|
+
*/
|
|
131
|
+
recordUnsupportedDialect(filePath, reason) {
|
|
132
|
+
this.unsupportedDialects.push({ filePath, reason });
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Verify the balance. Throws `AccountingBalanceError` if any file was touched
|
|
136
|
+
* but never classified (a leak) or classified more than once. Called at the
|
|
137
|
+
* end of the run before metadata assembly.
|
|
138
|
+
*/
|
|
139
|
+
assertBalanced() {
|
|
140
|
+
const leaked = [];
|
|
141
|
+
for (const [filePath, entry] of this.entries) {
|
|
142
|
+
if (entry.state === 'touched')
|
|
143
|
+
leaked.push(filePath);
|
|
144
|
+
}
|
|
145
|
+
if (leaked.length === 0 && this.conflicts.length === 0)
|
|
146
|
+
return;
|
|
147
|
+
const lines = [];
|
|
148
|
+
if (leaked.length > 0) {
|
|
149
|
+
lines.push(`${leaked.length} file(s) touched but never classified (silent-drop leak):`);
|
|
150
|
+
leaked.sort();
|
|
151
|
+
for (const f of leaked.slice(0, 20))
|
|
152
|
+
lines.push(` - ${f}`);
|
|
153
|
+
if (leaked.length > 20)
|
|
154
|
+
lines.push(` … and ${leaked.length - 20} more`);
|
|
155
|
+
}
|
|
156
|
+
if (this.conflicts.length > 0) {
|
|
157
|
+
lines.push(`${this.conflicts.length} file(s) classified more than once:`);
|
|
158
|
+
for (const c of this.conflicts.slice(0, 20))
|
|
159
|
+
lines.push(` - ${c}`);
|
|
160
|
+
if (this.conflicts.length > 20)
|
|
161
|
+
lines.push(` … and ${this.conflicts.length - 20} more`);
|
|
162
|
+
}
|
|
163
|
+
throw new AccountingBalanceError(lines.join('\n'));
|
|
164
|
+
}
|
|
165
|
+
/** Produce the summary (touched/analyzed/partiallyAnalyzed/dropped counts + per-reason lists). */
|
|
166
|
+
summary() {
|
|
167
|
+
let analyzed = 0;
|
|
168
|
+
let partiallyAnalyzed = 0;
|
|
169
|
+
let dropped = 0;
|
|
170
|
+
const reasons = new Map();
|
|
171
|
+
for (const [filePath, entry] of this.entries) {
|
|
172
|
+
if (entry.state === 'analyzed') {
|
|
173
|
+
analyzed++;
|
|
174
|
+
}
|
|
175
|
+
else if ((entry.state === 'dropped' || entry.state === 'partially analyzed') &&
|
|
176
|
+
entry.reason) {
|
|
177
|
+
if (entry.state === 'partially analyzed')
|
|
178
|
+
partiallyAnalyzed++;
|
|
179
|
+
else
|
|
180
|
+
dropped++;
|
|
181
|
+
let list = reasons.get(entry.reason);
|
|
182
|
+
if (!list) {
|
|
183
|
+
list = [];
|
|
184
|
+
reasons.set(entry.reason, list);
|
|
185
|
+
}
|
|
186
|
+
const detail = entry.detail ?? {};
|
|
187
|
+
list.push({
|
|
188
|
+
filePath,
|
|
189
|
+
...detail,
|
|
190
|
+
...(entry.state === 'partially analyzed' ? { partial: true } : {}),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const reasonsOut = {};
|
|
195
|
+
for (const [reason, list] of reasons) {
|
|
196
|
+
list.sort((a, b) => a.filePath.localeCompare(b.filePath));
|
|
197
|
+
reasonsOut[reason] = { count: list.length, files: list };
|
|
198
|
+
}
|
|
199
|
+
if (this.unsupportedDialects.length > 0) {
|
|
200
|
+
const list = this.unsupportedDialects
|
|
201
|
+
.map(({ filePath, reason }) => ({ filePath, ...(reason !== undefined ? { reason } : {}) }))
|
|
202
|
+
.sort((a, b) => a.filePath.localeCompare(b.filePath));
|
|
203
|
+
reasonsOut['unsupported dialect'] = { count: list.length, files: list };
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
touched: this.entries.size,
|
|
207
|
+
analyzed,
|
|
208
|
+
partiallyAnalyzed,
|
|
209
|
+
dropped,
|
|
210
|
+
infraPruned: [...this.infraPrunedMap.values()].sort((a, b) => b.directories - a.directories || a.directory.localeCompare(b.directory)),
|
|
211
|
+
reasons: reasonsOut,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=fileAccounting.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileAccounting.js","sourceRoot":"","sources":["../../src/services/fileAccounting.ts"],"names":[],"mappings":"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"}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Single entry point: extractDeclarations(filePath, adapter, sourceCode, ast?)
|
|
12
12
|
*/
|
|
13
13
|
import type { AST, LanguageAdapter } from '../languages/types.js';
|
|
14
|
-
import type { NormalizedDeclaration, StyleMechanism, StyleToken } from './types.js';
|
|
14
|
+
import type { NormalizedDeclaration, StyleMechanism, StyleToken, UnreadStyleSource } from './types.js';
|
|
15
15
|
import type { TailwindThemeTokens } from './tailwindConfigLoader.js';
|
|
16
16
|
/**
|
|
17
17
|
* Extract all style declarations from a file.
|
|
@@ -25,7 +25,7 @@ import type { TailwindThemeTokens } from './tailwindConfigLoader.js';
|
|
|
25
25
|
* @param ast - Optional pre-parsed AST (avoids re-parsing)
|
|
26
26
|
* @param tailwindTokens - Optional pre-loaded Tailwind tokens (avoids re-loading per file)
|
|
27
27
|
*/
|
|
28
|
-
export declare function extractDeclarations(filePath: string, adapter: LanguageAdapter, sourceCode: string, ast?: AST, tailwindTokens?: TailwindThemeTokens): NormalizedDeclaration[];
|
|
28
|
+
export declare function extractDeclarations(filePath: string, adapter: LanguageAdapter, sourceCode: string, ast?: AST, tailwindTokens?: TailwindThemeTokens, unreadSources?: UnreadStyleSource[]): NormalizedDeclaration[];
|
|
29
29
|
/**
|
|
30
30
|
* Extract CSS custom property definitions from all CSS/SCSS files
|
|
31
31
|
* and produce design tokens.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"styleExtractor.d.ts","sourceRoot":"","sources":["../../src/styles/styleExtractor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,eAAe,EAAW,MAAM,uBAAuB,CAAC;AAC3E,OAAO,KAAK,EAAE,qBAAqB,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"styleExtractor.d.ts","sourceRoot":"","sources":["../../src/styles/styleExtractor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,eAAe,EAAW,MAAM,uBAAuB,CAAC;AAC3E,OAAO,KAAK,EAAE,qBAAqB,EAAE,cAAc,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAIvG,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAOrE;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,eAAe,EACxB,UAAU,EAAE,MAAM,EAClB,GAAG,CAAC,EAAE,GAAG,EACT,cAAc,CAAC,EAAE,mBAAmB,EACpC,aAAa,CAAC,EAAE,iBAAiB,EAAE,GAClC,qBAAqB,EAAE,CAuCzB;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,UAAU,EAAE,CAwBd;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,mBAAmB,CAGrB;AA2ND,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,cAAc,EACzB,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,GAAG,IAAI,EAC7B,YAAY,EAAE,qBAAqB,EAAE,EACrC,QAAQ,EAAE,MAAM,GACf,IAAI,CAuFN"}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { normalizeValue, expandShorthand } from './normalizer.js';
|
|
14
14
|
import { expandUtility } from './tailwindExpander.js';
|
|
15
15
|
import { loadTailwindConfig } from './tailwindConfigLoader.js';
|
|
16
|
+
import { STYLE_MARKUP_EXTENSIONS, KNOWN_SOURCE_EXTENSIONS } from '../utils/fileDiscovery.js';
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
18
|
// Public API
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
@@ -28,8 +29,16 @@ import { loadTailwindConfig } from './tailwindConfigLoader.js';
|
|
|
28
29
|
* @param ast - Optional pre-parsed AST (avoids re-parsing)
|
|
29
30
|
* @param tailwindTokens - Optional pre-loaded Tailwind tokens (avoids re-loading per file)
|
|
30
31
|
*/
|
|
31
|
-
export function extractDeclarations(filePath, adapter, sourceCode, ast, tailwindTokens) {
|
|
32
|
+
export function extractDeclarations(filePath, adapter, sourceCode, ast, tailwindTokens, unreadSources) {
|
|
32
33
|
const ext = filePath.includes('.') ? filePath.slice(filePath.lastIndexOf('.')) : '';
|
|
34
|
+
// Markup/component files — extract class attributes + embedded <style> blocks.
|
|
35
|
+
// Membership is derived from the shared STYLE_MARKUP_EXTENSIONS (single source
|
|
36
|
+
// of truth) so a newly supported dialect is handled here automatically rather
|
|
37
|
+
// than silently dropped from a hand-maintained `case` list (the `.astro`
|
|
38
|
+
// silent-drop bug, Spec 42 R2).
|
|
39
|
+
if (STYLE_MARKUP_EXTENSIONS.includes(ext)) {
|
|
40
|
+
return extractFromHTML(filePath, sourceCode, tailwindTokens, unreadSources);
|
|
41
|
+
}
|
|
33
42
|
switch (ext) {
|
|
34
43
|
// .css files are handled by the AST pipeline (cssAstExtractor.ts +
|
|
35
44
|
// createStylesCssVisitor in pipelineAdapters.ts). SCSS stays on the
|
|
@@ -40,13 +49,21 @@ export function extractDeclarations(filePath, adapter, sourceCode, ast, tailwind
|
|
|
40
49
|
case '.jsx':
|
|
41
50
|
case '.ts':
|
|
42
51
|
case '.js':
|
|
52
|
+
case '.mts':
|
|
53
|
+
case '.cts':
|
|
54
|
+
case '.mjs':
|
|
55
|
+
case '.cjs':
|
|
43
56
|
return extractFromTypeScript(filePath, adapter, sourceCode, ast, tailwindTokens);
|
|
44
|
-
case '.html':
|
|
45
|
-
case '.vue':
|
|
46
|
-
case '.svelte':
|
|
47
|
-
// HTML/Vue/Svelte: extract class attributes only (limited support)
|
|
48
|
-
return extractFromHTML(filePath, sourceCode, tailwindTokens);
|
|
49
57
|
default:
|
|
58
|
+
// Not a style-bearing extension. Known source (`.css`/`.scss` — handled by
|
|
59
|
+
// the AST pipeline — plus JSON/Go/SQL/TOML/Prisma owned by other analyzers)
|
|
60
|
+
// is skipped silently; any *other* extension is unhandled — record it so
|
|
61
|
+
// undefined-class surfaces the gap (whole-run notApplicable) instead of
|
|
62
|
+
// silently dropping the file type. This is the loud backstop for the next
|
|
63
|
+
// dialect that isn't added to the known set.
|
|
64
|
+
if (unreadSources && ext && !KNOWN_SOURCE_EXTENSIONS.includes(ext)) {
|
|
65
|
+
unreadSources.push({ filePath, reason: `unsupported source extension: ${ext}` });
|
|
66
|
+
}
|
|
50
67
|
return [];
|
|
51
68
|
}
|
|
52
69
|
}
|
|
@@ -100,7 +117,7 @@ function extractRuleSetsFromSCSS(filePath, sourceCode) {
|
|
|
100
117
|
* parsing for declaration extraction. The tree-sitter adapter handles structural
|
|
101
118
|
* analysis when needed.
|
|
102
119
|
*/
|
|
103
|
-
function extractRuleSets(css, filePath, declarations) {
|
|
120
|
+
function extractRuleSets(css, filePath, declarations, mechanism = 'scss') {
|
|
104
121
|
// Track current context (selector, at-rule)
|
|
105
122
|
const lines = css.split('\n');
|
|
106
123
|
// Regex approach: find rule blocks with their selectors
|
|
@@ -182,7 +199,7 @@ function extractRuleSets(css, filePath, declarations) {
|
|
|
182
199
|
}
|
|
183
200
|
// Extract declarations inside this block
|
|
184
201
|
const block = css.slice(braceOpen + 1, braceClose);
|
|
185
|
-
extractDeclarationsFromBlock(block, filePath,
|
|
202
|
+
extractDeclarationsFromBlock(block, filePath, mechanism, selector, atRuleStack.length > 0 ? atRuleStack.join(', ') : null, declarations, css.slice(0, braceOpen).split('\n').length);
|
|
186
203
|
i = braceClose + 1;
|
|
187
204
|
}
|
|
188
205
|
}
|
|
@@ -524,8 +541,13 @@ function parseStyleObjectExpression(expr) {
|
|
|
524
541
|
// CSS-in-JS (styled-components / emotion)
|
|
525
542
|
// ---------------------------------------------------------------------------
|
|
526
543
|
function extractFromCSSinJS(node, sourceCode, filePath, declarations) {
|
|
527
|
-
// Tagged
|
|
528
|
-
|
|
544
|
+
// Tagged templates (styled.div`...` / css`...`) parse as a `call_expression`
|
|
545
|
+
// whose argument is a `template_string` — the WASM grammar emits no
|
|
546
|
+
// `tagged_template_expression` node. Guard on the template argument so a
|
|
547
|
+
// plain function call never reaches the tag-match below.
|
|
548
|
+
if (node.type !== 'call_expression')
|
|
549
|
+
return false;
|
|
550
|
+
if (!findChildByType(node, 'template_string'))
|
|
529
551
|
return false;
|
|
530
552
|
const nodeText = sourceCode.slice(node.range[0], node.range[1]);
|
|
531
553
|
const line = getLine(sourceCode, node.range[0]);
|
|
@@ -558,8 +580,12 @@ function extractFromCSSinJS(node, sourceCode, filePath, declarations) {
|
|
|
558
580
|
// ---------------------------------------------------------------------------
|
|
559
581
|
// Sub-extractor: HTML (limited)
|
|
560
582
|
// ---------------------------------------------------------------------------
|
|
561
|
-
function extractFromHTML(filePath, sourceCode, tailwindTokens) {
|
|
583
|
+
function extractFromHTML(filePath, sourceCode, tailwindTokens, unreadSources) {
|
|
562
584
|
const declarations = [];
|
|
585
|
+
// Embedded <style> blocks (Astro/Vue/Svelte) — extract the CSS text and feed
|
|
586
|
+
// it to the same rule-set parser the .scss path uses (Spec 42 R1). Blocks with
|
|
587
|
+
// an unsupported dialect (sass/less/stylus/…) are recorded as unread (R2).
|
|
588
|
+
extractStyleBlocks(filePath, sourceCode, declarations, unreadSources);
|
|
563
589
|
// Simple class attribute extraction from HTML
|
|
564
590
|
const classRegex = /class(Name)?\s*=\s*"([^"]*)"/g;
|
|
565
591
|
let match;
|
|
@@ -620,6 +646,52 @@ function extractFromHTML(filePath, sourceCode, tailwindTokens) {
|
|
|
620
646
|
}
|
|
621
647
|
return declarations;
|
|
622
648
|
}
|
|
649
|
+
/**
|
|
650
|
+
* Extract `<style>…</style>` blocks from a markup source (Astro/Vue/Svelte).
|
|
651
|
+
*
|
|
652
|
+
* Each block's CSS text is fed to `extractRuleSets`, the same parser the
|
|
653
|
+
* `.scss` path uses, so selectors (including `:global(.foo)`), at-rules, and
|
|
654
|
+
* declarations are extracted identically. The dialect is chosen from the
|
|
655
|
+
* `lang`/`type` attribute:
|
|
656
|
+
* - `css` (or absent) → `css`
|
|
657
|
+
* - `scss` → `scss` (nesting handled by the existing SCSS path)
|
|
658
|
+
* - anything else (`sass`, `less`, `styl`, `postcss`, …) → recorded as an
|
|
659
|
+
* unread source so `undefined-class` never asserts a class defined only here.
|
|
660
|
+
*/
|
|
661
|
+
function extractStyleBlocks(filePath, sourceCode, declarations, unreadSources) {
|
|
662
|
+
const styleRegex = /<style\b([^>]*)>([\s\S]*?)<\/style\s*>/gi;
|
|
663
|
+
let match;
|
|
664
|
+
while ((match = styleRegex.exec(sourceCode)) !== null) {
|
|
665
|
+
const attrs = match[1];
|
|
666
|
+
const body = match[2];
|
|
667
|
+
// Index of the first character of the block body (right after the opening
|
|
668
|
+
// `>`), used to map body line numbers back to the source file.
|
|
669
|
+
const bodyStart = match.index + match[0].indexOf('>') + 1;
|
|
670
|
+
const langAttr = attrs.match(/\blang\s*=\s*["']?([a-zA-Z0-9-]+)/);
|
|
671
|
+
const typeAttr = attrs.match(/\btype\s*=\s*["']?([a-zA-Z0-9-]+)/);
|
|
672
|
+
const lang = (langAttr?.[1] ?? typeAttr?.[1] ?? '').toLowerCase();
|
|
673
|
+
let mechanism;
|
|
674
|
+
if (lang === '' || lang === 'css') {
|
|
675
|
+
mechanism = 'css';
|
|
676
|
+
}
|
|
677
|
+
else if (lang === 'scss') {
|
|
678
|
+
mechanism = 'scss';
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
unreadSources?.push({
|
|
682
|
+
filePath,
|
|
683
|
+
reason: `unsupported style dialect: ${lang}`,
|
|
684
|
+
});
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
// Pad with newlines so that `extractRuleSets`' line arithmetic (which counts
|
|
688
|
+
// newlines up to each `{`) maps onto source-file line numbers. Newlines are
|
|
689
|
+
// whitespace, so selector extraction and at-rule parsing are unaffected.
|
|
690
|
+
const bodyLine0 = sourceCode.slice(0, bodyStart).split('\n').length;
|
|
691
|
+
const padded = '\n'.repeat(bodyLine0 - 1) + body;
|
|
692
|
+
extractRuleSets(padded, filePath, declarations, mechanism);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
623
695
|
// ---------------------------------------------------------------------------
|
|
624
696
|
// AST Helpers
|
|
625
697
|
// ---------------------------------------------------------------------------
|