@mmnto/totem 0.44.0 → 1.1.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/dist/ast-classifier.d.ts +8 -0
- package/dist/ast-classifier.d.ts.map +1 -1
- package/dist/ast-classifier.js +2 -2
- package/dist/ast-classifier.js.map +1 -1
- package/dist/ast-query.d.ts +18 -0
- package/dist/ast-query.d.ts.map +1 -0
- package/dist/ast-query.js +177 -0
- package/dist/ast-query.js.map +1 -0
- package/dist/ast-query.test.d.ts +2 -0
- package/dist/ast-query.test.d.ts.map +1 -0
- package/dist/ast-query.test.js +79 -0
- package/dist/ast-query.test.js.map +1 -0
- package/dist/compiler.d.ts +27 -11
- package/dist/compiler.d.ts.map +1 -1
- package/dist/compiler.js +94 -5
- package/dist/compiler.js.map +1 -1
- package/dist/compiler.test.js +2 -2
- package/dist/compiler.test.js.map +1 -1
- package/dist/config-schema.d.ts +8 -0
- package/dist/config-schema.d.ts.map +1 -1
- package/dist/config-schema.js +4 -0
- package/dist/config-schema.js.map +1 -1
- package/dist/embedders/ollama-embedder.d.ts.map +1 -1
- package/dist/embedders/ollama-embedder.js +5 -1
- package/dist/embedders/ollama-embedder.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/ingest/pipeline.d.ts +11 -0
- package/dist/ingest/pipeline.d.ts.map +1 -1
- package/dist/ingest/pipeline.js +64 -0
- package/dist/ingest/pipeline.js.map +1 -1
- package/dist/ingest/sync.d.ts +1 -1
- package/dist/ingest/sync.d.ts.map +1 -1
- package/dist/ingest/sync.js +1 -1
- package/dist/ingest/sync.js.map +1 -1
- package/dist/lock.d.ts +18 -0
- package/dist/lock.d.ts.map +1 -0
- package/dist/lock.js +192 -0
- package/dist/lock.js.map +1 -0
- package/dist/lock.test.d.ts +2 -0
- package/dist/lock.test.d.ts.map +1 -0
- package/dist/lock.test.js +94 -0
- package/dist/lock.test.js.map +1 -0
- package/package.json +1 -1
package/dist/ast-classifier.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { AstContext } from './compiler.js';
|
|
2
2
|
export type SupportedLanguage = 'typescript' | 'tsx' | 'javascript';
|
|
3
|
+
/**
|
|
4
|
+
* Initialize web-tree-sitter WASM engine. Idempotent — safe to call multiple times.
|
|
5
|
+
*/
|
|
6
|
+
export declare function ensureInit(): Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* Load a Tree-sitter grammar WASM file for the given language.
|
|
9
|
+
*/
|
|
10
|
+
export declare function loadGrammar(lang: SupportedLanguage): Promise<import('web-tree-sitter').Language>;
|
|
3
11
|
/**
|
|
4
12
|
* Classify specific lines of source code by their AST context.
|
|
5
13
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ast-classifier.d.ts","sourceRoot":"","sources":["../src/ast-classifier.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAIhD,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG,KAAK,GAAG,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"ast-classifier.d.ts","sourceRoot":"","sources":["../src/ast-classifier.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAIhD,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG,KAAK,GAAG,YAAY,CAAC;AASpE;;GAEG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAgBhD;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC,OAAO,iBAAiB,EAAE,QAAQ,CAAC,CA0B7C;AAqCD;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EAAE,EACrB,QAAQ,EAAE,iBAAiB,GAC1B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CA+ClC;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAe9E"}
|
package/dist/ast-classifier.js
CHANGED
|
@@ -6,7 +6,7 @@ const grammarCache = new Map();
|
|
|
6
6
|
/**
|
|
7
7
|
* Initialize web-tree-sitter WASM engine. Idempotent — safe to call multiple times.
|
|
8
8
|
*/
|
|
9
|
-
async function ensureInit() {
|
|
9
|
+
export async function ensureInit() {
|
|
10
10
|
if (Parser)
|
|
11
11
|
return;
|
|
12
12
|
if (initPromise)
|
|
@@ -25,7 +25,7 @@ async function ensureInit() {
|
|
|
25
25
|
/**
|
|
26
26
|
* Load a Tree-sitter grammar WASM file for the given language.
|
|
27
27
|
*/
|
|
28
|
-
async function loadGrammar(lang) {
|
|
28
|
+
export async function loadGrammar(lang) {
|
|
29
29
|
const cached = grammarCache.get(lang);
|
|
30
30
|
if (cached)
|
|
31
31
|
return cached;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ast-classifier.js","sourceRoot":"","sources":["../src/ast-classifier.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAQ5C,uDAAuD;AAEvD,IAAI,MAAM,GAAmD,IAAI,CAAC;AAClE,IAAI,WAAW,GAAyB,IAAI,CAAC;AAE7C,MAAM,YAAY,GAAG,IAAI,GAAG,EAAyD,CAAC;AAEtF;;GAEG;AACH,KAAK,UAAU,UAAU;
|
|
1
|
+
{"version":3,"file":"ast-classifier.js","sourceRoot":"","sources":["../src/ast-classifier.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface AstMatch {
|
|
2
|
+
lineNumber: number;
|
|
3
|
+
lineText: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Convenience wrapper: read + parse + query in one call.
|
|
7
|
+
* For batch operations, use `matchAstQueriesBatch` instead.
|
|
8
|
+
*/
|
|
9
|
+
export declare function matchAstQuery(filePath: string, astQuery: string, addedLineNumbers: number[], cwd: string): Promise<AstMatch[]>;
|
|
10
|
+
/**
|
|
11
|
+
* Parse a file once and run multiple AST queries against it efficiently.
|
|
12
|
+
* O(M + N) instead of O(M * N) — file is read and parsed exactly once.
|
|
13
|
+
*/
|
|
14
|
+
export declare function matchAstQueriesBatch(filePath: string, queries: Array<{
|
|
15
|
+
astQuery: string;
|
|
16
|
+
addedLineNumbers: number[];
|
|
17
|
+
}>, cwd: string): Promise<Map<string, AstMatch[]>>;
|
|
18
|
+
//# sourceMappingURL=ast-query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast-query.d.ts","sourceRoot":"","sources":["../src/ast-query.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,QAAQ;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AA6FD;;;GAGG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,gBAAgB,EAAE,MAAM,EAAE,EAC1B,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,QAAQ,EAAE,CAAC,CA0CrB;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,EAChE,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,CA6DlC"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import * as fs from 'node:fs/promises';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { ensureInit, extensionToLanguage, loadGrammar } from './ast-classifier.js';
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
// ─── File reading ───────────────────────────────────
|
|
8
|
+
/**
|
|
9
|
+
* Read file content — try `git show :path` first (staged content), fall back to disk.
|
|
10
|
+
* Fully async — does not block the event loop.
|
|
11
|
+
*/
|
|
12
|
+
async function readFileContent(filePath, cwd) {
|
|
13
|
+
try {
|
|
14
|
+
const { stdout } = await execFileAsync('git', ['show', `:${filePath}`], {
|
|
15
|
+
cwd,
|
|
16
|
+
encoding: 'utf-8',
|
|
17
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB safety cap
|
|
18
|
+
});
|
|
19
|
+
return stdout;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Fall back to disk
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const fullPath = path.resolve(cwd, filePath);
|
|
26
|
+
return await fs.readFile(fullPath, 'utf-8');
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// ─── Query execution ────────────────────────────────
|
|
33
|
+
/**
|
|
34
|
+
* Run a single S-expression query against a parsed tree.
|
|
35
|
+
* Returns matches that overlap with added line numbers.
|
|
36
|
+
*/
|
|
37
|
+
function runQuery(QueryClass, grammar, rootNode, lines, astQuery, addedLineNumbers) {
|
|
38
|
+
let query = null;
|
|
39
|
+
try {
|
|
40
|
+
query = new QueryClass(grammar, astQuery);
|
|
41
|
+
const matches = query.matches(rootNode);
|
|
42
|
+
const results = [];
|
|
43
|
+
for (const match of matches) {
|
|
44
|
+
// Find the @violation capture, or use the first capture
|
|
45
|
+
let targetNode = null;
|
|
46
|
+
for (const capture of match.captures) {
|
|
47
|
+
if (capture.name === 'violation') {
|
|
48
|
+
targetNode = capture.node;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (!targetNode && match.captures.length > 0) {
|
|
53
|
+
targetNode = match.captures[0].node;
|
|
54
|
+
}
|
|
55
|
+
if (!targetNode)
|
|
56
|
+
continue;
|
|
57
|
+
const startLine = targetNode.startPosition.row + 1;
|
|
58
|
+
const endLine = targetNode.endPosition.row + 1;
|
|
59
|
+
for (let lineNum = startLine; lineNum <= endLine; lineNum++) {
|
|
60
|
+
if (addedLineNumbers.has(lineNum)) {
|
|
61
|
+
results.push({
|
|
62
|
+
lineNumber: lineNum,
|
|
63
|
+
lineText: lines[lineNum - 1] ?? '',
|
|
64
|
+
});
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return results;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Invalid query — fail-open
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
query?.delete();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// ─── Public API ─────────────────────────────────────
|
|
80
|
+
/**
|
|
81
|
+
* Convenience wrapper: read + parse + query in one call.
|
|
82
|
+
* For batch operations, use `matchAstQueriesBatch` instead.
|
|
83
|
+
*/
|
|
84
|
+
export async function matchAstQuery(filePath, astQuery, addedLineNumbers, cwd) {
|
|
85
|
+
if (addedLineNumbers.length === 0)
|
|
86
|
+
return [];
|
|
87
|
+
const ext = path.extname(filePath);
|
|
88
|
+
const lang = extensionToLanguage(ext);
|
|
89
|
+
if (!lang)
|
|
90
|
+
return [];
|
|
91
|
+
const content = await readFileContent(filePath, cwd);
|
|
92
|
+
if (!content)
|
|
93
|
+
return [];
|
|
94
|
+
try {
|
|
95
|
+
await ensureInit();
|
|
96
|
+
const grammar = await loadGrammar(lang);
|
|
97
|
+
const TreeSitter = await import('web-tree-sitter');
|
|
98
|
+
const ParserClass = TreeSitter.default?.Parser ?? TreeSitter.Parser;
|
|
99
|
+
const QueryClass = TreeSitter.default?.Query ?? TreeSitter.Query;
|
|
100
|
+
const parser = new ParserClass();
|
|
101
|
+
try {
|
|
102
|
+
parser.setLanguage(grammar);
|
|
103
|
+
const tree = parser.parse(content);
|
|
104
|
+
if (!tree)
|
|
105
|
+
return [];
|
|
106
|
+
try {
|
|
107
|
+
return runQuery(QueryClass, grammar, tree.rootNode, content.split('\n'), astQuery, new Set(addedLineNumbers));
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
tree.delete();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
parser.delete();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Parse a file once and run multiple AST queries against it efficiently.
|
|
123
|
+
* O(M + N) instead of O(M * N) — file is read and parsed exactly once.
|
|
124
|
+
*/
|
|
125
|
+
export async function matchAstQueriesBatch(filePath, queries, cwd) {
|
|
126
|
+
const results = new Map();
|
|
127
|
+
if (queries.length === 0)
|
|
128
|
+
return results;
|
|
129
|
+
const ext = path.extname(filePath);
|
|
130
|
+
const lang = extensionToLanguage(ext);
|
|
131
|
+
if (!lang) {
|
|
132
|
+
for (const q of queries)
|
|
133
|
+
results.set(q.astQuery, []);
|
|
134
|
+
return results;
|
|
135
|
+
}
|
|
136
|
+
const content = await readFileContent(filePath, cwd);
|
|
137
|
+
if (!content) {
|
|
138
|
+
for (const q of queries)
|
|
139
|
+
results.set(q.astQuery, []);
|
|
140
|
+
return results;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
await ensureInit();
|
|
144
|
+
const grammar = await loadGrammar(lang);
|
|
145
|
+
const TreeSitter = await import('web-tree-sitter');
|
|
146
|
+
const ParserClass = TreeSitter.default?.Parser ?? TreeSitter.Parser;
|
|
147
|
+
const QueryClass = TreeSitter.default?.Query ?? TreeSitter.Query;
|
|
148
|
+
const parser = new ParserClass();
|
|
149
|
+
try {
|
|
150
|
+
parser.setLanguage(grammar);
|
|
151
|
+
const tree = parser.parse(content);
|
|
152
|
+
if (!tree) {
|
|
153
|
+
for (const q of queries)
|
|
154
|
+
results.set(q.astQuery, []);
|
|
155
|
+
return results;
|
|
156
|
+
}
|
|
157
|
+
const lines = content.split('\n');
|
|
158
|
+
try {
|
|
159
|
+
for (const { astQuery, addedLineNumbers } of queries) {
|
|
160
|
+
results.set(astQuery, runQuery(QueryClass, grammar, tree.rootNode, lines, astQuery, new Set(addedLineNumbers)));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
tree.delete();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
parser.delete();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
for (const q of queries)
|
|
173
|
+
results.set(q.astQuery, []);
|
|
174
|
+
}
|
|
175
|
+
return results;
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=ast-query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast-query.js","sourceRoot":"","sources":["../src/ast-query.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast-query.test.d.ts","sourceRoot":"","sources":["../src/ast-query.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { matchAstQuery } from './ast-query.js';
|
|
6
|
+
// ─── Helpers ────────────────────────────────────────
|
|
7
|
+
let tmpDir;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-ast-query-'));
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
function writeFile(name, content) {
|
|
15
|
+
const filePath = path.join(tmpDir, name);
|
|
16
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
17
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
18
|
+
return name;
|
|
19
|
+
}
|
|
20
|
+
// ─── matchAstQuery ──────────────────────────────────
|
|
21
|
+
describe('matchAstQuery', () => {
|
|
22
|
+
it('matches a valid S-expression query against code', async () => {
|
|
23
|
+
const file = writeFile('src/app.ts', ['const x = 1;', 'console.log(x);', 'const y = 2;'].join('\n'));
|
|
24
|
+
// Query to find console.log calls — match member_expression with console.log
|
|
25
|
+
const query = '(call_expression function: (member_expression object: (identifier) @obj (#eq? @obj "console"))) @violation';
|
|
26
|
+
const matches = await matchAstQuery(file, query, [2], tmpDir);
|
|
27
|
+
expect(matches).toHaveLength(1);
|
|
28
|
+
expect(matches[0].lineNumber).toBe(2);
|
|
29
|
+
expect(matches[0].lineText).toContain('console.log');
|
|
30
|
+
});
|
|
31
|
+
it('filters matches to only added lines', async () => {
|
|
32
|
+
const file = writeFile('src/multi.ts', ['console.log("line 1");', 'const x = 1;', 'console.log("line 3");'].join('\n'));
|
|
33
|
+
const query = '(call_expression function: (member_expression object: (identifier) @obj (#eq? @obj "console"))) @violation';
|
|
34
|
+
// Only line 3 is "added"
|
|
35
|
+
const matches = await matchAstQuery(file, query, [3], tmpDir);
|
|
36
|
+
expect(matches).toHaveLength(1);
|
|
37
|
+
expect(matches[0].lineNumber).toBe(3);
|
|
38
|
+
});
|
|
39
|
+
it('returns empty array for invalid S-expression (fail-open)', async () => {
|
|
40
|
+
const file = writeFile('src/safe.ts', 'const x = 1;\n');
|
|
41
|
+
const matches = await matchAstQuery(file, '(this is not valid!!!', [1], tmpDir);
|
|
42
|
+
expect(matches).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
it('returns empty array for non-JS/TS files', async () => {
|
|
45
|
+
const file = writeFile('config.py', 'import os\nprint("hello")\n');
|
|
46
|
+
const query = '(identifier) @violation';
|
|
47
|
+
const matches = await matchAstQuery(file, query, [1, 2], tmpDir);
|
|
48
|
+
expect(matches).toEqual([]);
|
|
49
|
+
});
|
|
50
|
+
it('returns empty array when no added lines overlap with matches', async () => {
|
|
51
|
+
const file = writeFile('src/no-overlap.ts', ['console.log("line 1");', 'const x = 1;', 'const y = 2;'].join('\n'));
|
|
52
|
+
const query = '(call_expression function: (member_expression object: (identifier) @obj (#eq? @obj "console"))) @violation';
|
|
53
|
+
// Lines 2 and 3 are "added" but console.log is on line 1
|
|
54
|
+
const matches = await matchAstQuery(file, query, [2, 3], tmpDir);
|
|
55
|
+
expect(matches).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
it('returns empty array for nonexistent file', async () => {
|
|
58
|
+
const matches = await matchAstQuery('nonexistent.ts', '(identifier) @violation', [1], tmpDir);
|
|
59
|
+
expect(matches).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
it('returns empty array for empty addedLineNumbers', async () => {
|
|
62
|
+
const file = writeFile('src/empty.ts', 'const x = 1;\n');
|
|
63
|
+
const matches = await matchAstQuery(file, '(identifier) @violation', [], tmpDir);
|
|
64
|
+
expect(matches).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
it('handles JavaScript files', async () => {
|
|
67
|
+
const file = writeFile('src/app.js', ['const x = 1;', 'console.log(x);'].join('\n'));
|
|
68
|
+
const query = '(call_expression function: (member_expression object: (identifier) @obj (#eq? @obj "console"))) @violation';
|
|
69
|
+
const matches = await matchAstQuery(file, query, [2], tmpDir);
|
|
70
|
+
expect(matches).toHaveLength(1);
|
|
71
|
+
});
|
|
72
|
+
it('handles TSX files', async () => {
|
|
73
|
+
const file = writeFile('src/component.tsx', ['const x = 1;', 'console.log(x);', 'const el = <div>hello</div>;'].join('\n'));
|
|
74
|
+
const query = '(call_expression function: (member_expression object: (identifier) @obj (#eq? @obj "console"))) @violation';
|
|
75
|
+
const matches = await matchAstQuery(file, query, [2], tmpDir);
|
|
76
|
+
expect(matches).toHaveLength(1);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
//# sourceMappingURL=ast-query.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast-query.test.js","sourceRoot":"","sources":["../src/ast-query.test.ts"],"names":[],"mappings":"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"}
|
package/dist/compiler.d.ts
CHANGED
|
@@ -8,8 +8,10 @@ export declare const CompiledRuleSchema: z.ZodObject<{
|
|
|
8
8
|
pattern: z.ZodString;
|
|
9
9
|
/** Human-readable violation message shown when the pattern matches */
|
|
10
10
|
message: z.ZodString;
|
|
11
|
-
/** Engine type —
|
|
12
|
-
engine: z.
|
|
11
|
+
/** Engine type — 'regex' for line-level matching, 'ast' for Tree-sitter S-expression queries */
|
|
12
|
+
engine: z.ZodEnum<["regex", "ast"]>;
|
|
13
|
+
/** Tree-sitter S-expression query (required when engine is 'ast') */
|
|
14
|
+
astQuery: z.ZodOptional<z.ZodString>;
|
|
13
15
|
/** ISO timestamp of when this rule was compiled */
|
|
14
16
|
compiledAt: z.ZodString;
|
|
15
17
|
/** ISO timestamp of when this rule was first created (survives recompilation) */
|
|
@@ -25,8 +27,9 @@ export declare const CompiledRuleSchema: z.ZodObject<{
|
|
|
25
27
|
lessonHeading: string;
|
|
26
28
|
pattern: string;
|
|
27
29
|
message: string;
|
|
28
|
-
engine: "regex";
|
|
30
|
+
engine: "regex" | "ast";
|
|
29
31
|
compiledAt: string;
|
|
32
|
+
astQuery?: string | undefined;
|
|
30
33
|
createdAt?: string | undefined;
|
|
31
34
|
fileGlobs?: string[] | undefined;
|
|
32
35
|
category?: "security" | "architecture" | "style" | "performance" | undefined;
|
|
@@ -36,8 +39,9 @@ export declare const CompiledRuleSchema: z.ZodObject<{
|
|
|
36
39
|
lessonHeading: string;
|
|
37
40
|
pattern: string;
|
|
38
41
|
message: string;
|
|
39
|
-
engine: "regex";
|
|
42
|
+
engine: "regex" | "ast";
|
|
40
43
|
compiledAt: string;
|
|
44
|
+
astQuery?: string | undefined;
|
|
41
45
|
createdAt?: string | undefined;
|
|
42
46
|
fileGlobs?: string[] | undefined;
|
|
43
47
|
category?: "security" | "architecture" | "style" | "performance" | undefined;
|
|
@@ -55,8 +59,10 @@ export declare const CompiledRulesFileSchema: z.ZodObject<{
|
|
|
55
59
|
pattern: z.ZodString;
|
|
56
60
|
/** Human-readable violation message shown when the pattern matches */
|
|
57
61
|
message: z.ZodString;
|
|
58
|
-
/** Engine type —
|
|
59
|
-
engine: z.
|
|
62
|
+
/** Engine type — 'regex' for line-level matching, 'ast' for Tree-sitter S-expression queries */
|
|
63
|
+
engine: z.ZodEnum<["regex", "ast"]>;
|
|
64
|
+
/** Tree-sitter S-expression query (required when engine is 'ast') */
|
|
65
|
+
astQuery: z.ZodOptional<z.ZodString>;
|
|
60
66
|
/** ISO timestamp of when this rule was compiled */
|
|
61
67
|
compiledAt: z.ZodString;
|
|
62
68
|
/** ISO timestamp of when this rule was first created (survives recompilation) */
|
|
@@ -72,8 +78,9 @@ export declare const CompiledRulesFileSchema: z.ZodObject<{
|
|
|
72
78
|
lessonHeading: string;
|
|
73
79
|
pattern: string;
|
|
74
80
|
message: string;
|
|
75
|
-
engine: "regex";
|
|
81
|
+
engine: "regex" | "ast";
|
|
76
82
|
compiledAt: string;
|
|
83
|
+
astQuery?: string | undefined;
|
|
77
84
|
createdAt?: string | undefined;
|
|
78
85
|
fileGlobs?: string[] | undefined;
|
|
79
86
|
category?: "security" | "architecture" | "style" | "performance" | undefined;
|
|
@@ -83,8 +90,9 @@ export declare const CompiledRulesFileSchema: z.ZodObject<{
|
|
|
83
90
|
lessonHeading: string;
|
|
84
91
|
pattern: string;
|
|
85
92
|
message: string;
|
|
86
|
-
engine: "regex";
|
|
93
|
+
engine: "regex" | "ast";
|
|
87
94
|
compiledAt: string;
|
|
95
|
+
astQuery?: string | undefined;
|
|
88
96
|
createdAt?: string | undefined;
|
|
89
97
|
fileGlobs?: string[] | undefined;
|
|
90
98
|
category?: "security" | "architecture" | "style" | "performance" | undefined;
|
|
@@ -99,8 +107,9 @@ export declare const CompiledRulesFileSchema: z.ZodObject<{
|
|
|
99
107
|
lessonHeading: string;
|
|
100
108
|
pattern: string;
|
|
101
109
|
message: string;
|
|
102
|
-
engine: "regex";
|
|
110
|
+
engine: "regex" | "ast";
|
|
103
111
|
compiledAt: string;
|
|
112
|
+
astQuery?: string | undefined;
|
|
104
113
|
createdAt?: string | undefined;
|
|
105
114
|
fileGlobs?: string[] | undefined;
|
|
106
115
|
category?: "security" | "architecture" | "style" | "performance" | undefined;
|
|
@@ -114,8 +123,9 @@ export declare const CompiledRulesFileSchema: z.ZodObject<{
|
|
|
114
123
|
lessonHeading: string;
|
|
115
124
|
pattern: string;
|
|
116
125
|
message: string;
|
|
117
|
-
engine: "regex";
|
|
126
|
+
engine: "regex" | "ast";
|
|
118
127
|
compiledAt: string;
|
|
128
|
+
astQuery?: string | undefined;
|
|
119
129
|
createdAt?: string | undefined;
|
|
120
130
|
fileGlobs?: string[] | undefined;
|
|
121
131
|
category?: "security" | "architecture" | "style" | "performance" | undefined;
|
|
@@ -175,13 +185,19 @@ export type RuleEventCallback = (event: 'trigger' | 'suppress', lessonHash: stri
|
|
|
175
185
|
* Optional `onRuleEvent` callback enables observability metrics collection.
|
|
176
186
|
*/
|
|
177
187
|
export declare function applyRulesToAdditions(rules: CompiledRule[], additions: DiffAddition[], onRuleEvent?: RuleEventCallback): Violation[];
|
|
188
|
+
/**
|
|
189
|
+
* Apply AST-engine compiled rules against pre-extracted diff additions.
|
|
190
|
+
* Async because it reads files and runs Tree-sitter queries.
|
|
191
|
+
* Handles fileGlobs filtering and suppression same as regex rules.
|
|
192
|
+
*/
|
|
193
|
+
export declare function applyAstRulesToAdditions(rules: CompiledRule[], additions: DiffAddition[], cwd: string, onRuleEvent?: RuleEventCallback): Promise<Violation[]>;
|
|
178
194
|
/**
|
|
179
195
|
* Apply compiled rules against added lines from a diff.
|
|
180
196
|
* Returns all violations found.
|
|
181
197
|
* @param excludeFiles — file paths to skip (e.g., compiled-rules.json to avoid self-matches)
|
|
182
198
|
*/
|
|
183
199
|
export declare function applyRules(rules: CompiledRule[], diff: string, excludeFiles?: string[]): Violation[];
|
|
184
|
-
/** Load compiled rules from a JSON file. Returns empty array if file missing
|
|
200
|
+
/** Load compiled rules from a JSON file. Returns empty array if file missing. */
|
|
185
201
|
export declare function loadCompiledRules(rulesPath: string, onWarn?: (msg: string) => void): CompiledRule[];
|
|
186
202
|
/** Load the full compiled rules file (rules + non-compilable cache). */
|
|
187
203
|
export declare function loadCompiledRulesFile(rulesPath: string, onWarn?: (msg: string) => void): CompiledRulesFile;
|
package/dist/compiler.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compiler.d.ts","sourceRoot":"","sources":["../src/compiler.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"compiler.d.ts","sourceRoot":"","sources":["../src/compiler.ts"],"names":[],"mappings":"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"}
|
package/dist/compiler.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import * as crypto from 'node:crypto';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
3
4
|
import safeRegex from 'safe-regex2';
|
|
4
5
|
import { z } from 'zod';
|
|
6
|
+
import { extensionToLanguage } from './ast-classifier.js';
|
|
7
|
+
import { matchAstQueriesBatch } from './ast-query.js';
|
|
8
|
+
import { TotemParseError } from './errors.js';
|
|
5
9
|
// ─── Schemas ─────────────────────────────────────────
|
|
6
10
|
export const CompiledRuleSchema = z.object({
|
|
7
11
|
/** SHA-256 hash (first 16 hex chars) of heading + body — detects edits */
|
|
@@ -12,8 +16,10 @@ export const CompiledRuleSchema = z.object({
|
|
|
12
16
|
pattern: z.string(),
|
|
13
17
|
/** Human-readable violation message shown when the pattern matches */
|
|
14
18
|
message: z.string(),
|
|
15
|
-
/** Engine type —
|
|
16
|
-
engine: z.
|
|
19
|
+
/** Engine type — 'regex' for line-level matching, 'ast' for Tree-sitter S-expression queries */
|
|
20
|
+
engine: z.enum(['regex', 'ast']),
|
|
21
|
+
/** Tree-sitter S-expression query (required when engine is 'ast') */
|
|
22
|
+
astQuery: z.string().optional(),
|
|
17
23
|
/** ISO timestamp of when this rule was compiled */
|
|
18
24
|
compiledAt: z.string(),
|
|
19
25
|
/** ISO timestamp of when this rule was first created (survives recompilation) */
|
|
@@ -247,6 +253,81 @@ export function applyRulesToAdditions(rules, additions, onRuleEvent) {
|
|
|
247
253
|
}
|
|
248
254
|
return violations;
|
|
249
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Apply AST-engine compiled rules against pre-extracted diff additions.
|
|
258
|
+
* Async because it reads files and runs Tree-sitter queries.
|
|
259
|
+
* Handles fileGlobs filtering and suppression same as regex rules.
|
|
260
|
+
*/
|
|
261
|
+
export async function applyAstRulesToAdditions(rules, additions, cwd, onRuleEvent) {
|
|
262
|
+
const astRules = rules.filter((r) => r.engine === 'ast' && r.astQuery);
|
|
263
|
+
if (astRules.length === 0 || additions.length === 0)
|
|
264
|
+
return [];
|
|
265
|
+
// Group additions by file
|
|
266
|
+
const byFile = new Map();
|
|
267
|
+
for (const a of additions) {
|
|
268
|
+
const existing = byFile.get(a.file);
|
|
269
|
+
if (existing) {
|
|
270
|
+
existing.push(a);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
byFile.set(a.file, [a]);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const violations = [];
|
|
277
|
+
// Process each file once — batch all applicable AST queries per file
|
|
278
|
+
for (const [file, fileAdditions] of byFile) {
|
|
279
|
+
// Check language support
|
|
280
|
+
const ext = path.extname(file);
|
|
281
|
+
if (!extensionToLanguage(ext))
|
|
282
|
+
continue;
|
|
283
|
+
// Collect added line numbers, filtering suppressed lines
|
|
284
|
+
const addedLineNumbers = [];
|
|
285
|
+
for (const addition of fileAdditions) {
|
|
286
|
+
if (addition.astContext && addition.astContext !== 'code')
|
|
287
|
+
continue;
|
|
288
|
+
if (isSuppressed(addition.line, addition.precedingLine))
|
|
289
|
+
continue;
|
|
290
|
+
addedLineNumbers.push(addition.lineNumber);
|
|
291
|
+
}
|
|
292
|
+
if (addedLineNumbers.length === 0)
|
|
293
|
+
continue;
|
|
294
|
+
// Collect all applicable rules for this file
|
|
295
|
+
const applicableRules = astRules.filter((rule) => {
|
|
296
|
+
if (rule.fileGlobs && rule.fileGlobs.length > 0) {
|
|
297
|
+
return fileMatchesGlobs(file, rule.fileGlobs);
|
|
298
|
+
}
|
|
299
|
+
return true;
|
|
300
|
+
});
|
|
301
|
+
if (applicableRules.length === 0)
|
|
302
|
+
continue;
|
|
303
|
+
// Batch: parse file once, run all queries against the cached tree
|
|
304
|
+
const queries = applicableRules.map((rule) => ({
|
|
305
|
+
astQuery: rule.astQuery,
|
|
306
|
+
addedLineNumbers,
|
|
307
|
+
}));
|
|
308
|
+
const batchResults = await matchAstQueriesBatch(file, queries, cwd);
|
|
309
|
+
// Map results back to violations
|
|
310
|
+
for (const rule of applicableRules) {
|
|
311
|
+
const matches = batchResults.get(rule.astQuery) ?? [];
|
|
312
|
+
// Check for suppressions per match
|
|
313
|
+
for (const match of matches) {
|
|
314
|
+
const addition = fileAdditions.find((a) => a.lineNumber === match.lineNumber);
|
|
315
|
+
if (addition && isSuppressed(addition.line, addition.precedingLine)) {
|
|
316
|
+
onRuleEvent?.('suppress', rule.lessonHash);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
onRuleEvent?.('trigger', rule.lessonHash);
|
|
320
|
+
violations.push({
|
|
321
|
+
rule,
|
|
322
|
+
file,
|
|
323
|
+
line: match.lineText,
|
|
324
|
+
lineNumber: match.lineNumber,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return violations;
|
|
330
|
+
}
|
|
250
331
|
/**
|
|
251
332
|
* Apply compiled rules against added lines from a diff.
|
|
252
333
|
* Returns all violations found.
|
|
@@ -263,18 +344,22 @@ export function applyRules(rules, diff, excludeFiles) {
|
|
|
263
344
|
return applyRulesToAdditions(rules, additions);
|
|
264
345
|
}
|
|
265
346
|
// ─── File I/O ────────────────────────────────────────
|
|
266
|
-
/** Load compiled rules from a JSON file. Returns empty array if file missing
|
|
347
|
+
/** Load compiled rules from a JSON file. Returns empty array if file missing. */
|
|
267
348
|
export function loadCompiledRules(rulesPath, onWarn) {
|
|
268
349
|
if (!fs.existsSync(rulesPath))
|
|
269
350
|
return [];
|
|
270
351
|
try {
|
|
271
352
|
const raw = fs.readFileSync(rulesPath, 'utf-8');
|
|
272
|
-
const
|
|
353
|
+
const json = JSON.parse(raw);
|
|
354
|
+
const parsed = CompiledRulesFileSchema.parse(json);
|
|
273
355
|
return parsed.rules;
|
|
274
356
|
}
|
|
275
357
|
catch (err) {
|
|
276
358
|
if (err instanceof Error && err.code === 'ENOENT')
|
|
277
359
|
return [];
|
|
360
|
+
if (err instanceof z.ZodError) {
|
|
361
|
+
throw new TotemParseError(`Invalid compiled-rules.json: ${err.issues.map((i) => i.message).join('; ')}`, "Delete the file and run 'totem compile' to regenerate it.");
|
|
362
|
+
}
|
|
278
363
|
onWarn?.(`Could not load compiled rules: ${err instanceof Error ? err.message : String(err)}`);
|
|
279
364
|
return [];
|
|
280
365
|
}
|
|
@@ -285,12 +370,16 @@ export function loadCompiledRulesFile(rulesPath, onWarn) {
|
|
|
285
370
|
return { version: 1, rules: [], nonCompilable: [] };
|
|
286
371
|
try {
|
|
287
372
|
const raw = fs.readFileSync(rulesPath, 'utf-8');
|
|
288
|
-
|
|
373
|
+
const json = JSON.parse(raw);
|
|
374
|
+
return CompiledRulesFileSchema.parse(json);
|
|
289
375
|
}
|
|
290
376
|
catch (err) {
|
|
291
377
|
if (err instanceof Error && err.code === 'ENOENT') {
|
|
292
378
|
return { version: 1, rules: [], nonCompilable: [] };
|
|
293
379
|
}
|
|
380
|
+
if (err instanceof z.ZodError) {
|
|
381
|
+
throw new TotemParseError(`Invalid compiled-rules.json: ${err.issues.map((i) => i.message).join('; ')}`, "Delete the file and run 'totem compile' to regenerate it.");
|
|
382
|
+
}
|
|
294
383
|
onWarn?.(`Could not load compiled rules: ${err instanceof Error ? err.message : String(err)}`);
|
|
295
384
|
return { version: 1, rules: [], nonCompilable: [] };
|
|
296
385
|
}
|