|
3 | 3 | import { readFile, readdir, writeFile } from "node:fs/promises"; |
4 | 4 | import { extname, resolve } from "node:path"; |
5 | 5 | 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"; |
6 | 8 |
|
7 | | -const methods = new Set(["record", "nonEmptyObject"]); |
8 | 9 | const ignoredDirectories = new Set([".git", "node_modules"]); |
| 10 | +const tokenRules = [scanNoObjectLiteralRecord, scanRepairNoArgs]; |
9 | 11 |
|
10 | 12 | function tokenize(source) { |
11 | 13 | const tokens = []; |
@@ -50,107 +52,20 @@ function tokenize(source) { |
50 | 52 | return tokens; |
51 | 53 | } |
52 | 54 |
|
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 | | - |
63 | 55 | export function lintSource(source) { |
64 | 56 | 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)); |
135 | 58 | } |
136 | 59 |
|
137 | 60 | export function fixSource(source, problems = lintSource(source)) { |
138 | 61 | 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); |
149 | 64 | for (const edit of edits) { |
150 | 65 | 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)}`; |
152 | 67 | } 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)}`; |
154 | 69 | } |
155 | 70 | } |
156 | 71 | return fixed; |
|
0 commit comments