Skip to content

Commit a8c197a

Browse files
Copilotpelikhan
andauthored
Refactor lint scanner to use rule-provided token scans
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
1 parent 97c2cb0 commit a8c197a

3 files changed

Lines changed: 104 additions & 93 deletions

File tree

skills/rig/eslint/lint.js

Lines changed: 8 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import { readFile, readdir, writeFile } from "node:fs/promises";
44
import { extname, resolve } from "node:path";
55
import { pathToFileURL } from "node:url";
6+
import { scanTokens as scanNoObjectLiteralRecord } from "./rules/no-object-literal-record.js";
7+
import { scanTokens as scanRepairNoArgs } from "./rules/repair-no-args.js";
68

7-
const methods = new Set(["record", "nonEmptyObject"]);
89
const ignoredDirectories = new Set([".git", "node_modules"]);
10+
const tokenRules = [scanNoObjectLiteralRecord, scanRepairNoArgs];
911

1012
function tokenize(source) {
1113
const tokens = [];
@@ -50,107 +52,20 @@ function tokenize(source) {
5052
return tokens;
5153
}
5254

53-
function closingBrace(tokens, openingIndex) {
54-
let depth = 0;
55-
for (let index = openingIndex; index < tokens.length; index += 1) {
56-
if (tokens[index].value === "{") depth += 1;
57-
if (tokens[index].value === "}") depth -= 1;
58-
if (depth === 0) return index;
59-
}
60-
return undefined;
61-
}
62-
6355
export function lintSource(source) {
6456
const tokens = tokenize(source);
65-
const problems = [];
66-
67-
for (let index = 0; index <= tokens.length - 5; index += 1) {
68-
const [schema, dot, method, openCall] = tokens.slice(index, index + 4);
69-
if (
70-
tokens[index - 1]?.value === "."
71-
|| schema.value !== "s"
72-
|| dot.value !== "."
73-
|| !methods.has(method.value)
74-
|| openCall.value !== "("
75-
) {
76-
continue;
77-
}
78-
79-
let objectIndex = index + 4;
80-
while (tokens[objectIndex]?.value === "(") objectIndex += 1;
81-
const object = tokens[objectIndex];
82-
if (object?.value !== "{") continue;
83-
84-
const closingIndex = closingBrace(tokens, objectIndex);
85-
const wrapperCount = objectIndex - (index + 4);
86-
const wrappersClose = Array.from(
87-
{ length: wrapperCount },
88-
(_, offset) => tokens[(closingIndex ?? tokens.length) + offset + 1]?.value,
89-
).every((value) => value === ")");
90-
if (closingIndex !== undefined && wrappersClose) {
91-
problems.push({
92-
start: object.start,
93-
end: tokens[closingIndex].end,
94-
message: `Wrap object-valued record fields with s.object(...).`,
95-
});
96-
}
97-
}
98-
99-
for (let index = 0; index <= tokens.length - 3; index += 1) {
100-
const [fn, openParen] = tokens.slice(index, index + 2);
101-
if (
102-
tokens[index - 1]?.value === "."
103-
|| fn.value !== "repair"
104-
|| openParen.value !== "("
105-
) {
106-
continue;
107-
}
108-
109-
const closeIndex = closingParen(tokens, index + 1);
110-
if (closeIndex === undefined) continue;
111-
const hasArgs = tokens
112-
.slice(index + 2, closeIndex)
113-
.some((t) => !/^\s*$/.test(t.value));
114-
if (hasArgs) {
115-
problems.push({
116-
start: openParen.end,
117-
end: tokens[closeIndex].start,
118-
message: "repair() takes no arguments. Set maxTurns on the agent spec instead.",
119-
kind: "repair-no-args",
120-
});
121-
}
122-
}
123-
124-
return problems;
125-
}
126-
127-
function closingParen(tokens, openingIndex) {
128-
let depth = 0;
129-
for (let index = openingIndex; index < tokens.length; index += 1) {
130-
if (tokens[index].value === "(") depth += 1;
131-
if (tokens[index].value === ")") depth -= 1;
132-
if (depth === 0) return index;
133-
}
134-
return undefined;
57+
return tokenRules.flatMap((scan) => scan(tokens));
13558
}
13659

13760
export function fixSource(source, problems = lintSource(source)) {
13861
let fixed = source;
139-
const edits = [];
140-
for (const problem of problems) {
141-
if (problem.kind === "repair-no-args") {
142-
edits.push({ index: problem.start, end: problem.end, text: "" });
143-
} else {
144-
edits.push({ index: problem.start, text: "s.object(" });
145-
edits.push({ index: problem.end, text: ")" });
146-
}
147-
}
148-
edits.sort((left, right) => right.index - left.index);
62+
const edits = problems.flatMap((problem) => problem.edits ?? []);
63+
edits.sort((left, right) => right.start - left.start);
14964
for (const edit of edits) {
15065
if (edit.end !== undefined) {
151-
fixed = `${fixed.slice(0, edit.index)}${edit.text}${fixed.slice(edit.end)}`;
66+
fixed = `${fixed.slice(0, edit.start)}${edit.text}${fixed.slice(edit.end)}`;
15267
} else {
153-
fixed = `${fixed.slice(0, edit.index)}${edit.text}${fixed.slice(edit.index)}`;
68+
fixed = `${fixed.slice(0, edit.start)}${edit.text}${fixed.slice(edit.start)}`;
15469
}
15570
}
15671
return fixed;

skills/rig/eslint/rules/no-object-literal-record.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,57 @@
1+
const methods = new Set(["record", "nonEmptyObject"]);
2+
3+
function closingBrace(tokens, openingIndex) {
4+
let depth = 0;
5+
for (let index = openingIndex; index < tokens.length; index += 1) {
6+
if (tokens[index].value === "{") depth += 1;
7+
if (tokens[index].value === "}") depth -= 1;
8+
if (depth === 0) return index;
9+
}
10+
return undefined;
11+
}
12+
13+
export function scanTokens(tokens) {
14+
const problems = [];
15+
16+
for (let index = 0; index <= tokens.length - 5; index += 1) {
17+
const [schema, dot, method, openCall] = tokens.slice(index, index + 4);
18+
if (
19+
tokens[index - 1]?.value === "."
20+
|| schema.value !== "s"
21+
|| dot.value !== "."
22+
|| !methods.has(method.value)
23+
|| openCall.value !== "("
24+
) {
25+
continue;
26+
}
27+
28+
let objectIndex = index + 4;
29+
while (tokens[objectIndex]?.value === "(") objectIndex += 1;
30+
const object = tokens[objectIndex];
31+
if (object?.value !== "{") continue;
32+
33+
const closingIndex = closingBrace(tokens, objectIndex);
34+
const wrapperCount = objectIndex - (index + 4);
35+
const wrappersClose = Array.from(
36+
{ length: wrapperCount },
37+
(_, offset) => tokens[(closingIndex ?? tokens.length) + offset + 1]?.value,
38+
).every((value) => value === ")");
39+
if (closingIndex === undefined || !wrappersClose) continue;
40+
41+
problems.push({
42+
start: object.start,
43+
end: tokens[closingIndex].end,
44+
message: "Wrap object-valued record fields with s.object(...).",
45+
edits: [
46+
{ start: object.start, text: "s.object(" },
47+
{ start: tokens[closingIndex].end, text: ")" },
48+
],
49+
});
50+
}
51+
52+
return problems;
53+
}
54+
155
export default {
256
meta: {
357
type: "problem",

skills/rig/eslint/rules/repair-no-args.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,45 @@
1+
function closingParen(tokens, openingIndex) {
2+
let depth = 0;
3+
for (let index = openingIndex; index < tokens.length; index += 1) {
4+
if (tokens[index].value === "(") depth += 1;
5+
if (tokens[index].value === ")") depth -= 1;
6+
if (depth === 0) return index;
7+
}
8+
return undefined;
9+
}
10+
11+
export function scanTokens(tokens) {
12+
const problems = [];
13+
14+
for (let index = 0; index <= tokens.length - 3; index += 1) {
15+
const [fn, openParen] = tokens.slice(index, index + 2);
16+
if (
17+
tokens[index - 1]?.value === "."
18+
|| fn.value !== "repair"
19+
|| openParen.value !== "("
20+
) {
21+
continue;
22+
}
23+
24+
const closeIndex = closingParen(tokens, index + 1);
25+
if (closeIndex === undefined) continue;
26+
const hasArgs = tokens
27+
.slice(index + 2, closeIndex)
28+
.some((t) => !/^\s*$/.test(t.value));
29+
if (!hasArgs) continue;
30+
31+
problems.push({
32+
start: openParen.end,
33+
end: tokens[closeIndex].start,
34+
message: "repair() takes no arguments. Set maxTurns on the agent spec instead.",
35+
kind: "repair-no-args",
36+
edits: [{ start: openParen.end, end: tokens[closeIndex].start, text: "" }],
37+
});
38+
}
39+
40+
return problems;
41+
}
42+
143
export default {
244
meta: {
345
type: "problem",

0 commit comments

Comments
 (0)