mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-14 20:25:26 +02:00
736 lines
21 KiB
JavaScript
736 lines
21 KiB
JavaScript
'use strict';
|
|
|
|
const { isSpace, isIdentChar, isLetter, rustCharEnd, isBlank, hashStartsComment, hasLoneCarriageReturn, PS_SMART_QUOTES, psHashStartsComment, isTrivialEdit } = require('./gateguard-code-lexer');
|
|
|
|
// --- File context ---
|
|
// see docs/gateguard/change-profile.md#file-context
|
|
|
|
const REPLACEMENT_PATTERN = /\$[$&'`<0-9]/;
|
|
const MAX_FILE_CHARS = 1024 * 1024;
|
|
const MAX_WINDOW_CHARS = 128 * 1024;
|
|
const MAX_CONTEXT_WORK = 8 * 1024 * 1024;
|
|
const JS_REGEX_KEYWORDS = new Set(['return', 'typeof', 'instanceof', 'in', 'of', 'new', 'delete', 'void', 'throw', 'case', 'do', 'else', 'yield', 'await']);
|
|
const JS_CONTROL_HEADERS = new Set(['if', 'while', 'for', 'with']);
|
|
const CPP_RAW_PREFIXES = new Set(['R', 'u8R', 'uR', 'UR', 'LR']);
|
|
|
|
function isNewline(ch) {
|
|
return ch === '\n' || ch === '\r';
|
|
}
|
|
|
|
function lineEndAt(text, i) {
|
|
let j = i;
|
|
while (j < text.length && !isNewline(text[j])) j++;
|
|
return j;
|
|
}
|
|
|
|
function quotedEnd(text, i, quote, multiline) {
|
|
for (let j = i + 1; j < text.length; j++) {
|
|
const ch = text[j];
|
|
if (ch === '\\') {
|
|
j++;
|
|
if (text[j] === '\r' && text[j + 1] === '\n') j++;
|
|
} else if (ch === quote) {
|
|
return j + 1;
|
|
} else if (isNewline(ch) && !multiline) {
|
|
return -1;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function regexEnd(text, i) {
|
|
let inClass = false;
|
|
for (let j = i + 1; j < text.length; j++) {
|
|
const ch = text[j];
|
|
if (isNewline(ch)) return -1;
|
|
if (ch === '\\') {
|
|
if (j + 1 >= text.length || isNewline(text[j + 1])) return -1;
|
|
j++;
|
|
} else if (inClass) {
|
|
if (ch === ']') inClass = false;
|
|
} else if (ch === '[') {
|
|
inClass = true;
|
|
} else if (ch === '/') {
|
|
let k = j + 1;
|
|
while (isIdentChar(text[k])) k++;
|
|
return k;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function nestedBlockEnd(text, i) {
|
|
let depth = 0;
|
|
for (let j = i; j + 1 < text.length; j++) {
|
|
if (text[j] === '/' && text[j + 1] === '*') {
|
|
depth++;
|
|
j++;
|
|
} else if (text[j] === '*' && text[j + 1] === '/') {
|
|
depth--;
|
|
j++;
|
|
if (depth === 0) return j + 1;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function textBlockEnd(text, i, escapes) {
|
|
for (let j = i + 3; j < text.length; j++) {
|
|
if (escapes && text[j] === '\\') {
|
|
j++;
|
|
} else if (!escapes && text[j] === '$' && text[j + 1] === '{') {
|
|
return -1;
|
|
} else if (text.startsWith('"""', j)) {
|
|
let end = j + 3;
|
|
while (text[end] === '"') end++;
|
|
return end;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function kotlinStringEnd(text, i) {
|
|
for (let j = i + 1; j < text.length; j++) {
|
|
const ch = text[j];
|
|
if (ch === '\\') j++;
|
|
else if (ch === '$' && text[j + 1] === '{') return -1;
|
|
else if (ch === '"') return j + 1;
|
|
else if (isNewline(ch)) return -1;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function csharpStringEnd(text, i) {
|
|
let j = i;
|
|
let verbatim = false;
|
|
let interpolated = false;
|
|
while (text[j] === '@' || text[j] === '$') {
|
|
if (text[j] === '@') verbatim = true;
|
|
else interpolated = true;
|
|
j++;
|
|
}
|
|
if (text.startsWith('"""', j)) return -1;
|
|
for (let k = j + 1; k < text.length; k++) {
|
|
const ch = text[k];
|
|
if (ch === '"') {
|
|
if (verbatim && text[k + 1] === '"') {
|
|
k++;
|
|
continue;
|
|
}
|
|
return k + 1;
|
|
}
|
|
if (!verbatim && ch === '\\') {
|
|
k++;
|
|
} else if (!verbatim && isNewline(ch)) {
|
|
return -1;
|
|
} else if (interpolated && ch === '{') {
|
|
if (text[k + 1] === '{') {
|
|
k++;
|
|
continue;
|
|
}
|
|
let m = k + 1;
|
|
while (m < text.length && text[m] !== '}') {
|
|
if ('"\'{/\n\r'.includes(text[m])) return -1;
|
|
m++;
|
|
}
|
|
if (m >= text.length) return -1;
|
|
k = m;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function cppRawEnd(text, quote) {
|
|
const open = text.indexOf('(', quote + 1);
|
|
if (open === -1 || open - quote - 1 > 16) return -1;
|
|
const delimiter = text.slice(quote + 1, open);
|
|
for (const ch of delimiter) {
|
|
if (isSpace(ch) || isNewline(ch) || '()\\"'.includes(ch)) return -1;
|
|
}
|
|
const end = text.indexOf(`)${delimiter}"`, open + 1);
|
|
return end === -1 ? -1 : end + delimiter.length + 2;
|
|
}
|
|
|
|
function rustRawEnd(text, i) {
|
|
let j = i + (text[i] === 'r' ? 1 : 2);
|
|
let hashes = '';
|
|
while (text[j] === '#') {
|
|
hashes += '#';
|
|
j++;
|
|
}
|
|
if (text[j] !== '"') return null;
|
|
const end = text.indexOf(`"${hashes}`, j + 1);
|
|
return end === -1 ? -1 : end + 1 + hashes.length;
|
|
}
|
|
|
|
function wordStart(text, i) {
|
|
let start = i;
|
|
while (start > 0 && isIdentChar(text[start - 1])) start--;
|
|
return start;
|
|
}
|
|
|
|
function cFamilyStringEnd(text, i, spec) {
|
|
const ch = text[i];
|
|
const flavor = spec.flavor;
|
|
if (ch === '`') {
|
|
if (flavor === 'kotlin') return quotedEnd(text, i, '`', false);
|
|
if (flavor !== 'go') return -1;
|
|
const end = text.indexOf('`', i + 1);
|
|
return end === -1 ? -1 : end + 1;
|
|
}
|
|
if (ch === "'") {
|
|
if (flavor === 'rust') {
|
|
const end = rustCharEnd(text, i);
|
|
return end === -1 ? null : end + 1;
|
|
}
|
|
if (flavor === 'c' && isIdentChar(text[i - 1]) && isIdentChar(text[i + 1])) {
|
|
const first = text[wordStart(text, i)];
|
|
if (first >= '0' && first <= '9') return null;
|
|
}
|
|
return quotedEnd(text, i, "'", false);
|
|
}
|
|
if (flavor === 'c') {
|
|
const start = wordStart(text, i);
|
|
const prefix = text.slice(start, i);
|
|
if (CPP_RAW_PREFIXES.has(prefix)) return cppRawEnd(text, i);
|
|
if (prefix.endsWith('R')) return -1;
|
|
}
|
|
if (flavor === 'java' && text.startsWith('"""', i)) return textBlockEnd(text, i, true);
|
|
if (flavor === 'kotlin') return text.startsWith('"""', i) ? textBlockEnd(text, i, false) : kotlinStringEnd(text, i);
|
|
if (flavor === 'csharp' && text.startsWith('"""', i)) return -1;
|
|
return quotedEnd(text, i, '"', flavor === 'rust');
|
|
}
|
|
|
|
function cFamilyStartsFresh(text, spec) {
|
|
const js = spec.flavor === 'js';
|
|
const frames = [{ template: false, depth: 0 }];
|
|
const parens = [];
|
|
let lastWord = '';
|
|
let lastSig = 'op';
|
|
let lineEnd = '';
|
|
let continued = false;
|
|
let i = js && text.startsWith('#!') ? lineEndAt(text, 0) : 0;
|
|
while (i < text.length) {
|
|
const frame = frames[frames.length - 1];
|
|
const ch = text[i];
|
|
const next = text[i + 1];
|
|
if (frame.template) {
|
|
if (ch === '\\') {
|
|
i += 2;
|
|
} else if (ch === '`') {
|
|
frames.pop();
|
|
lastSig = 'value';
|
|
i++;
|
|
} else if (ch === '$' && next === '{') {
|
|
frames.push({ template: false, depth: 0 });
|
|
lastSig = 'op';
|
|
i += 2;
|
|
} else {
|
|
i++;
|
|
}
|
|
continue;
|
|
}
|
|
if (isNewline(ch)) {
|
|
continued = lineEnd === '\\';
|
|
lineEnd = '';
|
|
i++;
|
|
continue;
|
|
}
|
|
if (isSpace(ch)) {
|
|
i++;
|
|
continue;
|
|
}
|
|
if (ch === '/' && next === '/') {
|
|
const end = lineEndAt(text, i);
|
|
if (text.slice(i, end).trimEnd().endsWith('\\')) return false;
|
|
i = end;
|
|
continue;
|
|
}
|
|
if (ch === '/' && next === '*') {
|
|
const end = spec.rustDocs ? nestedBlockEnd(text, i) : text.indexOf('*/', i + 2);
|
|
if (end === -1) return false;
|
|
i = spec.rustDocs ? end : end + 2;
|
|
lineEnd = '';
|
|
continue;
|
|
}
|
|
lineEnd = ch;
|
|
if (js) {
|
|
if (ch === '`') {
|
|
frames.push({ template: true, depth: 0 });
|
|
i++;
|
|
continue;
|
|
}
|
|
if (ch === '/') {
|
|
if (lastSig === 'op') {
|
|
const end = regexEnd(text, i);
|
|
if (end === -1) return false;
|
|
lineEnd = text[end - 1];
|
|
lastSig = 'value';
|
|
i = end;
|
|
continue;
|
|
}
|
|
if (lastSig === 'close' && /[/'"`]/.test(text.slice(i + 1, lineEndAt(text, i)))) return false;
|
|
lastSig = 'op';
|
|
i++;
|
|
continue;
|
|
}
|
|
if (ch === '<' && (next === '!' || (spec.jsx && (isLetter(next) || next === '/' || next === '>')))) return false;
|
|
if (ch === '-' && next === '-' && text[i + 2] === '>') return false;
|
|
if (frames.length > 1 && ch === '{') frame.depth++;
|
|
if (frames.length > 1 && ch === '}') {
|
|
if (frame.depth === 0) {
|
|
frames.pop();
|
|
i++;
|
|
continue;
|
|
}
|
|
frame.depth--;
|
|
}
|
|
}
|
|
if (spec.flavor === 'csharp' && (ch === '@' || ch === '$') && /^[@$]{1,3}"/.test(text.slice(i, i + 4))) {
|
|
const end = csharpStringEnd(text, i);
|
|
if (end === -1) return false;
|
|
lineEnd = '"';
|
|
i = end;
|
|
continue;
|
|
}
|
|
if (spec.flavor === 'rust' && (ch === 'r' || ((ch === 'b' || ch === 'c') && next === 'r')) && !isIdentChar(text[i - 1])) {
|
|
const end = rustRawEnd(text, i);
|
|
if (end === -1) return false;
|
|
if (end !== null) {
|
|
lineEnd = text[end - 1];
|
|
lastSig = 'value';
|
|
i = end;
|
|
continue;
|
|
}
|
|
}
|
|
if (ch === '"' || ch === "'" || ch === '`') {
|
|
const end = cFamilyStringEnd(text, i, spec);
|
|
if (end === -1) return false;
|
|
if (end !== null) {
|
|
lineEnd = text[end - 1];
|
|
lastSig = 'value';
|
|
i = end;
|
|
continue;
|
|
}
|
|
i++;
|
|
continue;
|
|
}
|
|
if (isIdentChar(ch)) {
|
|
let end = i;
|
|
while (end < text.length && isIdentChar(text[end])) end++;
|
|
lastWord = text.slice(i, end);
|
|
lastSig = js && JS_REGEX_KEYWORDS.has(lastWord) ? 'op' : 'value';
|
|
lineEnd = text[end - 1];
|
|
i = end;
|
|
continue;
|
|
}
|
|
if (ch === '(') {
|
|
parens.push(lastSig === 'value' && JS_CONTROL_HEADERS.has(lastWord));
|
|
lastSig = 'op';
|
|
} else if (ch === ')') {
|
|
lastSig = parens.pop() ? 'op' : 'value';
|
|
} else {
|
|
lastSig = ch === '}' ? 'close' : ch === ']' ? 'value' : 'op';
|
|
}
|
|
lastWord = '';
|
|
i++;
|
|
}
|
|
return frames.length === 1 && !continued;
|
|
}
|
|
|
|
function pythonStringEnd(text, i, prefix) {
|
|
const quote = text[i];
|
|
const triple = text[i + 1] === quote && text[i + 2] === quote;
|
|
const fString = prefix.toLowerCase().includes('f');
|
|
let depth = 0;
|
|
for (let j = i + (triple ? 3 : 1); j < text.length; j++) {
|
|
const ch = text[j];
|
|
if (fString && depth > 0) {
|
|
if (ch === '{') {
|
|
depth++;
|
|
} else if (ch === '}') {
|
|
depth--;
|
|
} else if ((ch === '"' || ch === "'") && ch !== quote) {
|
|
let k = j + 1;
|
|
while (k < text.length && text[k] !== ch) {
|
|
if (text[k] === quote || text[k] === '\\' || isNewline(text[k])) return -1;
|
|
k++;
|
|
}
|
|
if (k >= text.length) return -1;
|
|
j = k;
|
|
} else if (ch === quote || ch === '#' || ch === '\\' || isNewline(ch)) {
|
|
return -1;
|
|
}
|
|
continue;
|
|
}
|
|
if (ch === '\\') {
|
|
j++;
|
|
if (text[j] === '\r' && text[j + 1] === '\n') j++;
|
|
} else if (fString && ch === '{') {
|
|
if (text[j + 1] === '{') j++;
|
|
else depth = 1;
|
|
} else if (isNewline(ch) && !triple) {
|
|
return -1;
|
|
} else if (ch === quote && (!triple || (text[j + 1] === quote && text[j + 2] === quote))) {
|
|
return j + (triple ? 3 : 1);
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function pythonStartsFresh(text) {
|
|
let lineEnd = '';
|
|
let continued = false;
|
|
let i = 0;
|
|
while (i < text.length) {
|
|
const ch = text[i];
|
|
if (isNewline(ch)) {
|
|
continued = lineEnd === '\\';
|
|
lineEnd = '';
|
|
i++;
|
|
} else if (isSpace(ch)) {
|
|
i++;
|
|
} else if (ch === '#') {
|
|
i = lineEndAt(text, i);
|
|
} else if (ch === '"' || ch === "'") {
|
|
const start = wordStart(text, i);
|
|
const prefix = text.slice(start, i);
|
|
if (prefix.length > 2 || /[^rbuf]/i.test(prefix)) return false;
|
|
const end = pythonStringEnd(text, i, prefix);
|
|
if (end === -1) return false;
|
|
lineEnd = ch;
|
|
i = end;
|
|
} else {
|
|
lineEnd = ch;
|
|
i++;
|
|
}
|
|
}
|
|
return !continued;
|
|
}
|
|
|
|
function heredocDelimiter(text, i) {
|
|
let j = i + 2;
|
|
const stripTabs = text[j] === '-';
|
|
if (stripTabs) j++;
|
|
while (isBlank(text[j])) j++;
|
|
let word = '';
|
|
while (j < text.length && !isBlank(text[j]) && !isNewline(text[j]) && !';|&<>()'.includes(text[j])) {
|
|
if (!'\'"\\'.includes(text[j])) word += text[j];
|
|
j++;
|
|
}
|
|
return word ? { word, stripTabs, end: j } : null;
|
|
}
|
|
|
|
function skipHeredocs(text, i, pending) {
|
|
let at = i;
|
|
for (const doc of pending) {
|
|
for (;;) {
|
|
if (at >= text.length) return -1;
|
|
const end = lineEndAt(text, at);
|
|
let line = text.slice(at, end);
|
|
if (doc.stripTabs) line = line.replace(/^\t+/, '');
|
|
at = end + (text[end] === '\r' && text[end + 1] === '\n' ? 2 : 1);
|
|
if (line === doc.word) break;
|
|
}
|
|
}
|
|
return at;
|
|
}
|
|
|
|
const MAX_NESTING = 8;
|
|
|
|
function shSingleQuoteEnd(text, i) {
|
|
if (text[i - 1] === '$') return quotedEnd(text, i, "'", true);
|
|
return text.indexOf("'", i + 1) + 1 || -1;
|
|
}
|
|
|
|
function shSubstitutionEnd(text, i, level) {
|
|
if (level > MAX_NESTING) return -1;
|
|
let depth = 0;
|
|
for (let j = i + 1; j < text.length; j++) {
|
|
const ch = text[j];
|
|
if (ch === '\\') {
|
|
j++;
|
|
} else if (ch === '(') {
|
|
depth++;
|
|
} else if (ch === ')') {
|
|
depth--;
|
|
if (depth === 0) return j + 1;
|
|
} else if (ch === "'" || ch === '"') {
|
|
const end = ch === "'" ? shSingleQuoteEnd(text, j) : shDoubleQuoteScan(text, j, level + 1);
|
|
if (end === -1) return -1;
|
|
j = end - 1;
|
|
} else if (ch === '`' || (ch === '<' && text[j + 1] === '<') || (ch === '#' && hashStartsComment(text, j) !== false)) {
|
|
return -1;
|
|
} else if (ch === 'c' && text.startsWith('case', j) && !isIdentChar(text[j - 1]) && !isIdentChar(text[j + 4])) {
|
|
return -1;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function shDoubleQuoteScan(text, i, level) {
|
|
for (let j = i + 1; j < text.length; j++) {
|
|
const ch = text[j];
|
|
if (ch === '"') return j + 1;
|
|
if (ch === '\\') {
|
|
j++;
|
|
} else if (ch === '`') {
|
|
return -1;
|
|
} else if (ch === '$' && text[j + 1] === '(') {
|
|
const end = shSubstitutionEnd(text, j, level);
|
|
if (end === -1) return -1;
|
|
j = end - 1;
|
|
} else if (ch === '$' && text[j + 1] === '{') {
|
|
const close = text.indexOf('}', j + 2);
|
|
if (close === -1 || /["'\n\r]/.test(text.slice(j + 2, close))) return -1;
|
|
j = close;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function shStartsFresh(text) {
|
|
if (hasLoneCarriageReturn(text)) return false;
|
|
const pending = [];
|
|
let lineEnd = '';
|
|
let continued = false;
|
|
let i = 0;
|
|
while (i < text.length) {
|
|
const ch = text[i];
|
|
if (isNewline(ch)) {
|
|
continued = lineEnd === '\\';
|
|
lineEnd = '';
|
|
i += ch === '\r' && text[i + 1] === '\n' ? 2 : 1;
|
|
if (pending.length > 0) {
|
|
i = skipHeredocs(text, i, pending);
|
|
if (i === -1) return false;
|
|
pending.length = 0;
|
|
}
|
|
continue;
|
|
}
|
|
if (isBlank(ch)) {
|
|
i++;
|
|
continue;
|
|
}
|
|
lineEnd = ch;
|
|
if (ch === '\\') {
|
|
i += isNewline(text[i + 1]) ? 1 : 2;
|
|
} else if (ch === "'") {
|
|
const end = shSingleQuoteEnd(text, i);
|
|
if (end === -1) return false;
|
|
i = end;
|
|
} else if (ch === '"') {
|
|
const end = shDoubleQuoteScan(text, i, 0);
|
|
if (end === -1) return false;
|
|
i = end;
|
|
} else if (ch === '`') {
|
|
const end = text.indexOf('`', i + 1);
|
|
if (end === -1 || /['"\\#\n\r]/.test(text.slice(i + 1, end))) return false;
|
|
i = end + 1;
|
|
} else if (ch === '#') {
|
|
const starts = hashStartsComment(text, i);
|
|
if (starts === null) return false;
|
|
i = starts ? lineEndAt(text, i) : i + 1;
|
|
if (starts) lineEnd = '';
|
|
} else if (ch === '<' && text[i + 1] === '<') {
|
|
if (text[i + 2] === '<') {
|
|
i += 3;
|
|
continue;
|
|
}
|
|
const doc = heredocDelimiter(text, i);
|
|
if (!doc) return false;
|
|
pending.push(doc);
|
|
i = doc.end;
|
|
} else {
|
|
i++;
|
|
}
|
|
}
|
|
return !continued && pending.length === 0;
|
|
}
|
|
|
|
function psSingleQuoteEnd(text, i) {
|
|
for (let j = i + 1; j < text.length; j++) {
|
|
if (text[j] !== "'") continue;
|
|
if (text[j + 1] !== "'") return j + 1;
|
|
j++;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function psSubexpressionEnd(text, i, level) {
|
|
if (level > MAX_NESTING) return -1;
|
|
let depth = 0;
|
|
for (let j = i + 1; j < text.length; j++) {
|
|
const ch = text[j];
|
|
if (ch === '`') {
|
|
j++;
|
|
} else if (ch === '(') {
|
|
depth++;
|
|
} else if (ch === ')') {
|
|
depth--;
|
|
if (depth === 0) return j + 1;
|
|
} else if (ch === "'" || ch === '"') {
|
|
const end = ch === "'" ? psSingleQuoteEnd(text, j) : psDoubleQuoteScan(text, j, level + 1);
|
|
if (end === -1) return -1;
|
|
j = end - 1;
|
|
} else if (ch === '#' || (ch === '@' && (text[j + 1] === '"' || text[j + 1] === "'"))) {
|
|
return -1;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function psDoubleQuoteScan(text, i, level) {
|
|
for (let j = i + 1; j < text.length; j++) {
|
|
const ch = text[j];
|
|
if (ch === '`') {
|
|
j++;
|
|
} else if (ch === '$' && text[j + 1] === '(') {
|
|
const end = psSubexpressionEnd(text, j, level);
|
|
if (end === -1) return -1;
|
|
j = end - 1;
|
|
} else if (ch === '"') {
|
|
if (text[j + 1] !== '"') return j + 1;
|
|
j++;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function psStartsFresh(text) {
|
|
if (PS_SMART_QUOTES.test(text)) return false;
|
|
let lineEnd = '';
|
|
let continued = false;
|
|
let i = 0;
|
|
while (i < text.length) {
|
|
const ch = text[i];
|
|
const next = text[i + 1];
|
|
if (isNewline(ch)) {
|
|
continued = lineEnd === '`';
|
|
lineEnd = '';
|
|
i++;
|
|
continue;
|
|
}
|
|
if (isBlank(ch)) {
|
|
i++;
|
|
continue;
|
|
}
|
|
lineEnd = ch;
|
|
if (ch === '`') {
|
|
i += isNewline(next) ? 1 : 2;
|
|
} else if (ch === '@' && (next === '"' || next === "'")) {
|
|
const open = lineEndAt(text, i);
|
|
if (text.slice(i + 2, open).trim() !== '') return false;
|
|
let at = open;
|
|
let found = -1;
|
|
while (at < text.length) {
|
|
at += text[at] === '\r' && text[at + 1] === '\n' ? 2 : 1;
|
|
if (text.startsWith(`${next}@`, at)) {
|
|
found = at + 2;
|
|
break;
|
|
}
|
|
at = lineEndAt(text, at);
|
|
}
|
|
if (found === -1) return false;
|
|
i = found;
|
|
} else if (ch === "'" || ch === '"') {
|
|
const end = ch === "'" ? psSingleQuoteEnd(text, i) : psDoubleQuoteScan(text, i, 0);
|
|
if (end === -1) return false;
|
|
i = end;
|
|
} else if (ch === '<' && next === '#') {
|
|
if (!psHashStartsComment(text, i)) return false;
|
|
const end = text.indexOf('#>', i + 2);
|
|
if (end === -1) return false;
|
|
i = end + 2;
|
|
lineEnd = '';
|
|
} else if (ch === '#') {
|
|
if (psHashStartsComment(text, i)) {
|
|
i = lineEndAt(text, i);
|
|
lineEnd = '';
|
|
} else if (isIdentChar(text[i - 1]) || '-.:'.includes(text[i - 1])) {
|
|
i++;
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
i++;
|
|
}
|
|
}
|
|
return !continued;
|
|
}
|
|
|
|
function batchStartsFresh(text) {
|
|
const lines = text.split(/\r\n|\r|\n/);
|
|
const last = lines.length > 1 ? lines[lines.length - 2] : '';
|
|
return !last.endsWith('^');
|
|
}
|
|
|
|
function lineStartOf(text, i) {
|
|
let start = i;
|
|
while (start > 0 && !isNewline(text[start - 1])) start--;
|
|
return start;
|
|
}
|
|
|
|
function windowStart(text, at) {
|
|
const start = lineStartOf(text, at);
|
|
if (start === 0) return 0;
|
|
let end = start - 1;
|
|
if (text[end] === '\n' && text[end - 1] === '\r') end--;
|
|
return lineStartOf(text, end);
|
|
}
|
|
|
|
function windowEnd(text, at) {
|
|
const end = lineEndAt(text, at);
|
|
if (end >= text.length) return text.length;
|
|
return end + (text[end] === '\r' && text[end + 1] === '\n' ? 2 : 1);
|
|
}
|
|
|
|
function occurrences(text, needle, replaceAll) {
|
|
const first = text.indexOf(needle);
|
|
if (first === -1) return null;
|
|
let last = first;
|
|
for (let at = text.indexOf(needle, first + needle.length); at !== -1; at = text.indexOf(needle, at + needle.length)) {
|
|
if (!replaceAll) return null;
|
|
last = at;
|
|
}
|
|
return { first, last };
|
|
}
|
|
|
|
function trivialInFile(pairs, fileText, spec) {
|
|
if (typeof fileText !== 'string' || fileText.length > MAX_FILE_CHARS || !spec.startsFresh) return false;
|
|
if (spec.flavor === 'go' && fileText.includes('"C"')) return false;
|
|
let text = fileText;
|
|
let work = 0;
|
|
for (const [oldString, newString, replaceAll] of pairs) {
|
|
if (!oldString || REPLACEMENT_PATTERN.test(newString)) return false;
|
|
const found = occurrences(text, oldString, replaceAll);
|
|
if (!found) return false;
|
|
const start = windowStart(text, found.first);
|
|
const end = windowEnd(text, found.last + oldString.length);
|
|
work += start + 2 * (end - start);
|
|
if (end - start > MAX_WINDOW_CHARS || work > MAX_CONTEXT_WORK) return false;
|
|
const before = text.slice(start, end);
|
|
const after = replaceAll
|
|
? before.split(oldString).join(newString)
|
|
: `${text.slice(start, found.first)}${newString}${text.slice(found.first + oldString.length, end)}`;
|
|
// see docs/gateguard/change-profile.md#file-context
|
|
if (!isTrivialEdit(before, after, spec) || !spec.startsFresh(text.slice(0, start), spec)) return false;
|
|
text = `${text.slice(0, start)}${after}${text.slice(end)}`;
|
|
if (text.length > MAX_FILE_CHARS) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
module.exports = {
|
|
MAX_FILE_CHARS,
|
|
lineEndAt,
|
|
lineStartOf,
|
|
cFamilyStartsFresh,
|
|
pythonStartsFresh,
|
|
shStartsFresh,
|
|
psStartsFresh,
|
|
batchStartsFresh,
|
|
trivialInFile
|
|
};
|