mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-16 13:15:36 +02:00
458 lines
19 KiB
JavaScript
458 lines
19 KiB
JavaScript
'use strict';
|
|
|
|
const { isSpace, isIdentChar, isLetter, shCodeLines, psCodeLines, batchCodeLines, normalizeShellLine } = require('./gateguard-code-lexer');
|
|
const { MAX_FILE_CHARS, lineEndAt, lineStartOf, cFamilyStartsFresh, pythonStartsFresh, shStartsFresh, psStartsFresh, batchStartsFresh, trivialInFile } = require('./gateguard-file-context');
|
|
|
|
// see docs/gateguard/change-profile.md#change-profile
|
|
|
|
const MAX_SIDE_BYTES = 64 * 1024;
|
|
const MAX_TOTAL_BYTES = 256 * 1024;
|
|
const MAX_EDITS = 64;
|
|
|
|
const UNKNOWN_PROFILE = Object.freeze({ known: false, language: null, touchesPublicSurface: true, touchesData: true, trivial: false });
|
|
|
|
const LANGUAGE_BY_EXT = new Map([
|
|
['.js', 'js'], ['.mjs', 'js'], ['.cjs', 'js'], ['.jsx', 'js'], ['.ts', 'js'], ['.tsx', 'js'], ['.mts', 'js'], ['.cts', 'js'],
|
|
['.py', 'python'], ['.pyi', 'python'],
|
|
['.go', 'go'],
|
|
['.rs', 'rust'],
|
|
['.java', 'java'],
|
|
['.kt', 'kotlin'], ['.kts', 'kotlin'],
|
|
['.cs', 'csharp'],
|
|
['.c', 'c'], ['.h', 'c'],
|
|
['.cc', 'cpp'], ['.cpp', 'cpp'], ['.cxx', 'cpp'], ['.hpp', 'cpp'], ['.hh', 'cpp'], ['.hxx', 'cpp'],
|
|
['.sh', 'shell'], ['.bash', 'shell'], ['.zsh', 'shell'],
|
|
['.ps1', 'powershell'], ['.psm1', 'powershell'],
|
|
['.bat', 'batch'], ['.cmd', 'batch']
|
|
]);
|
|
|
|
// --- Lexing ---
|
|
// see docs/gateguard/change-profile.md#trivial-edits
|
|
|
|
const C_FAMILY = { lineComment: '//', blockComment: true, quotes: '"\'', indentSensitive: false };
|
|
|
|
const C_CONTEXT = { ...C_FAMILY, startsFresh: cFamilyStartsFresh };
|
|
|
|
const LEX_SPECS = {
|
|
js: { ...C_CONTEXT, flavor: 'js', forbiddenCode: '`/', jsx: true },
|
|
go: { ...C_CONTEXT, flavor: 'go', forbiddenCode: '`' },
|
|
rust: { ...C_CONTEXT, flavor: 'rust', quotes: '"', rustChars: true, rawPrefixes: 'r#', rustDocs: true },
|
|
java: { ...C_CONTEXT, flavor: 'java', tripleQuotes: true },
|
|
kotlin: { ...C_CONTEXT, flavor: 'kotlin', tripleQuotes: true, noDollarInStrings: true },
|
|
csharp: { ...C_CONTEXT, flavor: 'csharp', tripleQuotes: true, rawPrefixes: '@$' },
|
|
c: { ...C_CONTEXT, flavor: 'c', rawPrefixes: 'R' },
|
|
cpp: { ...C_CONTEXT, flavor: 'c', rawPrefixes: 'R' },
|
|
python: { lineComment: '#', blockComment: false, quotes: '"\'', indentSensitive: true, tripleQuotes: true, fStrings: true, startsFresh: pythonStartsFresh },
|
|
shell: { lexer: shCodeLines, normalize: line => normalizeShellLine(line, '\\'), startsFresh: shStartsFresh },
|
|
powershell: { lexer: psCodeLines, normalize: line => normalizeShellLine(line, '`'), startsFresh: psStartsFresh },
|
|
batch: { lexer: batchCodeLines, normalize: line => line, startsFresh: batchStartsFresh }
|
|
};
|
|
const TS_EXTS = new Set(['.ts', '.mts', '.cts']);
|
|
const TS_SPEC = { ...LEX_SPECS.js, jsx: false };
|
|
|
|
function lexSpecFor(filePath, language) {
|
|
if (language !== 'js') return LEX_SPECS[language];
|
|
const base = baseName(filePath);
|
|
return TS_EXTS.has(base.slice(base.lastIndexOf('.'))) ? TS_SPEC : LEX_SPECS.js;
|
|
}
|
|
|
|
// --- Public surface ---
|
|
|
|
function leadingWord(text, word) {
|
|
return text.startsWith(word) && !isIdentChar(text[word.length]);
|
|
}
|
|
|
|
function containsWord(text, word) {
|
|
for (let at = text.indexOf(word); at !== -1; at = text.indexOf(word, at + 1)) {
|
|
if (!isIdentChar(text[at - 1]) && !isIdentChar(text[at + word.length])) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function identifierAt(text, start) {
|
|
let end = start;
|
|
while (end < text.length && isIdentChar(text[end])) end++;
|
|
return text.slice(start, end);
|
|
}
|
|
|
|
function skipSpaces(text, at) {
|
|
while (at < text.length && isSpace(text[at])) at++;
|
|
return at;
|
|
}
|
|
|
|
function isUpper(ch) {
|
|
return ch !== undefined && ch >= 'A' && ch <= 'Z';
|
|
}
|
|
|
|
function jsLineIsSurface(line) {
|
|
const t = line.slice(skipSpaces(line, 0));
|
|
return leadingWord(t, 'export') || containsWord(t, 'exports') || leadingWord(t, 'public') || leadingWord(t, 'declare');
|
|
}
|
|
|
|
function pythonNameIsPublic(name) {
|
|
if (!name) return false;
|
|
return !name.startsWith('_') || (name.length > 4 && name.startsWith('__') && name.endsWith('__'));
|
|
}
|
|
|
|
function pythonLineIsSurface(line, index) {
|
|
const start = skipSpaces(line, 0);
|
|
const t = line.slice(start);
|
|
if (t.includes('__all__')) return true;
|
|
for (const keyword of ['def', 'class']) {
|
|
if (leadingWord(t, keyword)) return pythonNameIsPublic(identifierAt(t, skipSpaces(t, keyword.length)));
|
|
}
|
|
if (leadingWord(t, 'async')) {
|
|
const rest = t.slice(skipSpaces(t, 5));
|
|
if (leadingWord(rest, 'def')) return pythonNameIsPublic(identifierAt(rest, skipSpaces(rest, 3)));
|
|
}
|
|
if (start !== 0) return false;
|
|
const name = identifierAt(t, 0);
|
|
const rest = skipSpaces(t, name.length);
|
|
const after = t[rest];
|
|
const assigned = (after === '=' && t[rest + 1] !== '=') || (after === ':' && skipSpaces(t, rest + 1) < t.length);
|
|
if (!name || !isLetter(name[0]) || !assigned) return false;
|
|
return index > 0 ? pythonNameIsPublic(name) : name === name.toUpperCase();
|
|
}
|
|
|
|
function goLineIsSurface(line) {
|
|
const t = line.slice(skipSpaces(line, 0));
|
|
if (isUpper(t[0])) return true;
|
|
if (leadingWord(t, 'package')) return true;
|
|
for (const keyword of ['type', 'var', 'const']) {
|
|
if (leadingWord(t, keyword)) return isUpper(t[skipSpaces(t, keyword.length)]);
|
|
}
|
|
if (!leadingWord(t, 'func')) return false;
|
|
let at = skipSpaces(t, 4);
|
|
if (t[at] === '(') {
|
|
const close = t.indexOf(')', at);
|
|
if (close === -1) return true;
|
|
at = skipSpaces(t, close + 1);
|
|
}
|
|
return isUpper(t[at]);
|
|
}
|
|
|
|
const RUST_SURFACE_ATTRIBUTES = ['#[macro_export', '#[derive', '#[repr', '#[no_mangle', '#[unsafe(no_mangle', '#[export_name', '#[unsafe(export_name'];
|
|
|
|
function rustLineIsSurface(line) {
|
|
const t = line.slice(skipSpaces(line, 0));
|
|
return leadingWord(t, 'pub') || leadingWord(t, 'impl') || leadingWord(t, 'trait') || leadingWord(t, 'extern') ||
|
|
RUST_SURFACE_ATTRIBUTES.some(attribute => t.startsWith(attribute));
|
|
}
|
|
|
|
function shellFunctionName(t) {
|
|
let end = 0;
|
|
while (end < t.length && (isIdentChar(t[end]) || t[end] === '-' || t[end] === ':' || t[end] === '.')) end++;
|
|
if (end === 0) return false;
|
|
const open = skipSpaces(t, end);
|
|
return t[open] === '(' && t[skipSpaces(t, open + 1)] === ')';
|
|
}
|
|
|
|
function shellLineIsSurface(line) {
|
|
const t = line.slice(skipSpaces(line, 0));
|
|
if (leadingWord(t, 'export') || leadingWord(t, 'function')) return true;
|
|
if (leadingWord(t, 'declare') || leadingWord(t, 'typeset')) {
|
|
const flags = t.slice(skipSpaces(t, 7));
|
|
return flags[0] === '-' && identifierAt(flags, 1).includes('x');
|
|
}
|
|
return shellFunctionName(t);
|
|
}
|
|
|
|
const POWERSHELL_SURFACE_WORDS = ['function', 'filter', 'workflow', 'class', 'enum', 'param', 'export-modulemember', '[cmdletbinding', '$global:'];
|
|
|
|
function powershellLineIsSurface(line) {
|
|
const t = line.slice(skipSpaces(line, 0)).toLowerCase();
|
|
return POWERSHELL_SURFACE_WORDS.some(word => t.startsWith(word) && (word.endsWith(':') || !isIdentChar(t[word.length])));
|
|
}
|
|
|
|
const SURFACE_BY_LANGUAGE = {
|
|
js: jsLineIsSurface,
|
|
python: pythonLineIsSurface,
|
|
go: goLineIsSurface,
|
|
rust: rustLineIsSurface,
|
|
shell: shellLineIsSurface,
|
|
powershell: powershellLineIsSurface
|
|
};
|
|
|
|
function baseName(filePath) {
|
|
return String(filePath).replace(/\\/g, '/').split('/').pop().toLowerCase();
|
|
}
|
|
|
|
function wholeFileIsSurface(filePath, language) {
|
|
const base = baseName(filePath);
|
|
return (language === 'js' && (base.endsWith('.d.ts') || base.endsWith('.d.mts') || base.endsWith('.d.cts'))) ||
|
|
(language === 'python' && base === '__init__.py');
|
|
}
|
|
|
|
function textTouchesSurface(text, lineIsSurface) {
|
|
let index = 0;
|
|
let start = 0;
|
|
for (let i = 0; i <= text.length; i++) {
|
|
if (i === text.length || text[i] === '\n' || text[i] === '\r') {
|
|
if (lineIsSurface(text.slice(start, i), index)) return true;
|
|
index++;
|
|
start = i + 1;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// --- Container members ---
|
|
// see docs/gateguard/change-profile.md#public-surface
|
|
|
|
const MAX_OPENER_SCAN = 4 * 1024 * 1024;
|
|
const JS_BODY_KINDS = ['function', 'async'];
|
|
const JS_MEMBER_KINDS = ['class', 'abstract'];
|
|
const JS_CONTAINER_KINDS = ['interface', 'enum', 'type', 'namespace', 'module', 'const', 'let', 'var', 'declare', '{'];
|
|
const RUST_CONTAINER_KINDS = ['enum', 'struct', 'union', 'trait', 'use'];
|
|
|
|
function indentOf(line) {
|
|
return line.slice(0, skipSpaces(line, 0));
|
|
}
|
|
|
|
function lineAt(text, start) {
|
|
const end = lineEndAt(text, start);
|
|
return { text: text.slice(start, end), next: end + (text[end] === '\r' && text[end + 1] === '\n' ? 2 : 1) };
|
|
}
|
|
|
|
function isCommentLine(t) {
|
|
return t.startsWith('//') || t.startsWith('/*') || t.startsWith('*') || t.startsWith('#') || t.startsWith('@');
|
|
}
|
|
|
|
function enclosingOpener(text, at, budget) {
|
|
let end = lineStartOf(text, at);
|
|
while (end > 0) {
|
|
let last = end - 1;
|
|
if (text[last] === '\n' && text[last - 1] === '\r') last--;
|
|
const start = lineStartOf(text, last);
|
|
budget.left -= end - start;
|
|
if (budget.left < 0) return null;
|
|
const line = text.slice(start, last);
|
|
end = start;
|
|
if (!line.trim() || isSpace(line[0])) continue;
|
|
if ('}])'.includes(line[0])) return null;
|
|
if (isCommentLine(line)) continue;
|
|
return { line, start };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function jsContainerKind(opener) {
|
|
const t = opener.trimEnd();
|
|
const bodyLike = t.includes('=>') || containsWord(t, 'function');
|
|
if (containsWord(t, 'exports')) return !bodyLike && /[{[(]$/.test(t) ? 'all' : null;
|
|
if (!leadingWord(t, 'export')) return null;
|
|
let rest = t.slice(skipSpaces(t, 6));
|
|
if (leadingWord(rest, 'default')) rest = rest.slice(skipSpaces(rest, 7));
|
|
if (JS_BODY_KINDS.some(word => leadingWord(rest, word))) return null;
|
|
if (JS_MEMBER_KINDS.some(word => leadingWord(rest, word))) return 'members';
|
|
if (rest.startsWith('{')) return 'all';
|
|
if (!JS_CONTAINER_KINDS.some(word => leadingWord(rest, word))) return null;
|
|
return bodyLike ? null : 'all';
|
|
}
|
|
|
|
function containerKind(opener, language) {
|
|
if (language === 'js') return jsContainerKind(opener);
|
|
if (language === 'python') {
|
|
if (opener.includes('__all__')) return 'all';
|
|
return leadingWord(opener, 'class') && pythonNameIsPublic(identifierAt(opener, skipSpaces(opener, 5))) ? 'fields' : null;
|
|
}
|
|
if (language === 'rust' && leadingWord(opener, 'pub')) {
|
|
const words = opener.split(/[^A-Za-z0-9_]+/);
|
|
return RUST_CONTAINER_KINDS.some(word => words.includes(word)) ? 'members' : null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function memberIndent(text, opener) {
|
|
let at = lineAt(text, opener.start).next;
|
|
while (at < text.length) {
|
|
const line = lineAt(text, at);
|
|
if (line.text.trim()) return indentOf(line.text);
|
|
at = line.next;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function isMemberLine(line, indent, kind) {
|
|
if (!indent || indentOf(line) !== indent) return false;
|
|
const t = line.slice(indent.length);
|
|
if (!t || '}])'.includes(t[0]) || isCommentLine(t)) return false;
|
|
if (kind !== 'fields') return true;
|
|
const name = identifierAt(t, 0);
|
|
const after = t[skipSpaces(t, name.length)];
|
|
return pythonNameIsPublic(name) && isLetter(name[0]) && (after === ':' || (after === '=' && t[skipSpaces(t, name.length) + 1] !== '='));
|
|
}
|
|
|
|
function containerTouchesSurface(pairs, fileText, language) {
|
|
if (typeof fileText !== 'string' || fileText.length > MAX_FILE_CHARS) return false;
|
|
const budget = { left: MAX_OPENER_SCAN };
|
|
for (const [oldString, newString] of pairs) {
|
|
const at = oldString ? fileText.indexOf(oldString) : -1;
|
|
if (at === -1) continue;
|
|
const opener = enclosingOpener(fileText, at, budget);
|
|
if (budget.left < 0) return true;
|
|
const kind = opener ? containerKind(opener.line, language) : null;
|
|
if (!kind) continue;
|
|
if (kind === 'all') return true;
|
|
const indent = memberIndent(fileText, opener);
|
|
const start = lineStartOf(fileText, at);
|
|
const end = lineEndAt(fileText, at + oldString.length);
|
|
const before = fileText.slice(start, end);
|
|
const after = `${fileText.slice(start, at)}${newString}${fileText.slice(at + oldString.length, end)}`;
|
|
if (`${before}\n${after}`.split(/\r\n|\r|\n/).some(line => isMemberLine(line, indent, kind))) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// --- Data handling ---
|
|
|
|
const DATA_WORDS = new Set([
|
|
'json', 'jsonl', 'ndjson', 'yaml', 'yml', 'csv', 'tsv', 'xml', 'toml', 'ini', 'parquet', 'avro', 'arrow', 'pickle', 'protobuf',
|
|
'serde', 'serialize', 'serializer', 'serialise', 'deserialize', 'deserializer', 'deserialise', 'marshal', 'unmarshal',
|
|
'schema', 'schemas', 'sql', 'sqlite', 'sqlite3', 'postgres', 'mysql', 'mongo', 'mongodb', 'database', 'db', 'cursor',
|
|
'migration', 'migrations', 'orm', 'dataframe', 'pandas',
|
|
'date', 'dates', 'datetime', 'timestamp', 'timestamps', 'strftime', 'strptime', 'isoformat', 'iso', 'iso8601', 'timezone',
|
|
'tz', 'utc', 'epoch', 'dayjs', 'moment', 'luxon', 'instant',
|
|
'fs', 'fopen', 'fread', 'fwrite', 'fgets', 'ioutil', 'bufio', 'pathlib', 'shutil', 'readfile', 'writefile', 'fstream',
|
|
'ifstream', 'ofstream',
|
|
'chrono', 'zoneinfo', 'pytz', 'tzinfo', 'temporal', 'base64', 'encoding', 'charset', 'codec', 'msgpack', 'bson', 'cbor',
|
|
'xlsx', 'storage', 'cookie', 'cookies', 'redis', 'prisma', 'knex', 'sequelize', 'mongoose', 'typeorm', 'drizzle',
|
|
'sqlalchemy', 'jdbc', 'firestore', 'dynamodb'
|
|
]);
|
|
const DATA_PAIRS = [
|
|
['read', 'file'], ['write', 'file'], ['append', 'file'], ['open', 'file'], ['read', 'text'], ['write', 'text'],
|
|
['read', 'bytes'], ['write', 'bytes'], ['read', 'to'], ['read', 'lines'], ['write', 'lines'], ['read', 'all'],
|
|
['file', 'reader'], ['file', 'writer'], ['file', 'stream'], ['read', 'csv'], ['to', 'csv'],
|
|
['time', 'now'], ['time', 'parse'], ['time', 'unix'], ['time', 'since'], ['time', 'format'], ['time', 'time'],
|
|
['time', 'duration'], ['time', 'zone'], ['time', 'stamp'], ['system', 'time']
|
|
];
|
|
const SQL_PAIRS = [['select', 'from'], ['insert', 'into'], ['delete', 'from'], ['create', 'table'], ['alter', 'table'], ['drop', 'table'], ['update', 'set']];
|
|
|
|
function* words(text) {
|
|
let word = '';
|
|
let prev = '';
|
|
for (let i = 0; i <= text.length; i++) {
|
|
const ch = text[i];
|
|
const alnum = ch !== undefined && ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9'));
|
|
const boundary = alnum && word && (
|
|
(isUpper(ch) && !isUpper(prev) && !(prev >= '0' && prev <= '9')) ||
|
|
(isUpper(ch) && isUpper(prev) && text[i + 1] >= 'a' && text[i + 1] <= 'z')
|
|
);
|
|
if (!alnum || boundary) {
|
|
if (word) yield { word: word.toLowerCase(), call: !alnum && text[skipSpaces(text, i)] === '(' };
|
|
word = alnum ? ch : '';
|
|
} else {
|
|
word += ch;
|
|
}
|
|
prev = ch;
|
|
}
|
|
}
|
|
|
|
const SHELL_LANGUAGES = new Set(['shell', 'powershell', 'batch']);
|
|
const SHELL_DATA_WORDS = new Set(['curl', 'wget', 'jq', 'yq', 'xmllint', 'psql', 'tee', 'iwr', 'irm', 'clixml']);
|
|
const SHELL_DATA_PAIRS = [['out', 'file'], ['get', 'content'], ['set', 'content'], ['add', 'content'], ['web', 'request'], ['rest', 'method']];
|
|
const NULL_TARGETS = ['/dev/null', '/dev/stdout', '/dev/stderr', '$null', 'nul'];
|
|
|
|
function redirectTargetIsFile(text, at) {
|
|
const start = skipSpaces(text, at);
|
|
const ch = text[start];
|
|
if (ch === undefined || ch === '\n' || ch === '\r' || ch === '&') return false;
|
|
const rest = text.slice(start, start + 12).toLowerCase();
|
|
return !NULL_TARGETS.some(target => rest.startsWith(target) && !isIdentChar(rest[target.length]) && rest[target.length] !== '.');
|
|
}
|
|
|
|
function shellRedirectsFile(text) {
|
|
for (let i = 0; i < text.length; i++) {
|
|
const ch = text[i];
|
|
if (ch === '>') {
|
|
const prev = text[i - 1];
|
|
if (prev === '-' || prev === '=' || prev === '>') continue;
|
|
let at = i + 1;
|
|
if (text[at] === '>' || text[at] === '|') at++;
|
|
if (text[at] === '=' || text[at] === '&') continue;
|
|
if (redirectTargetIsFile(text, at)) return true;
|
|
} else if (ch === '<') {
|
|
const next = text[i + 1];
|
|
if (text[i - 1] === '<' || next === '<' || next === '(' || next === '&' || next === '#' || next === '=') continue;
|
|
if (redirectTargetIsFile(text, i + 1)) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function textTouchesData(text, language) {
|
|
const shell = SHELL_LANGUAGES.has(language);
|
|
if (shell && shellRedirectsFile(text)) return true;
|
|
const seen = new Set();
|
|
let previous = '';
|
|
for (const { word, call } of words(text)) {
|
|
if (DATA_WORDS.has(word)) return true;
|
|
if (shell && (SHELL_DATA_WORDS.has(word) || SHELL_DATA_PAIRS.some(([a, b]) => previous === a && word === b))) return true;
|
|
if (word === 'open' && call) return true;
|
|
if (DATA_PAIRS.some(([a, b]) => previous === a && word === b)) return true;
|
|
seen.add(word);
|
|
previous = word;
|
|
}
|
|
return SQL_PAIRS.some(([a, b]) => seen.has(a) && seen.has(b));
|
|
}
|
|
|
|
// --- Profile ---
|
|
|
|
/** Language of a target by extension, or null when unsupported. */
|
|
function languageFor(filePath) {
|
|
if (typeof filePath !== 'string' || !filePath) return null;
|
|
const base = baseName(filePath);
|
|
const dot = base.lastIndexOf('.');
|
|
if (dot <= 0) return null;
|
|
return LANGUAGE_BY_EXT.get(base.slice(dot)) || null;
|
|
}
|
|
|
|
function changeTexts(input) {
|
|
if (input.tool === 'Write') {
|
|
return typeof input.content === 'string' ? { sides: [input.content], pairs: [] } : null;
|
|
}
|
|
if (input.tool !== 'Edit' || !Array.isArray(input.edits) || input.edits.length === 0 || input.edits.length > MAX_EDITS) return null;
|
|
const pairs = [];
|
|
for (const entry of input.edits) {
|
|
if (!entry || typeof entry !== 'object') return null;
|
|
const oldString = entry.old_string;
|
|
const newString = entry.new_string;
|
|
if (typeof oldString !== 'string' || typeof newString !== 'string') return null;
|
|
pairs.push([oldString, newString, entry.replace_all === true]);
|
|
}
|
|
return { sides: pairs.flatMap(([oldString, newString]) => [oldString, newString]), pairs };
|
|
}
|
|
|
|
function withinBounds(sides) {
|
|
let total = 0;
|
|
for (const side of sides) {
|
|
if (side.length > MAX_SIDE_BYTES) return false;
|
|
const bytes = Buffer.byteLength(side, 'utf8');
|
|
if (bytes > MAX_SIDE_BYTES) return false;
|
|
total += bytes;
|
|
if (total > MAX_TOTAL_BYTES) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/** Profile of one target's change from Edit/MultiEdit entries or Write content, judged against `fileText` when given; unknown on any doubt. */
|
|
function profileChange(input) {
|
|
try {
|
|
if (!input || typeof input !== 'object') return UNKNOWN_PROFILE;
|
|
const language = languageFor(input.filePath);
|
|
if (!language) return UNKNOWN_PROFILE;
|
|
const texts = changeTexts(input);
|
|
if (!texts || !withinBounds(texts.sides)) return UNKNOWN_PROFILE;
|
|
const lineIsSurface = SURFACE_BY_LANGUAGE[language];
|
|
const touchesPublicSurface = input.tool === 'Write' || !lineIsSurface || wholeFileIsSurface(input.filePath, language) ||
|
|
texts.sides.some(side => textTouchesSurface(side, lineIsSurface)) || containerTouchesSurface(texts.pairs, input.fileText, language);
|
|
const touchesData = texts.sides.some(side => textTouchesData(side, language));
|
|
const trivial = input.tool === 'Edit' && trivialInFile(texts.pairs, input.fileText, lexSpecFor(input.filePath, language));
|
|
return Object.freeze({ known: true, language, touchesPublicSurface, touchesData, trivial });
|
|
} catch (_) {
|
|
return UNKNOWN_PROFILE;
|
|
}
|
|
}
|
|
|
|
module.exports = { MAX_SIDE_BYTES, MAX_TOTAL_BYTES, MAX_EDITS, UNKNOWN_PROFILE, languageFor, profileChange };
|