mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-17 23:52:36 +00:00
test(scope-resolution): rewrite workspace-index test for slimmed index
The test file previously asserted on \`defsByFileAndName\`,
\`callablesBySimpleName\`, and \`memberByOwner\` — fields removed when
symbol-keyed lookups moved to \`SemanticModel\`. Rewrite so the same
invariants are asserted via the authoritative consumers:
* New WorkspaceResolutionIndex shape test (scope-only maps).
* \`findExportedDef\` module-export visibility tests:
- keeps top-level class and function defs.
- excludes class-body Variable defs (MAX_USERS = 100).
- excludes class methods from module-export lookup.
* \`findExportedDefByName\` fallback excludes class methods when a
same-named module function exists.
* \`findOwnedMember\` via the reconciled SemanticModel finds Python
class methods after populateOwners + reconcileOwnership.
Total assertions preserved: every invariant from the old test file is
still pinned; the assertion surface shifted from the index shape to
the walker helpers.
Verified:
- workspace-index.test.ts 8/8 passing
This commit is contained in:
parent
ba94fdca91
commit
4aaf0db598
1 changed files with 116 additions and 57 deletions
|
|
@ -1,23 +1,32 @@
|
|||
/**
|
||||
* Directly assert the shape of `WorkspaceResolutionIndex` — in
|
||||
* particular that `defsByFileAndName` and `callablesBySimpleName`
|
||||
* filter class-body attributes and nested-function locals out of the
|
||||
* file-level export keyspace.
|
||||
* Pin the invariants the workspace-index layer MUST preserve after the
|
||||
* symbol-indexed duplicates moved to `SemanticModel`.
|
||||
*
|
||||
* The equivalent integration-level assertions in
|
||||
* `test/integration/resolvers/python.test.ts` (see the
|
||||
* `python-class-attr-export-leak` fixture) cover the downstream
|
||||
* edge-emission path. This unit test pins the index shape directly
|
||||
* because the downstream consumer in Python today doesn't emit an
|
||||
* ACCESSES edge for `mod.NAME` member access, so the leak would be
|
||||
* latent at the index layer until a future capture path makes it
|
||||
* visible. That's precisely when a unit-level pin is most valuable.
|
||||
* Previously this file asserted on `defsByFileAndName`,
|
||||
* `callablesBySimpleName`, and `memberByOwner` directly. Those fields
|
||||
* were removed — symbol-keyed lookups now consult `SemanticModel` and
|
||||
* `WorkspaceResolutionIndex` holds only `classScopeByDefId` +
|
||||
* `moduleScopeByFile`. The same invariants are now asserted via the
|
||||
* walker helpers (`findExportedDef`, `findExportedDefByName`,
|
||||
* `findOwnedMember`) which are the authoritative consumers. This
|
||||
* keeps the regression guard (class-body attributes / methods must
|
||||
* not leak into module-export lookups, and method membership must
|
||||
* stay reachable after `populateOwners`) without asserting on the
|
||||
* now-deleted index shape.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { extractParsedFile } from '../../../src/core/ingestion/scope-extractor-bridge.js';
|
||||
import { pythonScopeResolver } from '../../../src/core/ingestion/languages/python/scope-resolver.js';
|
||||
import { buildWorkspaceResolutionIndex } from '../../../src/core/ingestion/scope-resolution/workspace-index.js';
|
||||
import {
|
||||
findExportedDef,
|
||||
findExportedDefByName,
|
||||
findOwnedMember,
|
||||
} from '../../../src/core/ingestion/scope-resolution/scope/walkers.js';
|
||||
import { createSemanticModel } from '../../../src/core/ingestion/model/semantic-model.js';
|
||||
import { reconcileOwnership } from '../../../src/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js';
|
||||
import { finalizeScopeModel } from '../../../src/core/ingestion/finalize-orchestrator.js';
|
||||
|
||||
function parsePython(source: string, filePath: string) {
|
||||
const parsed = extractParsedFile(
|
||||
|
|
@ -30,7 +39,54 @@ function parsePython(source: string, filePath: string) {
|
|||
return parsed;
|
||||
}
|
||||
|
||||
describe('buildWorkspaceResolutionIndex — module-export filter', () => {
|
||||
describe('WorkspaceResolutionIndex — scope-only maps', () => {
|
||||
it('exposes classScopeByDefId and moduleScopeByFile only', () => {
|
||||
const parsed = parsePython(
|
||||
`
|
||||
class User:
|
||||
pass
|
||||
`,
|
||||
'mod.py',
|
||||
);
|
||||
const index = buildWorkspaceResolutionIndex([parsed]);
|
||||
expect(index.classScopeByDefId).toBeInstanceOf(Map);
|
||||
expect(index.moduleScopeByFile).toBeInstanceOf(Map);
|
||||
// No symbol-indexed duplicates.
|
||||
expect((index as { memberByOwner?: unknown }).memberByOwner).toBeUndefined();
|
||||
expect((index as { defsByFileAndName?: unknown }).defsByFileAndName).toBeUndefined();
|
||||
expect((index as { callablesBySimpleName?: unknown }).callablesBySimpleName).toBeUndefined();
|
||||
});
|
||||
|
||||
it('classScopeByDefId maps class nodeIds to their Scope', () => {
|
||||
const parsed = parsePython(
|
||||
`
|
||||
class User:
|
||||
pass
|
||||
`,
|
||||
'mod.py',
|
||||
);
|
||||
const index = buildWorkspaceResolutionIndex([parsed]);
|
||||
const classScope = parsed.scopes.find((s) => s.kind === 'Class');
|
||||
const classDef = classScope?.ownedDefs.find((d) => d.type === 'Class');
|
||||
expect(classDef).toBeDefined();
|
||||
expect(index.classScopeByDefId.get(classDef!.nodeId)).toBe(classScope);
|
||||
});
|
||||
|
||||
it('moduleScopeByFile maps filePath to Module scope', () => {
|
||||
const parsed = parsePython(
|
||||
`
|
||||
def helper() -> int:
|
||||
return 42
|
||||
`,
|
||||
'mod.py',
|
||||
);
|
||||
const index = buildWorkspaceResolutionIndex([parsed]);
|
||||
const moduleScope = parsed.scopes.find((s) => s.kind === 'Module');
|
||||
expect(index.moduleScopeByFile.get('mod.py')).toBe(moduleScope);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findExportedDef — module-export visibility filter', () => {
|
||||
it('keeps top-level class and function defs', () => {
|
||||
const parsed = parsePython(
|
||||
`
|
||||
|
|
@ -43,20 +99,20 @@ def helper() -> int:
|
|||
`,
|
||||
'mod.py',
|
||||
);
|
||||
pythonScopeResolver.populateOwners(parsed);
|
||||
finalizeScopeModel([parsed]);
|
||||
const index = buildWorkspaceResolutionIndex([parsed]);
|
||||
const fileBucket = index.defsByFileAndName.get('mod.py');
|
||||
expect(fileBucket).toBeDefined();
|
||||
expect(fileBucket!.get('User')?.type).toBe('Class');
|
||||
expect(fileBucket!.get('helper')?.type).toBe('Function');
|
||||
|
||||
expect(findExportedDef('mod.py', 'User', index)?.type).toBe('Class');
|
||||
expect(findExportedDef('mod.py', 'helper', index)?.type).toBe('Function');
|
||||
});
|
||||
|
||||
it('excludes class-body Variable defs from defsByFileAndName', () => {
|
||||
// Python's scope extractor captures `MAX_USERS = 100` inside a
|
||||
// class body as `Variable:MAX_USERS` in the Class scope's
|
||||
// ownedDefs. Without the scope-defining-def filter, this entry
|
||||
// would leak into defsByFileAndName['mod.py']['MAX_USERS'] and
|
||||
// `mod.MAX_USERS` / `from mod import MAX_USERS` would silently
|
||||
// resolve to the class attribute.
|
||||
it('excludes class-body Variable defs from module-export lookup', () => {
|
||||
// Python `MAX_USERS = 100` inside a class body is captured as
|
||||
// `Variable:MAX_USERS` in the Class scope's ownedDefs. It must
|
||||
// NOT be visible via the file-level export lookup — otherwise
|
||||
// `from mod import MAX_USERS` would silently resolve to the
|
||||
// class attribute.
|
||||
const parsed = parsePython(
|
||||
`
|
||||
class User:
|
||||
|
|
@ -64,19 +120,16 @@ class User:
|
|||
`,
|
||||
'mod.py',
|
||||
);
|
||||
pythonScopeResolver.populateOwners(parsed);
|
||||
finalizeScopeModel([parsed]);
|
||||
const index = buildWorkspaceResolutionIndex([parsed]);
|
||||
const fileBucket = index.defsByFileAndName.get('mod.py');
|
||||
expect(fileBucket).toBeDefined();
|
||||
expect(fileBucket!.get('MAX_USERS')).toBeUndefined();
|
||||
|
||||
expect(findExportedDef('mod.py', 'MAX_USERS', index)).toBeUndefined();
|
||||
// Positive-case invariant: the Class def itself is still exported.
|
||||
expect(fileBucket!.get('User')?.type).toBe('Class');
|
||||
expect(findExportedDef('mod.py', 'User', index)?.type).toBe('Class');
|
||||
});
|
||||
|
||||
it('excludes class methods from defsByFileAndName', () => {
|
||||
// A method lives in a Function scope whose parent is the Class
|
||||
// scope (not the Module), so it shouldn't be reachable through
|
||||
// the direct-child filter at all. Guard against a regression to
|
||||
// the earlier "method wins module-export slot" bug.
|
||||
it('excludes class methods from module-export lookup', () => {
|
||||
const parsed = parsePython(
|
||||
`
|
||||
class User:
|
||||
|
|
@ -85,15 +138,18 @@ class User:
|
|||
`,
|
||||
'mod.py',
|
||||
);
|
||||
pythonScopeResolver.populateOwners(parsed);
|
||||
finalizeScopeModel([parsed]);
|
||||
const index = buildWorkspaceResolutionIndex([parsed]);
|
||||
const fileBucket = index.defsByFileAndName.get('mod.py');
|
||||
expect(fileBucket).toBeDefined();
|
||||
// `save` is a method — NOT a module export.
|
||||
expect(fileBucket!.get('save')).toBeUndefined();
|
||||
expect(fileBucket!.get('User')?.type).toBe('Class');
|
||||
});
|
||||
|
||||
it('excludes class methods from callablesBySimpleName', () => {
|
||||
// `save` is a method — NOT a module export.
|
||||
expect(findExportedDef('mod.py', 'save', index)).toBeUndefined();
|
||||
expect(findExportedDef('mod.py', 'User', index)?.type).toBe('Class');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findExportedDefByName — workspace-wide callable fallback', () => {
|
||||
it('excludes class methods when same-named module function exists', () => {
|
||||
const parsed = parsePython(
|
||||
`
|
||||
class User:
|
||||
|
|
@ -105,19 +161,21 @@ def save(x: int) -> int:
|
|||
`,
|
||||
'mod.py',
|
||||
);
|
||||
pythonScopeResolver.populateOwners(parsed);
|
||||
const finalized = finalizeScopeModel([parsed]);
|
||||
const index = buildWorkspaceResolutionIndex([parsed]);
|
||||
const saves = index.callablesBySimpleName.get('save') ?? [];
|
||||
// Only the top-level `def save(x)` is a module-level callable.
|
||||
// The `User.save` method lives under a Class scope and must not
|
||||
// appear in the workspace callable fallback.
|
||||
expect(saves).toHaveLength(1);
|
||||
expect(saves[0].qualifiedName).toBe('save');
|
||||
});
|
||||
|
||||
it('keeps memberByOwner populated for class methods (unchanged contract)', () => {
|
||||
// Regression guard: the narrowing of defsByFileAndName must NOT
|
||||
// collaterally drop class-method entries from memberByOwner.
|
||||
// findOwnedMember relies on this for receiver-bound dispatch.
|
||||
// Workspace-wide fallback: iterates moduleScopeByFile and returns
|
||||
// the first locally-declared callable binding. The method
|
||||
// `User.save` lives under a Class scope and must not win.
|
||||
const moduleScope = parsed.scopes.find((s) => s.kind === 'Module')!;
|
||||
const result = findExportedDefByName('save', moduleScope.id, finalized, index);
|
||||
expect(result?.qualifiedName).toBe('save');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOwnedMember — SemanticModel-backed owner lookup', () => {
|
||||
it('resolves a class method via the reconciled model', () => {
|
||||
const parsed = parsePython(
|
||||
`
|
||||
class User:
|
||||
|
|
@ -127,14 +185,15 @@ class User:
|
|||
'mod.py',
|
||||
);
|
||||
pythonScopeResolver.populateOwners(parsed);
|
||||
const index = buildWorkspaceResolutionIndex([parsed]);
|
||||
// User's nodeId is derivable from its ownedDefs — find the Class
|
||||
// scope's Class def and look up 'save' under its nodeId.
|
||||
const model = createSemanticModel();
|
||||
reconcileOwnership([parsed], model);
|
||||
|
||||
const classScope = parsed.scopes.find((s) => s.kind === 'Class');
|
||||
const classDef = classScope?.ownedDefs.find((d) => d.type === 'Class');
|
||||
expect(classDef).toBeDefined();
|
||||
const members = index.memberByOwner.get(classDef!.nodeId);
|
||||
expect(members).toBeDefined();
|
||||
expect(members!.get('save')?.type).toBe('Function');
|
||||
|
||||
const found = findOwnedMember(classDef!.nodeId, 'save', model);
|
||||
expect(found?.type).toBe('Function');
|
||||
expect(found?.qualifiedName).toBe('User.save');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue