diff --git a/.github/workflows/pr-regression-gates.yml b/.github/workflows/pr-regression-gates.yml index 278d75aa9..64282dc17 100644 --- a/.github/workflows/pr-regression-gates.yml +++ b/.github/workflows/pr-regression-gates.yml @@ -111,6 +111,16 @@ jobs: npm run sanitizing:line_length -- --base-ref "${GATE_BASE_REF:-origin/main}" npm run sanitizing:file_size -- --base-ref "${GATE_BASE_REF:-origin/main}" + - name: "Check sanitizing layout and boundary regressions (#3168)" + timeout-minutes: 10 + env: + GATE_BASE_REF: ${{ github.event.pull_request.base.ref && format('origin/{0}', github.event.pull_request.base.ref) || '' }} + run: | + npm run sanitizing:file_layout -- --base-ref "${GATE_BASE_REF:-origin/main}" + npm run sanitizing:tauri_edge -- --base-ref "${GATE_BASE_REF:-origin/main}" + npm run sanitizing:global_state -- --base-ref "${GATE_BASE_REF:-origin/main}" + npm run sanitizing:process_env -- --base-ref "${GATE_BASE_REF:-origin/main}" + rust-regression: name: rust-regression # Skip branch-deletion push events while still requiring pull request validation. diff --git a/package.json b/package.json index 7cae58b91..d5b5a63dc 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,10 @@ "dup:changed:self": "node scripts/check-test-duplication.mjs --self-test", "sanitizing:line_length": "node scripts/sanitizing_line_length.mjs", "sanitizing:file_size": "node scripts/sanitizing_file_size.mjs", + "sanitizing:file_layout": "node scripts/sanitizing_file_layout.mjs", + "sanitizing:tauri_edge": "node scripts/sanitizing_tauri_edge.mjs", + "sanitizing:global_state": "node scripts/sanitizing_global_state.mjs", + "sanitizing:process_env": "node scripts/sanitizing_process_env.mjs", "sanitizing:self": "node --test scripts/sanitizing_tests.mjs" }, "dependencies": { diff --git a/scripts/sanitizing_file_layout.mjs b/scripts/sanitizing_file_layout.mjs new file mode 100644 index 000000000..ad0c0545a --- /dev/null +++ b/scripts/sanitizing_file_layout.mjs @@ -0,0 +1,103 @@ +import { codeOrder, gateCli, isMain } from './sanitizing_baseline.mjs'; +import { scanRust } from './sanitizing_rust_scan.mjs'; + +const relevantPath = new RegExp("(?:^|::)(?:test)$"); +function resolveAlias(scan, input, scope) { + const resolved = scan.resolvePath(input,scope); + if (!resolved.ambiguous) return resolved; + const raw = typeof input === 'string' ? input : input.map(t => t.name ?? t.text).join(''); + const suffix = raw.split('::').slice(1).join('::'); + const candidates = [resolved.path,raw]; + for (let b = resolved.binding; b; b = b.previous) if (b.target) + candidates.push(b.target + (suffix ? '::' + suffix : '')); + return { ...resolved, path: candidates.find(p => p && relevantPath.test(p)) ?? resolved.path }; +} + +// Finite syntactic signals, using the shared lexer, scope and production model. +function metric() { return { counts: {}, sites: {}, commands: new Map(), warnings: [] }; } +function hit(m, rule, line, identity) { + m.counts[rule] = (m.counts[rule] ?? 0) + 1; + (m.sites[rule] ??= []).push({ line, ...(identity ? { identity } : {}) }); +} +function warning(m, path, line, resolved) { + if (resolved.ambiguous) m.warnings.push({ path, line, reason: resolved.reason ?? 'ambiguous relevant alias', + remedy: 'use an explicit unambiguous import; signal counted conservatively' }); +} +export function runGate(snapshot) { + const diagnostics = [], measurements = { base: [], head: [] }; + try { + const trees = [snapshot.baseTree, snapshot.headTree], totals = [{}, {}], byPath = [new Map(), new Map()]; + for (const [side, tree] of trees.entries()) { + for (const path of [...tree.keys()].filter(inUniverse).sort(codeOrder)) { + const m = measure(path, scanRust(path, snapshot.readBlob(tree.get(path)))); + byPath[side].set(path, m); + for (const rule of rules) totals[side][rule] = (totals[side][rule] ?? 0) + (m.counts[rule] ?? 0); + measurements[side ? 'head' : 'base'].push({ path, counts: m.counts, sites: m.sites }); + if (side) diagnostics.push(...m.warnings.map(d => ({ gate, status: 'WARN', ...d }))); + } + } + const violation = (rule, path, line, base, head, reason) => diagnostics.push({ gate, status: 'FAIL', rule, path, line, + base, head, baseOid: snapshot.baseOid, headOid: snapshot.headOid, reason, + remedy: 'remove the new signal or use the source-defined owner; do not raise the baseline' }); + for (const rule of rules) if ((totals[1][rule] ?? 0) > (totals[0][rule] ?? 0)) { + const source = [...byPath[1]].find(([p,m]) => (m.counts[rule] ?? 0) > (byPath[0].get(p)?.counts[rule] ?? 0)); + violation(rule, source?.[0] ?? '', source?.[1].sites[rule]?.[0]?.line ?? 1, + totals[0][rule] ?? 0, totals[1][rule], 'global subrule debt increased'); + } + for (const [path, m] of byPath[1]) { + if (!snapshot.baseTree.has(path)) for (const rule of newFileRules) if (m.counts[rule]) + violation(rule, path, m.sites[rule][0].line, 0, m.counts[rule], 'new file must satisfy this subrule even with compensated totals'); + extraChecks(path, m, byPath[0].get(path), violation); + } + snapshot.assertStableHead(); + const fail = diagnostics.some(d => d.status === 'FAIL'); + return { gate, status: fail ? 'FAIL' : 'PASS', exit: fail ? 1 : 0, requestedRef: snapshot.requestedRef, + refOid: snapshot.refOid, baseOid: snapshot.baseOid, headOid: snapshot.headOid, totals, measurements, diagnostics }; + } catch (e) { + return { gate, status: 'ERROR', exit: e.exit ?? 3, baseOid: snapshot.baseOid, headOid: snapshot.headOid, + diagnostics: [...diagnostics, { gate, reason: e.message, remedy: 'repair inputs and rerun; no verdict available' }] }; + } +} + +const gate = '05'; +const rules = ['mod_rs', 'test_files', 'inline_mod', 'path', 'glob', 'pub_mod', 'pub_super_mod', 'pub_in_mod']; +const newFileRules = rules.slice(0, 5); +const inUniverse = p => /^src-tauri\/(?:src|tests)\/.*\.rs$/.test(p); +const extraChecks = () => {}; +function measure(path, scan) { + const m = metric(), integration = path.startsWith('src-tauri/tests/') || /(?:^|\/)tests\//.test(path); + if (path.endsWith('/mod.rs')) hit(m, 'mod_rs', 1); + let testLine; + for (const a of scan.attributes) for (const meta of a.signals) { + const r = resolveAlias(scan,meta.name,a.scope); + if (['test', 'tokio::test', 'async_std::test'].includes(r.path) || r.ambiguous && /(?:^|::)test$/.test(r.path ?? meta.name)) { + testLine ??= a.line; warning(m, path, a.line, r); + } + if (meta.name === 'path') hit(m, 'path', a.line); + } + if (testLine && !path.endsWith('/tests.rs') && !integration) hit(m, 'test_files', testLine); + const ts = scan.tokens; + for (const item of scan.items) { + if (item.kind === 'mod') { + if (item.bodyScope) hit(m, 'inline_mod', item.line); + // Visibility belongs to this item, including attributes preceding pub(...). + const prefix = ts.filter(t => t.start >= item.start && t.index < item.first); + const pub = prefix.findIndex(t => t.name === 'pub'); + if (pub >= 0) { + if (prefix[pub + 1]?.text !== '(') { + if (path !== 'src-tauri/src/lib.rs') hit(m, 'pub_mod', item.line); + } else if (prefix[pub + 2]?.name === 'super') hit(m, 'pub_super_mod', item.line); + else if (prefix[pub + 2]?.name === 'in') hit(m, 'pub_in_mod', item.line); + } + } + } + for (const scope of scan.scopes) for (const entry of scope.imports) if (entry.glob) { + if (path.endsWith('/tests.rs') && entry.target === 'super::*') continue; + const use = scan.items.find(i => i.kind === 'use' && i.scope === scope && + ts.slice(i.first, i.last).some(t => t.text === '*')); + hit(m, 'glob', use?.line ?? 1); + } + return m; +} + +if (isMain(import.meta.url)) process.exitCode = await gateCli(runGate); diff --git a/scripts/sanitizing_global_state.mjs b/scripts/sanitizing_global_state.mjs new file mode 100644 index 000000000..589e4a71c --- /dev/null +++ b/scripts/sanitizing_global_state.mjs @@ -0,0 +1,153 @@ +import { codeOrder, gateCli, isMain } from './sanitizing_baseline.mjs'; +import { scanRust } from './sanitizing_rust_scan.mjs'; + +const relevantPath = /(?:^|::)(?:Mutex|RwLock|Atomic\w*|OnceLock|OnceCell|Lazy|LazyLock|Cell|RefCell)$/; +function resolveAlias(scan, input, scope) { + const resolved = scan.resolvePath(input,scope); + if (!resolved.ambiguous) return resolved; + const raw = typeof input === 'string' ? input : input.map(t => t.name ?? t.text).join(''); + const suffix = raw.split('::').slice(1).join('::'); + const candidates = [resolved.path,raw]; + for (let b = resolved.binding; b; b = b.previous) if (b.target) + candidates.push(b.target + (suffix ? '::' + suffix : '')); + return { ...resolved, path: candidates.find(p => p && relevantPath.test(p)) ?? resolved.path }; +} + +// Finite syntactic signals, using the shared lexer, scope and production model. +function qualified(scan, tokens, i) { + let end = i + 1; + while (tokens[end]?.text === '::' && tokens[end + 1]?.kind === 'ident') end += 2; + const start = tokens[i - 1]?.text === '::' && tokens[i - 2]?.kind !== 'ident' ? i - 1 : i; + const resolved = resolveAlias(scan,tokens.slice(start, end),tokens[i].scope); + return { ...resolved, end, raw: tokens.slice(start, end).map(t => t.name ?? t.text).join('') }; +} +function metric() { return { counts: {}, sites: {}, commands: new Map(), warnings: [] }; } +function hit(m, rule, line, identity) { + m.counts[rule] = (m.counts[rule] ?? 0) + 1; + (m.sites[rule] ??= []).push({ line, ...(identity ? { identity } : {}) }); +} +function warning(m, path, line, resolved) { + if (resolved.ambiguous) m.warnings.push({ path, line, reason: resolved.reason ?? 'ambiguous relevant alias', + remedy: 'use an explicit unambiguous import; signal counted conservatively' }); +} +export function runGate(snapshot) { + const diagnostics = [], measurements = { base: [], head: [] }; + try { + const trees = [snapshot.baseTree, snapshot.headTree], totals = [{}, {}], byPath = [new Map(), new Map()]; + for (const [side, tree] of trees.entries()) { + for (const path of [...tree.keys()].filter(inUniverse).sort(codeOrder)) { + const m = measure(path, scanRust(path, snapshot.readBlob(tree.get(path)))); + byPath[side].set(path, m); + for (const rule of rules) totals[side][rule] = (totals[side][rule] ?? 0) + (m.counts[rule] ?? 0); + measurements[side ? 'head' : 'base'].push({ path, counts: m.counts, sites: m.sites }); + if (side) diagnostics.push(...m.warnings.map(d => ({ gate, status: 'WARN', ...d }))); + } + } + const violation = (rule, path, line, base, head, reason) => diagnostics.push({ gate, status: 'FAIL', rule, path, line, + base, head, baseOid: snapshot.baseOid, headOid: snapshot.headOid, reason, + remedy: 'remove the new signal or use the source-defined owner; do not raise the baseline' }); + for (const rule of rules) if ((totals[1][rule] ?? 0) > (totals[0][rule] ?? 0)) { + const source = [...byPath[1]].find(([p,m]) => (m.counts[rule] ?? 0) > (byPath[0].get(p)?.counts[rule] ?? 0)); + violation(rule, source?.[0] ?? '', source?.[1].sites[rule]?.[0]?.line ?? 1, + totals[0][rule] ?? 0, totals[1][rule], 'global subrule debt increased'); + } + for (const [path, m] of byPath[1]) { + if (!snapshot.baseTree.has(path)) for (const rule of newFileRules) if (m.counts[rule]) + violation(rule, path, m.sites[rule][0].line, 0, m.counts[rule], 'new file must satisfy this subrule even with compensated totals'); + extraChecks(path, m, byPath[0].get(path), violation); + } + snapshot.assertStableHead(); + const fail = diagnostics.some(d => d.status === 'FAIL'); + return { gate, status: fail ? 'FAIL' : 'PASS', exit: fail ? 1 : 0, requestedRef: snapshot.requestedRef, + refOid: snapshot.refOid, baseOid: snapshot.baseOid, headOid: snapshot.headOid, totals, measurements, diagnostics }; + } catch (e) { + return { gate, status: 'ERROR', exit: e.exit ?? 3, baseOid: snapshot.baseOid, headOid: snapshot.headOid, + diagnostics: [...diagnostics, { gate, reason: e.message, remedy: 'repair inputs and rerun; no verdict available' }] }; + } +} + +const gate = '08'; +const rules = ['globals']; +const newFileRules = rules; +const inUniverse = p => /^src-tauri\/src\/.*\.rs$/.test(p); +const extraChecks = () => {}; +const stateType = /(?:^|::)(?:Mutex|RwLock|Atomic\w*|OnceLock|OnceCell|Lazy|LazyLock)$/; +function canonical(scan, tokens) { + const out = []; + for (let i = 0; i < tokens.length; i++) { + if (tokens[i].text === '::' && tokens[i + 1]?.kind === 'ident' && tokens[i - 1]?.kind !== 'ident') i++; + const t = tokens[i]; + if (t.kind === 'string') { out.push('@literal'); continue; } + if (t.kind === 'ident' && tokens[i - 1]?.text !== '.' && !(tokens[i - 1]?.text === '::' && tokens[i - 2]?.kind === 'ident')) { + const r = qualified(scan,tokens,i); + if (r.ambiguous || r.shadowed || !r.path) return null; + out.push(r.path); i = r.end - 1; + } else out.push(t.name ?? t.text); + } + return out.join(' '); +} +function literalClosure(scan, tokens) { + if (tokens[0]?.text !== '||') return false; + let body = tokens.slice(1); + while (body[0]?.text === '{' && body[0].mate === body.at(-1)?.index) body = body.slice(1,-1); + const value = canonical(scan,body); + return value === 'regex::Regex::new ( @literal ) . unwrap ( )' || + value === 'regex::Regex::new ( @literal ) . expect ( @literal )'; +} +function readonlyRegex(scan, item, type, init) { + const typeText = canonical(scan,type); + if (typeText === 'std::sync::LazyLock < regex::Regex >') { + const open = init.findIndex(t => t.text === '('); + if (open < 0 || init[open].mate !== init.at(-1)?.index) return false; + return canonical(scan,init.slice(0,open)) === 'std::sync::LazyLock::new' && literalClosure(scan,init.slice(open + 1,-1)); + } + if (typeText !== 'std::sync::OnceLock < regex::Regex >' || !item || + canonical(scan,init) !== 'std::sync::OnceLock::new ( )') return false; + const ts = scan.tokens; + if (ts.filter(t => t.start >= item.start && t.index < item.first).some(t => t.name === 'pub')) return false; + const fn = scan.items.filter(i => i.kind === 'fn' && i.bodyStart < item.start && i.end > item.end).at(-1); + if (!fn) return false; + const refs = ts.filter(t => t.start > fn.bodyStart && t.start < fn.end && t.name === item.name && + !(t.index >= item.first && t.index <= item.last)); + if (!refs.length) return false; + for (const ref of refs) { + const i = ref.index; + if ((ts[i - 1]?.text === '.' || ts[i - 1]?.text === '::' && ts[i - 2]?.kind === 'ident') || ts[i + 1]?.text !== '.' || ts[i + 2]?.name !== 'get_or_init' || ts[i + 3]?.text !== '(') return false; + const end = ts[i + 3].mate; + if (!literalClosure(scan,ts.slice(i + 4,end))) return false; + } + return true; +} +function measure(path, scan) { + const m = metric(), ts = scan.tokens; + // The scanner deliberately does not expand macros. Inspect only the explicit + // static declarations in thread_local!'s token tree, never arbitrary macros. + const threadGroups = scan.groups.filter(g => ts[g.start - 1]?.text === '!' && ts[g.start - 2]?.name === 'thread_local'); + const declarations = scan.items.filter(i => i.kind === 'static').map(item => ({ item, first: item.first, last: item.last, thread: false })); + for (const group of threadGroups) for (let i = group.start + 1; i < group.end; i++) if (ts[i].name === 'static') { + let end = i + 1; + for (; end < group.end && ts[end].text !== ';'; end++) if (['(','[','{'].includes(ts[end].text)) end = ts[end].mate; + if (end === group.end) throw new Error(`${path}:${ts[i].line}: thread_local static not delimitable`); + declarations.push({ item: null, first: i, last: end, thread: true }); i = end; + } + for (const d of declarations) { + if (scan.isTestOffset(ts[d.first].start)) continue; + const segment = ts.slice(d.first,d.last), colon = segment.findIndex(t => t.text === ':'), eq = segment.findIndex(t => t.text === '='); + if (colon < 0 || eq < colon) throw new Error(`${path}:${ts[d.first].line}: static type/initializer not delimitable`); + const type = segment.slice(colon + 1,eq), init = segment.slice(eq + 1); + let state = segment[1]?.name === 'mut'; + for (let i = 0; i < type.length; i++) if (type[i].kind === 'ident' && !(type[i - 1]?.text === '::' && type[i - 2]?.kind === 'ident')) { + const r = qualified(scan,type,i), value = r.path ?? (r.ambiguous ? r.raw : ''); + if (stateType.test(value) || d.thread && /(?:^|::)(?:Cell|RefCell)$/.test(value)) { + state = true; warning(m,path,type[i].line,r); + } + i = r.end - 1; + } + if (!state) continue; + if (segment[1]?.name !== 'mut' && readonlyRegex(scan,d.item,type,init)) continue; + hit(m,'globals',ts[d.first].line); + } + return m; +} + +if (isMain(import.meta.url)) process.exitCode = await gateCli(runGate); diff --git a/scripts/sanitizing_process_env.mjs b/scripts/sanitizing_process_env.mjs new file mode 100644 index 000000000..490e529bf --- /dev/null +++ b/scripts/sanitizing_process_env.mjs @@ -0,0 +1,101 @@ +import { codeOrder, gateCli, isMain } from './sanitizing_baseline.mjs'; +import { scanRust } from './sanitizing_rust_scan.mjs'; + +const relevantPath = new RegExp("^(?:(?:std|tokio)::process::Command::new|Command::new|(?:std::)?env::(?:var|var_os|set_var|remove_var))$"); +function resolveAlias(scan, input, scope) { + const resolved = scan.resolvePath(input,scope); + if (!resolved.ambiguous) return resolved; + const raw = typeof input === 'string' ? input : input.map(t => t.name ?? t.text).join(''); + const suffix = raw.split('::').slice(1).join('::'); + const candidates = [resolved.path,raw]; + for (let b = resolved.binding; b; b = b.previous) if (b.target) + candidates.push(b.target + (suffix ? '::' + suffix : '')); + return { ...resolved, path: candidates.find(p => p && relevantPath.test(p)) ?? resolved.path }; +} + +// Finite syntactic signals, using the shared lexer, scope and production model. +function qualified(scan, tokens, i) { + let end = i + 1; + while (tokens[end]?.text === '::' && tokens[end + 1]?.kind === 'ident') end += 2; + const start = tokens[i - 1]?.text === '::' && tokens[i - 2]?.kind !== 'ident' ? i - 1 : i; + const resolved = resolveAlias(scan,tokens.slice(start, end),tokens[i].scope); + return { ...resolved, end, raw: tokens.slice(start, end).map(t => t.name ?? t.text).join('') }; +} +function metric() { return { counts: {}, sites: {}, commands: new Map(), warnings: [] }; } +function hit(m, rule, line, identity) { + m.counts[rule] = (m.counts[rule] ?? 0) + 1; + (m.sites[rule] ??= []).push({ line, ...(identity ? { identity } : {}) }); +} +function warning(m, path, line, resolved) { + if (resolved.ambiguous) m.warnings.push({ path, line, reason: resolved.reason ?? 'ambiguous relevant alias', + remedy: 'use an explicit unambiguous import; signal counted conservatively' }); +} +export function runGate(snapshot) { + const diagnostics = [], measurements = { base: [], head: [] }; + try { + const trees = [snapshot.baseTree, snapshot.headTree], totals = [{}, {}], byPath = [new Map(), new Map()]; + for (const [side, tree] of trees.entries()) { + for (const path of [...tree.keys()].filter(inUniverse).sort(codeOrder)) { + const m = measure(path, scanRust(path, snapshot.readBlob(tree.get(path)))); + byPath[side].set(path, m); + for (const rule of rules) totals[side][rule] = (totals[side][rule] ?? 0) + (m.counts[rule] ?? 0); + measurements[side ? 'head' : 'base'].push({ path, counts: m.counts, sites: m.sites }); + if (side) diagnostics.push(...m.warnings.map(d => ({ gate, status: 'WARN', ...d }))); + } + } + const violation = (rule, path, line, base, head, reason) => diagnostics.push({ gate, status: 'FAIL', rule, path, line, + base, head, baseOid: snapshot.baseOid, headOid: snapshot.headOid, reason, + remedy: 'remove the new signal or use the source-defined owner; do not raise the baseline' }); + for (const rule of rules) if ((totals[1][rule] ?? 0) > (totals[0][rule] ?? 0)) { + const source = [...byPath[1]].find(([p,m]) => (m.counts[rule] ?? 0) > (byPath[0].get(p)?.counts[rule] ?? 0)); + violation(rule, source?.[0] ?? '', source?.[1].sites[rule]?.[0]?.line ?? 1, + totals[0][rule] ?? 0, totals[1][rule], 'global subrule debt increased'); + } + for (const [path, m] of byPath[1]) { + if (!snapshot.baseTree.has(path)) for (const rule of newFileRules) if (m.counts[rule]) + violation(rule, path, m.sites[rule][0].line, 0, m.counts[rule], 'new file must satisfy this subrule even with compensated totals'); + extraChecks(path, m, byPath[0].get(path), violation); + } + snapshot.assertStableHead(); + const fail = diagnostics.some(d => d.status === 'FAIL'); + return { gate, status: fail ? 'FAIL' : 'PASS', exit: fail ? 1 : 0, requestedRef: snapshot.requestedRef, + refOid: snapshot.refOid, baseOid: snapshot.baseOid, headOid: snapshot.headOid, totals, measurements, diagnostics }; + } catch (e) { + return { gate, status: 'ERROR', exit: e.exit ?? 3, baseOid: snapshot.baseOid, headOid: snapshot.headOid, + diagnostics: [...diagnostics, { gate, reason: e.message, remedy: 'repair inputs and rerun; no verdict available' }] }; + } +} + +const gate = '09'; +const rules = ['launch_files', 'env_read_files', 'env_write']; +const newFileRules = ['launch_files', 'env_read_files']; +const inUniverse = p => /^src-tauri\/src\/.*\.rs$/.test(p); +function extraChecks(path, m, before, violation) { + if (m.counts.env_write) violation('env_write', path, m.sites.env_write[0].line, + before?.counts.env_write ?? 0, m.counts.env_write, 'global environment mutation outside main.rs is forbidden, including existing debt'); +} +function measure(path, scan) { + const m = metric(), ts = scan.productionTokens; + const launchOwner = /^src-tauri\/src\/(?:process\.rs$|process\/|pty\/)/.test(path); + const readOwner = /^src-tauri\/src\/(?:config\.rs$|config\/|main\.rs$|lib\.rs$)/.test(path); + const writeOwner = path === 'src-tauri/src/main.rs'; + let launch, read; + for (let i = 0; i < ts.length; i++) { + if (ts[i].kind !== 'ident' || (ts[i - 1]?.text === '.' || ts[i - 1]?.text === '::' && ts[i - 2]?.kind === 'ident')) continue; + const r = qualified(scan, ts, i), value = r.path ?? (r.ambiguous ? r.raw : ''); + if (ts[r.end]?.text !== '(') continue; + if (/^(?:(?:std|tokio)::process::)?Command::new$/.test(value)) { launch ??= ts[i].line; warning(m,path,ts[i].line,r); } + if (/^(?:(?:std::)?env::)?(?:var|var_os)$/.test(value) && (value.includes('env::') || r.binding?.kind === 'import')) { + read ??= ts[i].line; warning(m,path,ts[i].line,r); + } + if (/^(?:(?:std::)?env::)?(?:set_var|remove_var)$/.test(value) && (value.includes('env::') || r.binding?.kind === 'import')) { + if (!writeOwner) hit(m,'env_write',ts[i].line); warning(m,path,ts[i].line,r); + } + i = r.end - 1; + } + if (launch && !launchOwner) hit(m,'launch_files',launch); + if (read && !readOwner) hit(m,'env_read_files',read); + return m; +} + +if (isMain(import.meta.url)) process.exitCode = await gateCli(runGate); diff --git a/scripts/sanitizing_tauri_edge.mjs b/scripts/sanitizing_tauri_edge.mjs new file mode 100644 index 000000000..ddfe0654d --- /dev/null +++ b/scripts/sanitizing_tauri_edge.mjs @@ -0,0 +1,124 @@ +import { codeOrder, gateCli, isMain } from './sanitizing_baseline.mjs'; +import { scanRust } from './sanitizing_rust_scan.mjs'; + +const relevantPath = new RegExp("^(?:tauri(?:::|$)|(?:(?:std|tokio)::)?fs::|(?:(?:std|tokio)::fs::)?(?:AppHandle|File|OpenOptions|read_dir)(?:::|$))"); +function resolveAlias(scan, input, scope) { + const resolved = scan.resolvePath(input,scope); + if (!resolved.ambiguous) return resolved; + const raw = typeof input === 'string' ? input : input.map(t => t.name ?? t.text).join(''); + const suffix = raw.split('::').slice(1).join('::'); + const candidates = [resolved.path,raw]; + for (let b = resolved.binding; b; b = b.previous) if (b.target) + candidates.push(b.target + (suffix ? '::' + suffix : '')); + return { ...resolved, path: candidates.find(p => p && relevantPath.test(p)) ?? resolved.path }; +} + +// Finite syntactic signals, using the shared lexer, scope and production model. +function qualified(scan, tokens, i) { + let end = i + 1; + while (tokens[end]?.text === '::' && tokens[end + 1]?.kind === 'ident') end += 2; + const start = tokens[i - 1]?.text === '::' && tokens[i - 2]?.kind !== 'ident' ? i - 1 : i; + const resolved = resolveAlias(scan,tokens.slice(start, end),tokens[i].scope); + return { ...resolved, end, raw: tokens.slice(start, end).map(t => t.name ?? t.text).join('') }; +} +function signals(scan, item) { + return item.attributes.flatMap(a => a.signals.map(meta => ({ ...resolveAlias(scan,meta.name,a.scope), meta, line: a.line }))); +} +function metric() { return { counts: {}, sites: {}, commands: new Map(), warnings: [] }; } +function hit(m, rule, line, identity) { + m.counts[rule] = (m.counts[rule] ?? 0) + 1; + (m.sites[rule] ??= []).push({ line, ...(identity ? { identity } : {}) }); +} +function warning(m, path, line, resolved) { + if (resolved.ambiguous) m.warnings.push({ path, line, reason: resolved.reason ?? 'ambiguous relevant alias', + remedy: 'use an explicit unambiguous import; signal counted conservatively' }); +} +export function runGate(snapshot) { + const diagnostics = [], measurements = { base: [], head: [] }; + try { + const trees = [snapshot.baseTree, snapshot.headTree], totals = [{}, {}], byPath = [new Map(), new Map()]; + for (const [side, tree] of trees.entries()) { + for (const path of [...tree.keys()].filter(inUniverse).sort(codeOrder)) { + const m = measure(path, scanRust(path, snapshot.readBlob(tree.get(path)))); + byPath[side].set(path, m); + for (const rule of rules) totals[side][rule] = (totals[side][rule] ?? 0) + (m.counts[rule] ?? 0); + measurements[side ? 'head' : 'base'].push({ path, counts: m.counts, sites: m.sites }); + if (side) diagnostics.push(...m.warnings.map(d => ({ gate, status: 'WARN', ...d }))); + } + } + const violation = (rule, path, line, base, head, reason) => diagnostics.push({ gate, status: 'FAIL', rule, path, line, + base, head, baseOid: snapshot.baseOid, headOid: snapshot.headOid, reason, + remedy: 'remove the new signal or use the source-defined owner; do not raise the baseline' }); + for (const rule of rules) if ((totals[1][rule] ?? 0) > (totals[0][rule] ?? 0)) { + const source = [...byPath[1]].find(([p,m]) => (m.counts[rule] ?? 0) > (byPath[0].get(p)?.counts[rule] ?? 0)); + violation(rule, source?.[0] ?? '', source?.[1].sites[rule]?.[0]?.line ?? 1, + totals[0][rule] ?? 0, totals[1][rule], 'global subrule debt increased'); + } + for (const [path, m] of byPath[1]) { + if (!snapshot.baseTree.has(path)) for (const rule of newFileRules) if (m.counts[rule]) + violation(rule, path, m.sites[rule][0].line, 0, m.counts[rule], 'new file must satisfy this subrule even with compensated totals'); + extraChecks(path, m, byPath[0].get(path), violation); + } + snapshot.assertStableHead(); + const fail = diagnostics.some(d => d.status === 'FAIL'); + return { gate, status: fail ? 'FAIL' : 'PASS', exit: fail ? 1 : 0, requestedRef: snapshot.requestedRef, + refOid: snapshot.refOid, baseOid: snapshot.baseOid, headOid: snapshot.headOid, totals, measurements, diagnostics }; + } catch (e) { + return { gate, status: 'ERROR', exit: e.exit ?? 3, baseOid: snapshot.baseOid, headOid: snapshot.headOid, + diagnostics: [...diagnostics, { gate, reason: e.message, remedy: 'repair inputs and rerun; no verdict available' }] }; + } +} + +const gate = '06'; +const rules = ['tauri_files', 'emit_files', 'state_files', 'command_files', 'disk_commands']; +const newFileRules = ['tauri_files']; +const inUniverse = p => /^src-tauri\/src\/.*\.rs$/.test(p); +function extraChecks(path, m, before, violation) { + for (const [id, line] of m.commands) if (!before?.commandIds.has(id)) + violation('new_disk_command', path, line, 0, 1, `new command with disk access: ${id}`); +} +function measure(path, scan) { + const m = metric(), ts = scan.productionTokens; + const edge = /^src-tauri\/src\/(?:(?:lib|main|commands|events)\.rs$|(?:commands|events)\/)/.test(path); + let tauri, emit, state, command; + for (let i = 0; i < ts.length; i++) { + const t = ts[i]; + if (t.text === '.' && ['emit','emit_to'].includes(ts[i + 1]?.name) && ts[i + 2]?.text === '(') emit ??= t.line; + if (t.text === '.' && ts[i + 1]?.name === 'state' && ts[i + 2]?.text === '::' && ts[i + 3]?.text === '<') state ??= t.line; + if (t.kind !== 'ident' || (ts[i - 1]?.text === '.' || ts[i - 1]?.text === '::' && ts[i - 2]?.kind === 'ident')) continue; + const r = qualified(scan,ts,i), value = r.path ?? (r.ambiguous ? r.raw : ''); + if (value.startsWith('tauri::') || value === 'tauri' || /(?:^|::)AppHandle$/.test(value)) { + tauri ??= t.line; warning(m,path,t.line,r); + } + i = r.end - 1; + } + const ordinals = new Map(); m.commandIds = new Set(); + for (const item of scan.items) { + if (item.kind !== 'fn' || item.testOnly) continue; + const containers = scan.items.filter(c => c !== item && c.start <= item.start && c.end >= item.end) + .map(c => `${c.kind}:${c.name ?? scan.tokens.slice(c.first,c.last).filter(t => t.start < c.bodyStart).map(t => t.text).join('')}`); + const key = [...containers,item.name].join('/'), ordinal = ordinals.get(key) ?? 0; + ordinals.set(key,ordinal + 1); + const attrs = signals(scan,item).filter(a => a.path === 'tauri::command' || a.ambiguous && /(?:^|::)command$/.test(a.path ?? a.meta.name)); + if (!attrs.length) continue; + command ??= item.line; + for (const a of attrs) warning(m,path,a.line,a); + const identity = `${path}:${key}#${ordinal}`; m.commandIds.add(identity); + let disk = false; + for (let i = 0; i < ts.length; i++) { + const t = ts[i]; + if (item.bodyStart === null || t.start <= item.bodyStart || t.start >= item.end || t.kind !== 'ident' || (ts[i - 1]?.text === '.' || ts[i - 1]?.text === '::' && ts[i - 2]?.kind === 'ident')) continue; + const r = qualified(scan,ts,i), value = r.path ?? (r.ambiguous ? r.raw : ''); + if (/^(?:(?:std|tokio)::)?fs::/.test(value) || /^(?:(?:std|tokio)::fs::)?File::(?:open|create)$/.test(value) || + /^(?:(?:std|tokio)::fs::)?OpenOptions(?:::|$)/.test(value) || /^(?:(?:std|tokio)::fs::)?read_dir$/.test(value)) { + disk = true; warning(m,path,t.line,r); + } + i = r.end - 1; + } + if (disk) { const id = identity; m.commands.set(id,item.line); hit(m,'disk_commands',item.line,id); } + } + if (!edge) for (const [rule,line] of [['tauri_files',tauri],['emit_files',emit],['state_files',state],['command_files',command]]) if (line) hit(m,rule,line); + return m; +} + +if (isMain(import.meta.url)) process.exitCode = await gateCli(runGate); diff --git a/scripts/sanitizing_tests.mjs b/scripts/sanitizing_tests.mjs index c59d07f5a..f5716fabf 100644 --- a/scripts/sanitizing_tests.mjs +++ b/scripts/sanitizing_tests.mjs @@ -375,3 +375,243 @@ test('absolute test attributes resolve without shadowing or hiding later product assert.equal(a.items[0].testOnly, false); assert.ok(a.attributes[0].signals.some(s => s.name === '::tokio::test')); assert.throws(() => scanRust('x.rs', '#[::] fn t(){}'), e => e.exit === 3); }); + +// P02 uses the same snapshot contract. Pure tree controls keep every subrule's +// counterfactual explicit; real Git controls below exercise the landed OID API. +import { runGate as layoutGate } from './sanitizing_file_layout.mjs'; +import { runGate as edgeGate } from './sanitizing_tauri_edge.mjs'; +import { runGate as globalsGate } from './sanitizing_global_state.mjs'; +import { runGate as processGate } from './sanitizing_process_env.mjs'; +const rustRoot = 'src-tauri/src/'; +function trees(base, head = base) { + const tree = files => new Map(Object.entries(files).map(([path, text]) => [path,{path,text,mode:'100644',type:'blob'}])); + return { baseTree: tree(base), headTree: tree(head), baseOid: 'base-fixture', headOid: 'head-fixture', + refOid: 'ref-fixture', requestedRef:'fixture', readBlob: e => e?.text ?? '', assertStableHead() {} }; +} +function control(gate, rule, name, before, mutation) { + const p = rustRoot + name, base = {[p]:before}; + assert.equal(gate(trees(base)).exit,0); + const result = gate(trees(base,{[p]:mutation})); + assert.equal(result.exit,1,JSON.stringify(result)); + assert.ok(result.diagnostics.some(d=>d.status==='FAIL' && d.rule===rule && d.line>0 && d.path===p && d.remedy),JSON.stringify(result)); + assert.equal(gate(trees(base,base)).exit,0); + return result; +} +test('05 every independent counter: PASS → FAIL with diagnostic → restored PASS', () => { + for (const [rule,p,clean,bad] of [ + ['mod_rs','mod.rs','', 'fn f() {}'], + ['test_files','x.rs','fn f() {}', '#[test]\nfn f() {}'], + ['inline_mod','x.rs','mod x;', 'mod x {}'], + ['path','x.rs','mod x;', '#[path="other.rs"]\nmod x;'], + ['glob','x.rs','use a::b;', 'use a::{b::*,c::*};'], + ['pub_mod','x.rs','mod x;', 'pub\nmod x;'], + ['pub_super_mod','x.rs','mod x;', 'pub(super)\nmod x;'], + ['pub_in_mod','x.rs','mod x;', 'pub(in crate::a)\nmod x;'], + ]) { + if (rule==='mod_rs') { + const old = rustRoot+'old.rs', added=rustRoot+p; + assert.equal(layoutGate(trees({[old]:clean})).exit,0); + const r=layoutGate(trees({[old]:clean},{[old]:clean,[added]:bad})); + assert.equal(r.exit,1); assert.ok(r.diagnostics.some(d=>d.rule===rule)); + assert.equal(layoutGate(trees({[old]:clean})).exit,0); + } else control(layoutGate,rule,p,clean,bad); + } +}); +test('05 grouped globs, test/path cfg_attr, async aliases, integration and visibility boundaries', () => { + for(const attr of ['#[tokio::test]','#[async_std::test]','use tokio::test as check; #[check]','#[cfg_attr(feature="x",tokio::test)]']) + control(layoutGate,'test_files','x.rs','fn f() {}',attr+' async fn f() {}'); + control(layoutGate,'path','x.rs','mod a;','#[cfg_attr(unix,path="a.rs")] mod a;'); + const m=layoutGate(trees({}, {[rustRoot+'tests.rs']:'use super::*; #[test] fn f() {}'})); + assert.equal(m.exit,0,JSON.stringify(m)); + assert.equal(layoutGate(trees({}, {[rustRoot+'tests/x.rs']:'#[test] fn f() {}'})).exit,0); + assert.equal(layoutGate(trees({}, {'src-tauri/tests/it.rs':'#[test] fn f() {}'})).exit,0); + control(layoutGate,'glob','tests.rs','use super::*;','use super::*; use x::*;'); + assert.equal(layoutGate(trees({}, {[rustRoot+'lib.rs']:'pub mod a; pub(crate) mod b;'})).exit,0); + const r=layoutGate(trees({}, {[rustRoot+'x.rs']:'use a::{b::*,c::*,d::{e::*,f}};'})); + assert.equal(r.totals[1].glob,3); +}); +test('05 debt equal/improved/worse and strict compensated new file, C8 ratchet only', () => { + const p=rustRoot+'old.rs',q=rustRoot+'new.rs',bad='mod a {}'; + assert.equal(layoutGate(trees({[p]:bad},{[p]:''})).exit,0); + control(layoutGate,'inline_mod','old.rs',bad,bad+'mod b {}'); + assert.equal(layoutGate(trees({[p]:bad},{[p]:'',[q]:bad})).exit,1); + // Visibility has a global ratchet, not an invented strict new-file prohibition. + assert.equal(layoutGate(trees({[p]:'pub mod a;'},{[q]:'pub mod a;'})).exit,0); +}); +test('06 every counter and exact boundary, alias imports and homonymous methods', () => { + for(const [rule,bad] of [ + ['tauri_files','fn f(x: tauri::AppHandle) {}'], + ['emit_files','fn f(x: X) { x.emit("event",0); }'], + ['state_files','fn f(x: X) { x.state::(); }'], + ['command_files','#[cfg_attr(unix,tauri::command)] fn f() {}'], + ['disk_commands','#[tauri::command] fn f() { std::fs::read("a"); }'], + ]) control(edgeGate,rule,'x.rs','fn f() {}',bad); + for(const p of ['lib.rs','main.rs','commands.rs','events.rs','commands/a.rs','events/a.rs']) + assert.equal(edgeGate(trees({}, {[rustRoot+p]:'fn f(x: tauri::AppHandle) { x.emit("a",0); x.state::(); }'})).exit,0,p); + for(const p of ['cli/a.rs','api/a.rs','web/a.rs','window.rs']) + control(edgeGate,'tauri_files',p,'','use tauri::AppHandle as Handle; fn f(x: Handle) {}'); + control(edgeGate,'disk_commands','commands.rs','use tauri::command as cmd; #[cmd] fn f() {}', + 'use tauri::command as cmd; use std::fs::read as disk; #[cmd] fn f() { disk("a"); }'); +}); +test('06 new disk command in existing file cannot hide behind reductions; cfg identity and renames', () => { + const p=rustRoot+'commands.rs'; + const old='#[tauri::command] fn old() { fs::read("a"); }'; + for(const bad of ['#[tauri::command] fn fresh() { fs::read("a"); }', + '#[cfg(unix)] #[tauri::command] fn old() { fs::read("a"); } #[cfg(windows)] #[tauri::command] fn old() { fs::read("a"); }']) { + const r=control(edgeGate,'new_disk_command','commands.rs',old,bad); + assert.ok(r.diagnostics.some(d=>d.rule==='new_disk_command')); + } + assert.equal(edgeGate(trees({[p]:old},{[p]:old.replace('fs::read("a");','')})).exit,0); + assert.equal(edgeGate(trees({}, {[p]:'#[tauri::command] fn f() { #[cfg(test)] fn test_only() { fs::read("a"); } }'})).exit,0); +}); +test('08 static mutation, state wrappers/aliases/multiline and thread_local each reject increase', () => { + for(const bad of [ + 'static mut X: usize = 0;', 'static X: std::sync::Mutex = Mutex::new(0);', + 'static X: Arc> = init();','static X: AtomicUsize = init();', + 'static X: OnceCell = init();','static X: Lazy = init();', + 'use std::sync::Mutex as M; static\nX: M = M::new(0);', + 'thread_local! { static X: Cell = Cell::new(0); }', + 'thread_local! { static X: RefCell = RefCell::new(0); }', + ]) control(globalsGate,'globals','x.rs','',bad); + const p=rustRoot+'old.rs',q=rustRoot+'new.rs',bad='static X: Mutex = init();'; + assert.equal(globalsGate(trees({[p]:bad},{[p]:''})).exit,0); + control(globalsGate,'globals','old.rs',bad,bad+'static Y: OnceLock = init();'); + assert.equal(globalsGate(trees({[p]:bad},{[q]:bad})).exit,1); + assert.equal(globalsGate(trees({}, {[p]:'static N: usize = 0; static S: &str = "x";'})).exit,0); +}); +const regexImports='use std::sync::{LazyLock,OnceLock}; use regex::Regex; '; +const lazyRegex='static RE: LazyLock = LazyLock::new(|| { Regex::new("a").unwrap() });'; +const onceRegex='fn f() { static RE: OnceLock = OnceLock::new(); RE.get_or_init(|| Regex::new("a").expect("valid")); }'; +test('08 only demonstrated literal Regex initialization/usage is exempt', () => { + for(const source of [lazyRegex,onceRegex,onceRegex.replace('RE.get_or_init','RE.get_or_init'), + lazyRegex.replace('unwrap()','expect("valid")'),lazyRegex.replace('|| {','|| {{{').replace(' });','}} });')]) { + const r=globalsGate(trees({}, {[rustRoot+'x.rs']:regexImports+source})); + assert.equal(r.exit,0,JSON.stringify(r)); + } + const variants=[ + lazyRegex.replace('"a"','runtime()'),lazyRegex.replace('Regex>','Mutex>'), + lazyRegex.replace('Regex::new','let x=0; Regex::new'),lazyRegex.replace('||','|x|'), + onceRegex.replace(' RE.get_or_init(|| Regex::new("a").expect("valid"));',''), + onceRegex.replace('get_or_init','get_or_try_init'),onceRegex.replace('RE.get_or_init','RE.set'), + onceRegex.replace('RE.get_or_init','take(&RE); RE.get_or_init'), + onceRegex.replace('Regex::new("a")','Regex::new(runtime())'), + 'static RE: OnceLock = OnceLock::new();', + 'static RULES: OnceLock = OnceLock::new();', + lazyRegex.replace('LazyLock','OnceCell'), + ]; + for(const bad of variants) control(globalsGate,'globals','x.rs',regexImports+lazyRegex,regexImports+bad); + const alias='use std::sync::LazyLock as L; use regex::Regex as R; static RE: L = L::new(|| R::new(r"a").unwrap());'; + assert.equal(globalsGate(trees({}, {[rustRoot+'x.rs']:alias})).exit,0); +}); +test('09 independent counters, absolute write, exact ownership and aliases/shadow', () => { + control(processGate,'launch_files','x.rs','fn f() {}','use tokio::process::Command as C; fn f() { C::new("x"); }'); + control(processGate,'env_read_files','x.rs','fn f() {}','use std::env::var as read; fn f() { read("x"); }'); + control(processGate,'env_write','x.rs','fn f() {}','use std::env as e; fn f() { e::set_var("x","y"); }'); + for(const p of ['process.rs','process/a.rs','pty/mod.rs','pty/a.rs']) { + assert.equal(processGate(trees({}, {[rustRoot+p]:'fn f() { Command::new("x").env("x","y"); }'})).exit,0); + control(processGate,'env_read_files',p,'','fn f() { std::env::var_os("x"); }'); + control(processGate,'env_write',p,'','fn f() { std::env::remove_var("x"); }'); + } + for(const p of ['config.rs','config/a.rs','lib.rs','main.rs']) { + assert.equal(processGate(trees({}, {[rustRoot+p]:'fn f() { std::env::var("x"); }'})).exit,0); + control(processGate,'launch_files',p,'','fn f() { std::process::Command::new("x"); }'); + } + assert.equal(processGate(trees({}, {[rustRoot+'main.rs']:'fn f() { std::env::set_var("x","y"); }'})).exit,0); + const bad={[rustRoot+'pty/a.rs']:'fn f() { env::set_var("x","y"); }'}; + assert.equal(processGate(trees(bad)).exit,1); + const shadow='use std::env::var as read; fn f(read: F) { read("x"); }'; + assert.equal(processGate(trees({}, {[rustRoot+'x.rs']:shadow})).exit,0); +}); +test('P02 production predicates preserve following code; comments/strings and tests do not signal', () => { + for(const [gate,bad,rule] of [[edgeGate,'tauri::AppHandle::new();','tauri_files'], + [globalsGate,'static X: Mutex = init();','globals'],[processGate,'env::var("x");','env_read_files']]) { + for(const cfg of ['test','all(test,unix)','any(test,test)']) { + const s=`#[cfg(${cfg})] fn test_only() { ${bad} }`; + assert.equal(gate(trees({}, {[rustRoot+'x.rs']:s})).exit,0); + control(gate,rule,'x.rs',s,s+` fn production() { ${bad} }`); + } + for(const cfg of ['any(has_embedded_dist,test)','not(test)','all()','any()']) + control(gate,rule,'x.rs','',`#[cfg(${cfg})] fn production() { ${bad} }`); + control(gate,rule,'x.rs','',`#[cfg_attr(unix,cfg(test))] fn production() { ${bad} }`); + for(const attr of ['#[test]','#[tokio::test]','#[async_std::test]','use tokio::test as check; #[check]']) { + const s=`${attr} async fn test_fn() { ${bad} }`; + assert.equal(gate(trees({}, {[rustRoot+'x.rs']:s})).exit,0); + control(gate,rule,'x.rs',s,s+` fn production() { ${bad} }`); + } + const inert='// '+bad+'\nfn f() { let x = r###"'+bad+'"###; }'; + assert.equal(gate(trees({}, {[rustRoot+'x.rs']:inert})).exit,0); + for(const p of ['tests.rs','x_tests.rs','tests/a.rs']) assert.equal(gate(trees({}, {[rustRoot+p]:`fn f() { ${bad} }`})).exit,0); + } +}); +test('09 compensated new launch/env read files fail; unchanged/down debt passes independently', () => { + for(const [rule,code] of [['launch_files','Command::new("x");'],['env_read_files','env::var("x");']]) { + const p=rustRoot+'old.rs',q=rustRoot+'new.rs',before=`fn f() { ${code} }`; + assert.equal(processGate(trees({[p]:before},{[p]:''})).exit,0); + const r=processGate(trees({[p]:before},{[q]:before})); + assert.equal(r.exit,1);assert.ok(r.diagnostics.some(d=>d.rule===rule)); + const up=processGate(trees({[p]:before},{[p]:before,[q]:before})); + assert.equal(up.exit,1); assert.ok(up.diagnostics.some(d=>d.rule===rule)); + } +}); +test('P02 actual Git OID mutation/reversion and ERROR precedence for every detector', t => { + const f=fixture(t); + for(const [gate,p,code] of [[layoutGate,'layout.rs','mod inline {}'],[edgeGate,'edge.rs','fn f(x: tauri::AppHandle) {}'], + [globalsGate,'global.rs','static X: Mutex = init();'],[processGate,'env.rs','fn f() { env::var("x"); }']]) { + sequence(f,gate,rustRoot+p,'',code); + const s=f.snapshot(); s.assertStableHead=()=>{throw Error('HEAD drift');}; + assert.equal(gate(s).exit,3); + const malformed=trees({}, {[rustRoot+p]:'#[cfg(test,unix)] fn f() {}'}); + assert.equal(gate(malformed).exit,3); + const broken=trees({}, {[rustRoot+p]:''});broken.readBlob=()=>{throw Error('invalid UTF8/mode/blob');}; + assert.equal(gate(broken).exit,3); + } +}); + +test('P02 relevant ambiguous aliases count with diagnostics instead of erasing the previous import', () => { + for(const [gate,rule,source] of [ + [processGate,'env_read_files','use std::env::var as read; use other::read; fn f() { read("x"); }'], + [edgeGate,'tauri_files','use tauri::AppHandle as Handle; use other::Handle; fn f(x: Handle) {}'], + [globalsGate,'globals','use std::sync::Mutex as M; use other::M; static X: M = init();'], + [globalsGate,'globals','use std::sync::atomic::AtomicUsize as A; use other::A; static X: A = init();'], + [globalsGate,'globals','#[cfg(unix)] use std::sync::atomic::AtomicU64 as Ctr; #[cfg(not(unix))] use other::Ctr; static X: Ctr = Ctr::new(0);'], + [layoutGate,'test_files','use tokio::test as check; use other::check; #[check] fn f() {}'], + ]) { + const r=control(gate,rule,'x.rs','',source); + assert.ok(r.diagnostics.some(d=>d.status==='WARN'),JSON.stringify(r)); + } +}); + +test('P02 absolute paths retain syntactic signals', () => { + for(const [gate,rule,code] of [[edgeGate,'tauri_files','fn f(x: ::tauri::AppHandle) {}'], + [globalsGate,'globals','static X: ::std::sync::Mutex = init();'], + [processGate,'env_read_files','fn f() { ::std::env::var("x"); }'], + [processGate,'launch_files','fn f() { ::tokio::process::Command::new("x"); }']]) control(gate,rule,'x.rs','',code); +}); +test('06 existing command may gain disk when another loses it; only NEW command has strict condition', () => { + const p=rustRoot+'commands.rs'; + const base='#[tauri::command] fn a() { fs::read("a"); } #[tauri::command] fn b() {}'; + const head='#[tauri::command] fn a() {} #[tauri::command] fn b() { fs::read("a"); }'; + const r=edgeGate(trees({[p]:base},{[p]:head}));assert.equal(r.exit,0,JSON.stringify(r)); +}); + +test('08 absolute readonly Regex aliases and conservative lock escape/alias/init cases', () => { + const absolute='static RE: ::std::sync::LazyLock<::regex::Regex> = ::std::sync::LazyLock::new(|| ::regex::Regex::new("a").unwrap());'; + assert.equal(globalsGate(trees({}, {[rustRoot+'x.rs']:absolute})).exit,0); + for(const bad of [onceRegex.replace('RE.get_or_init','let alias = &RE; alias.get_or_init'), + onceRegex.replace('OnceLock::new()','runtime()'),onceRegex.replace('static RE:','pub static RE:'), + onceRegex.replace('RE.get_or_init','let alias = RE; alias.get_or_init')]) + control(globalsGate,'globals','x.rs',regexImports+onceRegex,regexImports+bad); +}); +test('P02 explicit aliases obey local shadowing and grouped imports', () => { + const cases=[ + [processGate,'use std::{env::{self as e,var as read},process::Command as C}; fn f(){ e::var("x"); read("x"); C::new("x"); }'], + [edgeGate,'use tauri::{self as t,AppHandle as H}; fn f(x: H){ t::f(); }'], + [globalsGate,'use std::sync::{Mutex as M,RwLock as R}; static X: Option>> = init();'], + ]; + for(const [gate,source] of cases) assert.equal(gate(trees({}, {[rustRoot+'x.rs']:source})).exit,1); + for(const [gate,source] of [ + [processGate,'use std::process::Command; fn f(){ struct Command; Command::new("x"); }'], + ]) assert.equal(gate(trees({}, {[rustRoot+'x.rs']:source})).exit,0); + assert.equal(edgeGate(trees({}, {[rustRoot+'commands.rs']:'use std::fs::read; #[tauri::command] fn f(read: F) { read("x"); }'})).exit,0); + assert.equal(layoutGate(trees({}, {[rustRoot+'x.rs']:'// mod a {} #[test]\nfn f(){ let x="use a::*;"; }'})).exit,0); +});