( absPath: string, beforeContent: string | null, afterContent: string, )
| 132 | * uncheckable), or a ready [SYNTAX_REJECTED] message when it must be refused. |
| 133 | */ |
| 134 | export async function checkSyntaxForWrite( |
| 135 | absPath: string, |
| 136 | beforeContent: string | null, |
| 137 | afterContent: string, |
| 138 | ): Promise<string | null> { |
| 139 | if (!gateEnabled) return null; |
| 140 | if (afterContent.length > MAX_GATED_BYTES) return null; |
| 141 | |
| 142 | const lower = absPath.toLowerCase(); |
| 143 | if (lower.endsWith('.json')) { |
| 144 | const after = checkJsonSyntax(afterContent); |
| 145 | if (after.length === 0) return null; |
| 146 | const beforeHad = beforeContent === null ? null : checkJsonSyntax(beforeContent).length > 0; |
| 147 | return shouldReject(beforeHad, true) ? buildSyntaxRejectMessage(absPath, 'JSON', after) : null; |
| 148 | } |
| 149 | |
| 150 | try { |
| 151 | const { detectLanguage, getParser } = await import('./parser.js'); |
| 152 | const lang = detectLanguage(absPath); |
| 153 | if (!lang) return null; // unknown extension → fail-open |
| 154 | const p = await getParser(lang); |
| 155 | if (!p) return null; // grammar unavailable → fail-open |
| 156 | |
| 157 | const parseIssues = (text: string): SyntaxIssue[] | null => { |
| 158 | let tree: any = null; |
| 159 | try { |
| 160 | tree = p.parser.parse(text); |
| 161 | if (!tree?.rootNode) return null; |
| 162 | return findIssuesInTree(tree.rootNode as TSNodeLike, text); |
| 163 | } catch (e: any) { |
| 164 | // Fail-open (never block the write), but surface that the parser could |
| 165 | // not run so a silently-broken parser isn't mistaken for "clean file". |
| 166 | console.warn(`[syntax-check] parser could not run for ${absPath}; skipping syntax gate: ${e?.message ?? String(e)}`); |
| 167 | return null; // parser hiccup → fail-open |
| 168 | } finally { |
| 169 | try { tree?.delete?.(); } catch { /* wasm cleanup best-effort */ } |
| 170 | } |
| 171 | }; |
| 172 | |
| 173 | const after = parseIssues(afterContent); |
| 174 | if (after === null || after.length === 0) return null; |
| 175 | // Lazy baseline: only parse the original when the candidate is broken. |
| 176 | const beforeIssues = beforeContent === null ? null : parseIssues(beforeContent); |
| 177 | const beforeHad = beforeIssues === null ? (beforeContent === null ? null : true) : beforeIssues.length > 0; |
| 178 | return shouldReject(beforeHad, true) ? buildSyntaxRejectMessage(absPath, lang, after) : null; |
| 179 | } catch (e: any) { |
| 180 | // Fail-open, but log so an unexpectedly-broken gate isn't read as "clean". |
| 181 | console.warn(`[syntax-check] gate could not run for ${absPath}; skipping syntax check: ${e?.message ?? String(e)}`); |
| 182 | return null; // anything unexpected → fail-open |
| 183 | } |
| 184 | } |
no test coverage detected