mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-05 07:37:30 +00:00
Some checks are pending
Gitleaks / gitleaks (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
* fix(js): withdraw receiver drops for object-literal key writes a later pass answers
The JS capture makes the enclosing object literal the receiver of each key
it writes (`return { wickRatio: row.a }`). Case 0 of the receiver-bound pass
treats any receiver text holding `.` or `(` as a compound expression, and a
literal holding `row.a`, `0.5` or `...seed` does, so the fold failed and the
site was recorded as a `receiver-unresolved` drop. The return-shape and
unique-name passes run later and answered 17 of these 19 writes with an
ACCESSES edge, but nothing withdrew the earlier drop.
Both passes now report the sites they emitted an edge for, keyed by file,
position, site kind and member name (position alone is shared by `a.b` and
`a.b.c()`), and run.ts withdraws the matching `receiver-unresolved` drops
once they have run. A site they leave unanswered keeps its drop.
The other two writes, in `SignalService.make()`, had no edge at all: the
own-return-shape tier read the caller's owner qualifier as
`SignalService.make#0`, so it never matched the method's keys
(`SignalService.make.secretFlag`) and three same-named candidates were
refused. The qualifier is now the qualified name after `<Label>:<file>:`
without the `#<arity>` suffix. It also stops cutting a positioned id
(`outer.parse@25:2`) down to its column.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(js): type a static field from its initializer for class-level receivers
`Config.DEFAULT.validate()` with `static DEFAULT = new Config()` produced no
CALLS edge. JS and TS keep a static field's type binding out of the class
scope on purpose: the scope has one binding map for static and instance
members, so the static binding would retype an instance field of the same
name. The class-level receiver route (`declaredTypeOfClassLevelField`) reads
the static Property node instead, but only its annotation-derived
`declaredType`, and an unannotated field has none. The type had nowhere to
live.
The field extractor now records `initializerType` for a field that declares
no type and is initialized by `new X()` / `new ns.X()` (the same two shapes
the scope queries bind), the parse worker stamps it on the Property node, and
the class-level route falls back to it after `declaredType`. The binding still
never enters the shared class-scope map, so the instance-twin hazard does not
return.
This resolves one hop: `Host.q.m()` now reaches `Alien.m`. A longer chain off
the field still goes through the compound fold, which types a field step only
from the class-scope map, so it stays unresolved. The matrix known-gap row
`static-read-without-a-twin-loses-its-type` flips to a positive row for TS,
with a JS twin added.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(js): keep CommonJS module.exports declarations out of receiver drops
`module.exports.arrowRequest = (url) => fetch(url)` declares this module's
`arrowRequest` export; the CJS capture rules already bind it like
`exports.memberRequest = …`. The same line also yields a write reference
whose receiver is `module.exports`, and because that text has a dot the
receiver-bound pass folded it as a compound value, failed, and recorded the
declaration as a `receiver-unresolved` write. The bare `exports.x = …`
spelling never reached that path.
A new ScopeResolver hook, `isModuleExportAssignment`, lets a language say a
write site is a module-export declaration. The receiver-bound pass consults
it only at the drop guard, after every case has failed, so it records no
drop and changes nothing else: the site is not marked handled, no edge is
added, and the existing module-level write edge stays. The JS and TS
resolvers implement it for a receiver of exactly `module.exports` (from the
minted chain, or the text when no chain was minted) when `module` resolves
to nothing in scope. A `module` parameter, as in a UMD wrapper, or a write
into a value hanging off the export object still records its drop.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ts): record an owned-`this` chain receiver as owned-but-unbound
`this.route.go()` inside an object-literal method was filed as a generic
`receiver-unresolved` drop (or, depending on site order, as nothing). The
method scope owns `this` and gives it no type, which the pass detects with
`isReceiverOwnedButUnbound`, but that check was handed the whole receiver
text `this.route`. No scope owns a chain, so the call fell through to Case 0,
whose fold cannot type an owned `this` either.
The check now receives the receiver's base (from the minted chain, else the
text before the first `.`/`(`), the same base the `suppressReceiverLookup`
check already used. The call is recorded as `receiver-owned-but-unbound`,
its real reason, and still emits no edge: the dynamic-this negative control
holds. On the fixture corpus this moves only that site; no edge changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(resolution): preserve receiver ownership
Bind a return key's declaration write by exact file and position so
unrelated input accesses keep their ordinary candidate ranking. Stop at
untyped static overrides and resolve constructor initializers as values.
Preserve the combined drop guards after rebasing onto main, cover the
binding regressions, and refresh the receiver baseline after #3654.
Key answered sites by their full range to retain repeated-name outer
reads. Keep owned-but-unbound calls in the persisted completeness signal.
* fix(resolution): address feedback (#3653)
Classify module-export declarations using captured module kind before
suppressing receiver drops. Match same-named field metadata by source
position, including declarations on one line.
Require reference kinds in suppression assertions and guard temporary
fixture cleanup. Cover the reported failures with regression tests.
* perf(parser): index repeated field declarations
Sort private field buckets once and use binary search to avoid
quadratic scans while preserving declaration disambiguation.
* fix(ingestion): preserve capture metadata
Record module kind from the parsed tree on worker and main-thread paths.
Match Ruby field metadata at the accessor call instead of its argument.
* fix(ingestion): harden capture metadata
Keep Ruby reader/writer properties writable. Walk module-kind ASTs iteratively so deep expressions cannot abort extraction.
* test(ingestion): clarify worker metadata scope
* docs(ingestion): state capture metadata effects
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
976 lines
37 KiB
TypeScript
976 lines
37 KiB
TypeScript
/**
|
|
* JavaScript: self/this resolution, parent resolution, super resolution
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import path from 'path';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import { summarizeUnresolvedReceivers } from '../../../src/core/ingestion/scope-resolution/unresolved-receivers.js';
|
|
import {
|
|
FIXTURES,
|
|
CROSS_FILE_FIXTURES,
|
|
getRelationships,
|
|
getNodesByLabel,
|
|
getNodesByLabelFull,
|
|
edgeSet,
|
|
runPipelineFromRepo,
|
|
suppressionReasonsAt,
|
|
writeFixtureRepo,
|
|
type OutcomeSite,
|
|
type PipelineResult,
|
|
} from './helpers.js';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Qualified (namespaced) base (#1951): `extends ns.Base` parses as a
|
|
// class_heritage holding a member_expression (object: `ns`, property: `Base`).
|
|
// An earlier synth (synthesizeJsInheritanceReferences) dropped member_expression
|
|
// bases, emitting only for a direct identifier base, so production silently
|
|
// omitted this EXTENDS edge. It is now resolved by the base's trailing
|
|
// property_identifier (`Base`), matching the documented per-shape reduction.
|
|
// `Plain extends Base` is the bare control (its simple-base handling is
|
|
// unchanged). Scope-resolution owns these edges since #942.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('JavaScript qualified-base heritage resolution (#1951)', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(path.join(FIXTURES, 'javascript-qualified-base'), () => {});
|
|
}, 60000);
|
|
|
|
it('emits EXTENDS for the qualified base (ns.Base) and the bare control (Base)', () => {
|
|
const extends_ = getRelationships(result, 'EXTENDS');
|
|
expect(edgeSet(extends_)).toEqual(['Plain → Base', 'Service → Base']);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// skipGraphPhases: verify pipeline works correctly when graph phases are skipped
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Pipeline skipGraphPhases option', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'javascript-self-this-resolution'),
|
|
() => {},
|
|
{ skipGraphPhases: true },
|
|
);
|
|
}, 60000);
|
|
|
|
it('produces graph nodes without community/process phases', () => {
|
|
expect(getNodesByLabel(result, 'Class').length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('still resolves CALLS edges correctly', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
expect(calls.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('omits communityResult when skipGraphPhases is true', () => {
|
|
expect(result.communityResult).toBeUndefined();
|
|
});
|
|
|
|
it('omits processResult when skipGraphPhases is true', () => {
|
|
expect(result.processResult).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// this.save() resolves to enclosing class's own save method
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('JavaScript this resolution', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'javascript-self-this-resolution'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects User and Repo classes, each with a save method', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toEqual(['Repo', 'User']);
|
|
const saveMethods = getNodesByLabel(result, 'Method').filter((m) => m === 'save');
|
|
expect(saveMethods.length).toBe(2);
|
|
});
|
|
|
|
it('resolves this.save() inside User.process to User.save, not Repo.save', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveCall = calls.find((c) => c.target === 'save' && c.source === 'process');
|
|
expect(saveCall).toBeDefined();
|
|
expect(saveCall!.targetFilePath).toBe('src/models/User.js');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Parent class resolution: EXTENDS edge
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('JavaScript parent resolution', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'javascript-parent-resolution'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects BaseModel and User classes', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toEqual(['BaseModel', 'User']);
|
|
});
|
|
|
|
it('emits EXTENDS edge: User → BaseModel', () => {
|
|
const extends_ = getRelationships(result, 'EXTENDS');
|
|
expect(extends_.length).toBe(1);
|
|
expect(extends_[0].source).toBe('User');
|
|
expect(extends_[0].target).toBe('BaseModel');
|
|
});
|
|
|
|
it('EXTENDS edge points to real graph node', () => {
|
|
const extends_ = getRelationships(result, 'EXTENDS');
|
|
const target = result.graph.getNode(extends_[0].rel.targetId);
|
|
expect(target).toBeDefined();
|
|
expect(target!.properties.name).toBe('BaseModel');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Nullable receiver: JSDoc @param {User | null} strips nullable via TypeEnv
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('JavaScript nullable receiver resolution', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(path.join(FIXTURES, 'js-nullable-receiver'), () => {});
|
|
}, 60000);
|
|
|
|
it('detects User and Repo classes, both with save methods', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toEqual(['Repo', 'User']);
|
|
const saveMethods = getNodesByLabel(result, 'Method').filter((m) => m === 'save');
|
|
expect(saveMethods.length).toBe(2);
|
|
});
|
|
|
|
it('resolves user.save() to src/user.js via nullable-stripped JSDoc type', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const userSave = calls.find(
|
|
(c) =>
|
|
c.target === 'save' && c.source === 'processEntities' && c.targetFilePath === 'src/user.js',
|
|
);
|
|
expect(userSave).toBeDefined();
|
|
});
|
|
|
|
it('resolves repo.save() to src/repo.js via nullable-stripped JSDoc type', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const repoSave = calls.find(
|
|
(c) =>
|
|
c.target === 'save' && c.source === 'processEntities' && c.targetFilePath === 'src/repo.js',
|
|
);
|
|
expect(repoSave).toBeDefined();
|
|
});
|
|
|
|
it('emits exactly 2 save() CALLS edges (one per receiver type)', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveCalls = calls.filter((c) => c.target === 'save');
|
|
expect(saveCalls.length).toBe(2);
|
|
});
|
|
|
|
it('each save() call resolves to a distinct file (no duplicates)', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveCalls = calls.filter((c) => c.target === 'save' && c.source === 'processEntities');
|
|
const files = saveCalls.map((c) => c.targetFilePath).sort();
|
|
expect(files).toEqual(['src/repo.js', 'src/user.js']);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// super.save() resolves to parent class's save method
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('JavaScript super resolution', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'javascript-super-resolution'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects BaseModel, User, and Repo classes, each with a save method', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toEqual(['BaseModel', 'Repo', 'User']);
|
|
const saveMethods = getNodesByLabel(result, 'Method').filter((m) => m === 'save');
|
|
expect(saveMethods.length).toBe(3);
|
|
});
|
|
|
|
it('emits EXTENDS edge: User → BaseModel', () => {
|
|
const extends_ = getRelationships(result, 'EXTENDS');
|
|
expect(extends_.length).toBe(1);
|
|
expect(extends_[0].source).toBe('User');
|
|
expect(extends_[0].target).toBe('BaseModel');
|
|
});
|
|
|
|
it('resolves super.save() inside User to BaseModel.save, not Repo.save', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const superSave = calls.find(
|
|
(c) =>
|
|
c.source === 'save' && c.target === 'save' && c.targetFilePath === 'src/models/Base.js',
|
|
);
|
|
expect(superSave).toBeDefined();
|
|
const repoSave = calls.find(
|
|
(c) => c.target === 'save' && c.targetFilePath === 'src/models/Repo.js',
|
|
);
|
|
expect(repoSave).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Chained method calls: svc.getUser().save()
|
|
// Tests that JavaScript chain call resolution correctly infers the intermediate
|
|
// receiver type from getUser()'s JSDoc @returns {User} and resolves save().
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('JavaScript chained method call resolution', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(path.join(FIXTURES, 'javascript-chain-call'), () => {});
|
|
}, 60000);
|
|
|
|
it('detects User and Repo classes, and UserService', () => {
|
|
const classes = getNodesByLabel(result, 'Class');
|
|
expect(classes).toContain('User');
|
|
expect(classes).toContain('Repo');
|
|
expect(classes).toContain('UserService');
|
|
});
|
|
|
|
it('detects getUser and save methods', () => {
|
|
const methods = getNodesByLabel(result, 'Method');
|
|
expect(methods).toContain('getUser');
|
|
expect(methods).toContain('save');
|
|
});
|
|
|
|
it('resolves svc.getUser().save() to User#save via chain resolution', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const userSave = calls.find(
|
|
(c) =>
|
|
c.target === 'save' && c.source === 'processUser' && c.targetFilePath?.includes('user.js'),
|
|
);
|
|
expect(userSave).toBeDefined();
|
|
});
|
|
|
|
it('does NOT resolve svc.getUser().save() to Repo#save', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const repoSave = calls.find(
|
|
(c) =>
|
|
c.target === 'save' && c.source === 'processUser' && c.targetFilePath?.includes('repo.js'),
|
|
);
|
|
expect(repoSave).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Phase 8: Field/property type resolution — class field_definition capture
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Field type resolution (JavaScript)', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(path.join(FIXTURES, 'js-field-types'), () => {});
|
|
}, 60000);
|
|
|
|
it('detects classes: Address, Config, User', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'Config', 'User']);
|
|
});
|
|
|
|
it('detects Property nodes for JS class fields', () => {
|
|
const properties = getNodesByLabel(result, 'Property');
|
|
expect(properties).toContain('address');
|
|
expect(properties).toContain('name');
|
|
expect(properties).toContain('city');
|
|
});
|
|
|
|
it('emits HAS_PROPERTY edges linking fields to classes', () => {
|
|
const propEdges = getRelationships(result, 'HAS_PROPERTY');
|
|
expect(propEdges.length).toBe(4);
|
|
expect(edgeSet(propEdges)).toContain('User → address');
|
|
expect(edgeSet(propEdges)).toContain('User → name');
|
|
expect(edgeSet(propEdges)).toContain('Address → city');
|
|
expect(edgeSet(propEdges)).toContain('Config → DEFAULT');
|
|
});
|
|
|
|
it('populates field metadata (visibility, isStatic, isReadonly) on Property nodes', () => {
|
|
const properties = getNodesByLabelFull(result, 'Property');
|
|
|
|
const city = properties.find((p) => p.name === 'city');
|
|
expect(city).toBeDefined();
|
|
expect(city!.properties.visibility).toBe('public');
|
|
expect(city!.properties.isStatic).toBe(false);
|
|
expect(city!.properties.isReadonly).toBe(false);
|
|
|
|
const addr = properties.find((p) => p.name === 'address');
|
|
expect(addr).toBeDefined();
|
|
expect(addr!.properties.visibility).toBe('public');
|
|
expect(addr!.properties.isStatic).toBe(false);
|
|
expect(addr!.properties.isReadonly).toBe(false);
|
|
});
|
|
|
|
it('marks Config.DEFAULT as static', () => {
|
|
const properties = getNodesByLabelFull(result, 'Property');
|
|
const def = properties.find((p) => p.name === 'DEFAULT');
|
|
expect(def).toBeDefined();
|
|
expect(def!.properties.isStatic).toBe(true);
|
|
expect(def!.properties.visibility).toBe('public');
|
|
});
|
|
|
|
// `static DEFAULT = new Config()` declares no type, and a static field's
|
|
// initializer binding is kept out of the class scope on purpose (it would
|
|
// retype an instance field of the same name). So `Config.DEFAULT` had no
|
|
// type anywhere and the call was dropped. The Property node now records the
|
|
// type its initializer constructs, and the class-level receiver route reads it.
|
|
it('records the type a static field initializer constructs on its Property node', () => {
|
|
const def = getNodesByLabelFull(result, 'Property').find((p) => p.name === 'DEFAULT');
|
|
expect(def?.properties.initializerType).toBe('Config');
|
|
});
|
|
|
|
it('resolves Config.DEFAULT.validate() through the static field initializer type', () => {
|
|
const targets = getRelationships(result, 'CALLS')
|
|
.filter((c) => c.source === 'validateConfig')
|
|
.map((c) => c.rel.targetId);
|
|
expect(targets).toEqual(['Method:models.js:Config.validate#0']);
|
|
});
|
|
});
|
|
|
|
// ACCESSES write edges from assignment expressions
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Write access tracking (JavaScript)', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(path.join(FIXTURES, 'js-write-access'), () => {});
|
|
}, 60000);
|
|
|
|
it('emits ACCESSES write edges for field assignments', () => {
|
|
const accesses = getRelationships(result, 'ACCESSES');
|
|
const writes = accesses.filter((e) => e.rel.reason === 'write');
|
|
expect(writes.length).toBe(2);
|
|
const fieldNames = writes.map((e) => e.target);
|
|
expect(fieldNames).toContain('name');
|
|
expect(fieldNames).toContain('address');
|
|
const sources = writes.map((e) => e.source);
|
|
expect(sources).toContain('updateUser');
|
|
});
|
|
|
|
it('write ACCESSES edges have confidence 1.0', () => {
|
|
const accesses = getRelationships(result, 'ACCESSES');
|
|
const writes = accesses.filter((e) => e.rel.reason === 'write');
|
|
for (const edge of writes) {
|
|
expect(edge.rel.confidence).toBe(1.0);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Phase A: JS object destructuring — const { field } = receiver → fieldAccess PendingAssignment
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('JavaScript object destructuring resolution (Phase A)', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(path.join(FIXTURES, 'js-object-destructuring'), () => {});
|
|
}, 60000);
|
|
|
|
it('detects User, Address classes', () => {
|
|
const classes = getNodesByLabel(result, 'Class');
|
|
expect(classes).toContain('User');
|
|
expect(classes).toContain('Address');
|
|
});
|
|
|
|
it('resolves address.save() to Address#save via object destructuring', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveCall = calls.find((c) => c.target === 'save' && c.targetFilePath.includes('models'));
|
|
expect(saveCall).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Phase A: Post-fixpoint for-loop replay — iterable resolved via callResult fixpoint
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('JavaScript post-fixpoint for-loop replay (Phase A ex-9B)', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(path.join(FIXTURES, 'js-fixpoint-for-loop'), () => {});
|
|
}, 60000);
|
|
|
|
it('resolves u.save() to User#save via post-fixpoint for-loop replay', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveCall = calls.find(
|
|
(c) => c.target === 'save' && c.source === 'process' && c.targetFilePath.includes('models'),
|
|
);
|
|
expect(saveCall).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Phase 14: Cross-file binding propagation
|
|
// models.js exports getUser() returning User
|
|
// app.js imports getUser, calls const u = getUser(); u.save(); u.getName()
|
|
// → u is typed User via cross-file return type propagation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('JavaScript cross-file binding propagation', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(path.join(CROSS_FILE_FIXTURES, 'js-cross-file'), () => {});
|
|
}, 60000);
|
|
|
|
it('detects User class with save and getName methods', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
|
expect(getNodesByLabel(result, 'Method')).toContain('save');
|
|
expect(getNodesByLabel(result, 'Method')).toContain('getName');
|
|
});
|
|
|
|
it('detects getUser and run functions', () => {
|
|
expect(getNodesByLabel(result, 'Function')).toContain('getUser');
|
|
expect(getNodesByLabel(result, 'Function')).toContain('run');
|
|
});
|
|
|
|
it('emits IMPORTS edge from app.js to models.js', () => {
|
|
const imports = getRelationships(result, 'IMPORTS');
|
|
const edge = imports.find(
|
|
(e) => e.sourceFilePath.includes('app') && e.targetFilePath.includes('models'),
|
|
);
|
|
expect(edge).toBeDefined();
|
|
});
|
|
|
|
it('resolves u.save() in run() to User#save via cross-file return type propagation', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveCall = calls.find(
|
|
(c) => c.target === 'save' && c.source === 'run' && c.targetFilePath.includes('models'),
|
|
);
|
|
expect(saveCall).toBeDefined();
|
|
});
|
|
|
|
it('resolves u.getName() in run() to User#getName via cross-file return type propagation', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const getNameCall = calls.find(
|
|
(c) => c.target === 'getName' && c.source === 'run' && c.targetFilePath.includes('models'),
|
|
);
|
|
expect(getNameCall).toBeDefined();
|
|
});
|
|
|
|
it('emits HAS_METHOD edges linking save and getName to User', () => {
|
|
const hasMethod = getRelationships(result, 'HAS_METHOD');
|
|
const saveEdge = hasMethod.find((e) => e.source === 'User' && e.target === 'save');
|
|
const getNameEdge = hasMethod.find((e) => e.source === 'User' && e.target === 'getName');
|
|
expect(saveEdge).toBeDefined();
|
|
expect(getNameEdge).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Method enrichment: static, parameterTypes (no abstract in JS)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('JavaScript method enrichment', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'javascript-method-enrichment'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects Animal and Dog classes', () => {
|
|
const classes = getNodesByLabel(result, 'Class');
|
|
expect(classes).toContain('Animal');
|
|
expect(classes).toContain('Dog');
|
|
});
|
|
|
|
it('emits HAS_METHOD edges for Animal methods', () => {
|
|
const hasMethod = getRelationships(result, 'HAS_METHOD');
|
|
const animalMethods = hasMethod
|
|
.filter((e) => e.source === 'Animal')
|
|
.map((e) => e.target)
|
|
.sort();
|
|
expect(animalMethods).toContain('speak');
|
|
expect(animalMethods).toContain('classify');
|
|
expect(animalMethods).toContain('breathe');
|
|
});
|
|
|
|
it('emits HAS_METHOD edge for Dog.speak', () => {
|
|
const hasMethod = getRelationships(result, 'HAS_METHOD');
|
|
const dogSpeak = hasMethod.find((e) => e.source === 'Dog' && e.target === 'speak');
|
|
expect(dogSpeak).toBeDefined();
|
|
});
|
|
|
|
it('emits EXTENDS edge Dog -> Animal', () => {
|
|
const extends_ = getRelationships(result, 'EXTENDS');
|
|
const dogExtends = extends_.find((e) => e.source === 'Dog' && e.target === 'Animal');
|
|
expect(dogExtends).toBeDefined();
|
|
});
|
|
|
|
it('marks classify as isStatic (conditional)', () => {
|
|
const methods = getNodesByLabelFull(result, 'Function');
|
|
const classify = methods.find((n) => n.name === 'classify');
|
|
if (classify?.properties.isStatic !== undefined) {
|
|
expect(classify.properties.isStatic).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('marks breathe as NOT isStatic (conditional)', () => {
|
|
const methods = getNodesByLabelFull(result, 'Function');
|
|
const breathe = methods.find((n) => n.name === 'breathe');
|
|
if (breathe?.properties.isStatic !== undefined) {
|
|
expect(breathe.properties.isStatic).toBe(false);
|
|
}
|
|
});
|
|
|
|
it('resolves dog.speak() CALLS edge', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const speakCall = calls.find(
|
|
(c) => c.target === 'speak' && c.sourceFilePath.includes('app.js'),
|
|
);
|
|
expect(speakCall).toBeDefined();
|
|
});
|
|
|
|
it('resolves Animal.classify("dog") static CALLS edge', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const classifyCall = calls.find(
|
|
(c) => c.target === 'classify' && c.sourceFilePath.includes('app.js'),
|
|
);
|
|
expect(classifyCall).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// SM-9: inherited method resolution — child.parentMethod() via first-wins walk
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('JavaScript Child extends Parent — inherited method resolution (SM-9)', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'javascript-child-extends-parent'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects Parent and Child classes', () => {
|
|
const classes = getNodesByLabel(result, 'Class');
|
|
expect(classes).toContain('Parent');
|
|
expect(classes).toContain('Child');
|
|
});
|
|
|
|
it('emits EXTENDS edge: Child → Parent', () => {
|
|
const extends_ = getRelationships(result, 'EXTENDS');
|
|
expect(edgeSet(extends_)).toContain('Child → Parent');
|
|
});
|
|
|
|
it('resolves c.parentMethod() to Parent.parentMethod via first-wins MRO walk', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const parentMethodCall = calls.find(
|
|
(c) => c.target === 'parentMethod' && c.targetFilePath.includes('Parent.js'),
|
|
);
|
|
expect(parentMethodCall).toBeDefined();
|
|
expect(parentMethodCall!.source).toBe('run');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Issue #1358: class-instance singleton (`export const x = new C()`)
|
|
// PR #1718 closed the object-literal-shorthand sub-case; this fixture covers
|
|
// the class-instance sub-case for JavaScript. Same resolution chain as TS but
|
|
// the receiver type comes from the `new ClassName()` initializer (no JSDoc
|
|
// annotation needed — the @type-binding.constructor capture handles it).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('JavaScript class-instance singleton resolution (issue #1358 sub-case)', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'javascript-class-instance-singleton'),
|
|
() => {},
|
|
{ skipGraphPhases: true },
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects FooService class, getUser method, caller function, fooService Const', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toContain('FooService');
|
|
expect(getNodesByLabel(result, 'Method')).toContain('getUser');
|
|
expect(getNodesByLabel(result, 'Function')).toContain('caller');
|
|
expect(getNodesByLabel(result, 'Const')).toContain('fooService');
|
|
});
|
|
|
|
it('emits HAS_METHOD edge from FooService to getUser', () => {
|
|
const hasMethod = getRelationships(result, 'HAS_METHOD');
|
|
const fromClass = hasMethod.filter((e) => e.source === 'FooService').map((e) => e.target);
|
|
expect(fromClass).toEqual(['getUser']);
|
|
});
|
|
|
|
it('resolves caller.fooService.getUser() to FooService.getUser via constructor-inferred typeBinding', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const projected = calls
|
|
.filter((e) => e.source === 'caller' && e.target === 'getUser')
|
|
.map((e) => ({
|
|
targetFilePath: e.targetFilePath,
|
|
reason: e.rel.reason,
|
|
confidence: e.rel.confidence,
|
|
}));
|
|
|
|
expect(projected).toEqual([
|
|
{
|
|
targetFilePath: 'src/service.js',
|
|
reason: 'import-resolved',
|
|
confidence: 0.85,
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Issue #1358: factory-pattern singleton (`export const x = makeC()`)
|
|
// Tests the @type-binding.alias chain-follow for JS — fooService aliases the
|
|
// return of makeFooService(), whose JSDoc @returns {FooService} ties the chain
|
|
// back to the class. Resolution propagates cross-file via
|
|
// propagateImportedReturnTypes followChainPostFinalize.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('JavaScript factory-pattern singleton resolution (issue #1358 sub-case)', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'javascript-factory-singleton'),
|
|
() => {},
|
|
{ skipGraphPhases: true },
|
|
);
|
|
}, 60000);
|
|
|
|
it('detects FooService class, makeFooService function, fooService Const, caller function', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toContain('FooService');
|
|
expect(getNodesByLabel(result, 'Function')).toContain('makeFooService');
|
|
expect(getNodesByLabel(result, 'Function')).toContain('caller');
|
|
expect(getNodesByLabel(result, 'Const')).toContain('fooService');
|
|
});
|
|
|
|
it('resolves caller.fooService.getUser() through the factory chain to FooService.getUser', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const projected = calls
|
|
.filter((e) => e.source === 'caller' && e.target === 'getUser')
|
|
.map((e) => ({
|
|
targetFilePath: e.targetFilePath,
|
|
reason: e.rel.reason,
|
|
confidence: e.rel.confidence,
|
|
}));
|
|
|
|
expect(projected).toEqual([
|
|
{
|
|
targetFilePath: 'src/service.js',
|
|
reason: 'import-resolved',
|
|
confidence: 0.85,
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Inline constructor receiver: new Legacy().doWork() (#2708)
|
|
// The javascript provider is wired `{ keyword: 'new' }` independently of
|
|
// typescript, so it needs its own fixture. Kept under a `javascript-` prefix
|
|
// deliberately: the capture-fingerprint bench globs fixture directories by
|
|
// language prefix, so `.js` sources parked under a `typescript-` directory
|
|
// would be covered by neither language's guard.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('JavaScript inline constructor receiver resolution', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(
|
|
path.join(FIXTURES, 'javascript-inline-constructor-receiver'),
|
|
() => {},
|
|
);
|
|
}, 60000);
|
|
|
|
it('resolves new Legacy().doWork() to Legacy.doWork', () => {
|
|
const call = getRelationships(result, 'CALLS').find(
|
|
(c) => c.source === 'viaInlineNewJs' && c.target === 'doWork',
|
|
);
|
|
expect(call).toMatchObject({ target: 'doWork', targetFilePath: 'svc.js' });
|
|
expect(call!.rel.targetId).toContain('Legacy');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// CommonJS export assignments are declarations, not dropped member writes
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('CommonJS export assignment receivers (JavaScript)', () => {
|
|
let repoDir: string | undefined;
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-js-cjs-export-receiver-'));
|
|
writeFixtureRepo(repoDir, {
|
|
'lib.js': `exports.memberRequest = function (url) {
|
|
return url;
|
|
};
|
|
module.exports.arrowRequest = (url) => url;
|
|
function wrap(module) {
|
|
module.exports.wrapped = 1;
|
|
}
|
|
module.exports.api.nested = 1;
|
|
`,
|
|
'esm-export.js': 'export {};\nmodule.exports.esmExport = 1;\n',
|
|
'esm-import.js': 'import "./side-effect.js";\nmodule.exports.esmImport = 1;\n',
|
|
'side-effect.js': 'const initialized = true;\n',
|
|
'explicit.mjs': 'module.exports.explicitEsm = 1;\n',
|
|
});
|
|
result = await runPipelineFromRepo(repoDir, () => {}, {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
if (repoDir !== undefined) fs.rmSync(repoDir, { recursive: true, force: true });
|
|
});
|
|
|
|
// `module.exports.arrowRequest = …` DECLARES this module's `arrowRequest`
|
|
// export, exactly as `exports.memberRequest = …` on line 1 does. Its receiver
|
|
// merely has a dot in it, so the receiver-bound pass tried to fold
|
|
// `module.exports` as a value, failed, and filed the declaration as a dropped
|
|
// member write. The bare `exports` spelling never reached that path at all.
|
|
//
|
|
// Inside `wrap`, `module` is a PARAMETER: that write goes through a local
|
|
// value the resolver cannot type, so it is a real drop and must stay one. So
|
|
// is the last line, which writes INTO a value hanging off the export object
|
|
// rather than declaring an export.
|
|
it.each<readonly [OutcomeSite, readonly string[]]>([
|
|
[['lib.js', 1, 0, 'memberRequest', 'write'], []],
|
|
[['lib.js', 4, 0, 'arrowRequest', 'write'], []],
|
|
[['lib.js', 6, 2, 'wrapped', 'write'], ['receiver-unresolved']],
|
|
[['lib.js', 8, 0, 'nested', 'write'], ['receiver-unresolved']],
|
|
])('records %j as %j', (site, reasons) => {
|
|
expect(suppressionReasonsAt(result, site)).toEqual(reasons);
|
|
});
|
|
|
|
// Instrumentation only: the declaration keeps the write edge it already had,
|
|
// and gains nothing.
|
|
it('keeps the module-level write edge to each exported function', () => {
|
|
const writes = getRelationships(result, 'ACCESSES')
|
|
.filter((e) => e.sourceLabel === 'File' && e.rel.reason === 'scope-resolution: write')
|
|
.map((e) => e.target)
|
|
.sort();
|
|
expect(writes).toEqual(['arrowRequest', 'memberRequest']);
|
|
});
|
|
|
|
it.each(['esm-export.js', 'esm-import.js', 'explicit.mjs'])(
|
|
'keeps a receiver drop for an unbound module in %s',
|
|
(filePath) => {
|
|
expect(
|
|
result.resolutionOutcomes?.some(
|
|
(outcome) =>
|
|
outcome.kind === 'suppressed' &&
|
|
outcome.filePath === filePath &&
|
|
outcome.reason === 'receiver-unresolved' &&
|
|
outcome.siteKind === 'write',
|
|
),
|
|
).toBe(true);
|
|
},
|
|
);
|
|
});
|
|
|
|
describe('Literal writes and owned receiver bases preserve bindings', () => {
|
|
let result: PipelineResult;
|
|
let fixtureRoot: string | undefined;
|
|
|
|
beforeAll(async () => {
|
|
fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-literal-bindings-'));
|
|
writeFixtureRepo(fixtureRoot, {
|
|
'private.js': `const config = { privateFlag: true };
|
|
class PrivateProducer {
|
|
#make(row) { return { privateFlag: row.value }; }
|
|
}
|
|
`,
|
|
'owned.dart': `class DartWorker { void go() {} }
|
|
class DartBundle { DartWorker worker = DartWorker(); }
|
|
class DartOwner {
|
|
DartBundle task = DartBundle();
|
|
void loop(xs) {
|
|
for (final task in xs) { task.worker.go(); }
|
|
}
|
|
}
|
|
`,
|
|
'input.js': `const config = { secretFlag: true };
|
|
export class SignalService {
|
|
make(options) {
|
|
options.secretFlag = false;
|
|
return { secretFlag: options.secretFlag };
|
|
}
|
|
}
|
|
new SignalService().make(config);
|
|
`,
|
|
'foreign-reader.js': `const local = { foreignFlag: 1 };
|
|
export class Service { make(obj) { return obj.foreignFlag; } }
|
|
`,
|
|
'foreign-producer.js': `export class Service { make() { return { foreignFlag: 0.5 }; } }
|
|
`,
|
|
'repeated.js': `function build() { return { data: { data: 1 } }; }
|
|
function use() { const res = build(); return res.data.data; }
|
|
`,
|
|
'static.js': `class StaticService { go() {} }
|
|
class StaticOther { go() {} }
|
|
class StaticBase { static svc = new StaticService(); }
|
|
class NullChild extends StaticBase { static svc = null; }
|
|
class UnknownChild extends StaticBase { static svc = unknownFactory(); }
|
|
class InheritedChild extends StaticBase {}
|
|
class TypedChild extends StaticBase { static svc = new StaticOther(); }
|
|
function nullProbe() { return NullChild.svc.go(); }
|
|
function unknownProbe() { return UnknownChild.svc.go(); }
|
|
function inheritedProbe() { return InheritedChild.svc.go(); }
|
|
function typedProbe() { return TypedChild.svc.go(); }
|
|
`,
|
|
'constructor.js': `class JsRepo { save() {} }
|
|
class JsActual { save() {} }
|
|
function jsShadow(JsRepo) {
|
|
class JsHolder { static item = new JsRepo(); }
|
|
JsHolder.item.save();
|
|
}
|
|
jsShadow(JsActual);
|
|
`,
|
|
'constructor.ts': `class TsRepo { save() {} }
|
|
class TsActual { save() {} }
|
|
function tsShadow(TsRepo: any) {
|
|
class TsHolder { static item = new TsRepo(); }
|
|
TsHolder.item.save();
|
|
}
|
|
function tsAnnotated(TsRepo: any) {
|
|
class AnnotatedHolder { static item: TsRepo; }
|
|
AnnotatedHolder.item.save();
|
|
}
|
|
tsShadow(TsActual);
|
|
tsAnnotated(TsActual);
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(fixtureRoot, () => {}, { skipGraphPhases: true });
|
|
}, 120000);
|
|
|
|
afterAll(() => {
|
|
if (fixtureRoot !== undefined) fs.rmSync(fixtureRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
it.each(['read', 'write'])('keeps an input property %s on the declared input', (kind) => {
|
|
const edges = getRelationships(result, 'ACCESSES').filter(
|
|
(e) =>
|
|
e.rel.sourceId === 'Method:input.js:SignalService.make#1' &&
|
|
e.rel.reason?.endsWith(`: ${kind}`),
|
|
);
|
|
expect(edges.some((e) => e.rel.targetId === 'Property:input.js:config.secretFlag')).toBe(true);
|
|
if (kind === 'read') {
|
|
expect(edges.some((e) => e.rel.targetId.includes('SignalService.make.secretFlag@'))).toBe(
|
|
false,
|
|
);
|
|
}
|
|
});
|
|
|
|
it('keeps the returned literal declaration write on its own key', () => {
|
|
const writes = getRelationships(result, 'ACCESSES').filter(
|
|
(e) =>
|
|
e.rel.sourceId === 'Method:input.js:SignalService.make#1' &&
|
|
e.rel.targetId === 'Property:input.js:SignalService.make.secretFlag@4:13' &&
|
|
e.rel.reason?.endsWith(': write'),
|
|
);
|
|
expect(writes).toHaveLength(1);
|
|
});
|
|
|
|
it('binds a private method literal write to its own returned key', () => {
|
|
const writes = getRelationships(result, 'ACCESSES').filter(
|
|
(e) =>
|
|
e.sourceFilePath === 'private.js' &&
|
|
e.sourceLabel !== 'File' &&
|
|
e.rel.reason?.endsWith(': write'),
|
|
);
|
|
expect(writes).toHaveLength(1);
|
|
expect(writes[0]?.rel.targetId).toContain(
|
|
'Property:private.js:PrivateProducer.#make.privateFlag@',
|
|
);
|
|
});
|
|
|
|
it('records a Dart chain on a masked field base as owned but unbound', () => {
|
|
expect(getRelationships(result, 'CALLS').filter((e) => e.source === 'loop')).toEqual([]);
|
|
expect(
|
|
result.resolutionOutcomes?.some(
|
|
(o) =>
|
|
o.kind === 'suppressed' &&
|
|
o.reason === 'receiver-owned-but-unbound' &&
|
|
o.filePath === 'owned.dart' &&
|
|
o.name === 'go',
|
|
),
|
|
).toBe(true);
|
|
const owned =
|
|
result.resolutionOutcomes?.filter((outcome) => outcome.filePath === 'owned.dart') ?? [];
|
|
expect(summarizeUnresolvedReceivers(owned)?.counts.go).toBe(1);
|
|
});
|
|
|
|
it('keeps the unresolved outer read when a repeated-name inner read was answered', () => {
|
|
const drops = result.resolutionOutcomes?.filter(
|
|
(outcome) =>
|
|
outcome.kind === 'suppressed' &&
|
|
outcome.reason === 'receiver-unresolved' &&
|
|
outcome.filePath === 'repeated.js' &&
|
|
outcome.name === 'data',
|
|
);
|
|
expect(drops).toHaveLength(1);
|
|
expect(drops?.[0]).toMatchObject({
|
|
range: { startLine: 2, startCol: 45, endCol: 58 },
|
|
});
|
|
expect(
|
|
getRelationships(result, 'ACCESSES').some(
|
|
(edge) =>
|
|
edge.sourceFilePath === 'repeated.js' &&
|
|
edge.rel.reason === 'scope-resolution: return-shape member: read',
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it('does not borrow a same-named method return shape from another file', () => {
|
|
const reads = getRelationships(result, 'ACCESSES').filter(
|
|
(e) => e.rel.sourceId === 'Method:foreign-reader.js:Service.make#1',
|
|
);
|
|
expect(reads.map((e) => e.rel.targetId)).toEqual([
|
|
'Property:foreign-reader.js:local.foreignFlag',
|
|
]);
|
|
});
|
|
|
|
it.each(['nullProbe', 'unknownProbe'])('stops at an untyped static override in %s', (source) => {
|
|
expect(getRelationships(result, 'CALLS').filter((e) => e.source === source)).toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
['inheritedProbe', 'Method:static.js:StaticService.go#0'],
|
|
['typedProbe', 'Method:static.js:StaticOther.go#0'],
|
|
['tsAnnotated', 'Method:constructor.ts:TsRepo.save#0'],
|
|
])('resolves the available field type in %s', (source, target) => {
|
|
expect(
|
|
getRelationships(result, 'CALLS')
|
|
.filter((e) => e.source === source)
|
|
.map((e) => e.rel.targetId),
|
|
).toContain(target);
|
|
});
|
|
|
|
it.each(['jsShadow', 'tsShadow'])('respects the constructor value parameter in %s', (source) => {
|
|
expect(getRelationships(result, 'CALLS').filter((e) => e.source === source)).toEqual([]);
|
|
});
|
|
});
|