fix(scan): canonicalize traversal without order-sensitive Rust binding

This commit is contained in:
Eva 2026-07-14 12:15:54 +07:00
parent 1482c0bc89
commit f0f316a7b2
3 changed files with 51 additions and 2 deletions

View file

@ -83,6 +83,11 @@ export const walkRepositoryPaths = async (
}
}
// Filesystem/glob traversal order is not stable across filesystems or repeated
// scans. Canonicalize once at the scan boundary so every downstream phase sees
// the same repository order.
entries.sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
if (skippedLarge > 0) {
const isDefault = maxFileSizeBytes === DEFAULT_MAX_FILE_SIZE_BYTES;
const isOverrideUnset = !process.env.GITNEXUS_MAX_FILE_SIZE;

View file

@ -89,6 +89,13 @@ export function populateRustRangeBindings(
}
}
}
// Publish per-type member bindings for the whole workspace before resolving
// assignments. Otherwise an importer processed before its defining file can
// miss a field or identity-method type solely because of file order.
const scopeMap = new Map(parsed.scopes.map((scope) => [scope.id, scope]));
processFieldTypeBindings(tree.rootNode, parsed, scopeMap);
processIdentityMethodBindings(parsed);
}
for (const parsed of parsedFiles) {
@ -122,8 +129,6 @@ export function populateRustRangeBindings(
const moduleScope = parsed.scopes.find((s) => s.kind === 'Module');
if (moduleScope === undefined) continue;
processFieldTypeBindings(tree.rootNode, parsed, scopeMap);
processIdentityMethodBindings(parsed);
processForLoops(tree.rootNode, parsed, scopeMap, moduleScope, allReturnTypes);
processPatternBindings(tree.rootNode, parsed, scopeMap, moduleScope);
processStructDestructuring(tree.rootNode, parsed, scopeMap, moduleScope, allFieldTypes);

View file

@ -0,0 +1,39 @@
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { glob } from 'glob';
import { afterEach, describe, expect, it, vi } from 'vitest';
vi.mock('glob', () => ({ glob: vi.fn() }));
vi.mock('../../src/config/ignore-service.js', () => ({
createIgnoreFilter: vi.fn(async () => []),
}));
import { walkRepositoryPaths } from '../../src/core/ingestion/filesystem-walker.js';
const temporaryRoots: string[] = [];
afterEach(async () => {
vi.mocked(glob).mockReset();
await Promise.all(
temporaryRoots.splice(0).map((root) => fs.rm(root, { recursive: true, force: true })),
);
});
describe('walkRepositoryPaths ordering', () => {
it('returns accepted files in canonical path order when glob order is unstable', async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-scan-order-'));
temporaryRoots.push(root);
await Promise.all(
['zeta.ts', 'alpha.ts', 'middle.ts'].map((file) =>
fs.writeFile(path.join(root, file), `export const ${file[0]} = true;\n`),
),
);
vi.mocked(glob).mockResolvedValue(['zeta.ts', 'alpha.ts', 'middle.ts']);
const result = await walkRepositoryPaths(root);
expect(result.map((entry) => entry.path)).toEqual(['alpha.ts', 'middle.ts', 'zeta.ts']);
});
});