Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions graph/bundle/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,21 @@ export async function buildCore(inp: BuildInputs): Promise<CoreTables> {
}
}
}
// a field access whose site is no expression: a module variable read in a type (`typeof X`)
if (A.ir.fieldSites && fieldSites.size > 0) {
const L = A.ir.fieldSites;
const src = await clientSource(inp.clientIrDir, L.file);
if (src) {
const h = src.header;
const ci = h.col(L.id), cl = h.col(L.startLine), cel = h.col(L.endLine);
for await (const r of rowsOf(src)) {
const fa = fieldSites.get(r[ci] ?? '');
if (!fa) continue;
const file = fileOf(L.fileVia, h, r);
for (const fr of fa) { fill(fr, 6, file); fill(fr, 7, int(r[cl])); fill(fr, 9, int(r[cel])); }
}
}
}
// a site the IR writes no name for, where the engine derived the accessor it calls (#1441)
if (A.raw.siteNames) {
for (const [site, name] of await readSource(rawDir, A.raw.siteNames)) {
Expand Down
10 changes: 10 additions & 0 deletions graph/bundle/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ export interface LanguageAdapter {
callSites?: CallSitesIR;
decorators?: DecoratorsIR;
localSites?: LocalSitesIR;
/** field access sites that are not expressions (TypeScript: a `typeof X` type reference), positioned from this table */
fieldSites?: LocalSitesIR;
/** absent where the front end writes no skipped-files report */
skipped?: SkippedIR;
};
Expand Down Expand Up @@ -277,6 +279,11 @@ const TYPESCRIPT: LanguageAdapter = {
startLine: 'startLine', startColumn: 'startColumn',
fileVia: { column: 'tsModuleLinkHash', through: 'modules' },
},
// a module variable read in a TYPE (`typeof X`): the field_access site is the type reference
fieldSites: {
file: 'all-typescript-type-references.csv', id: 'tsTypeReferenceUniqueHash', startLine: 'startLine', endLine: 'endLine',
fileVia: { column: 'tsModuleLinkHash', through: 'modules' },
},
skipped: { file: 'skipped-typescript-files.csv', filePath: 'filePath', reason: 'reason', detail: 'detail' },
},
};
Expand Down Expand Up @@ -337,6 +344,9 @@ const JAVASCRIPT: LanguageAdapter = {
typeAncestors: { file: 'resolution-type-ancestor.csv', columns: [0, 1] },
entryPoints: { file: 'entry-point.csv', columns: [0, 1] },
entryReachable: { file: 'entry-reachable.csv', columns: [0] },
// site, caller, variable, provenance, tier, access: a module variable an identifier reads,
// directly or through an import binding (the field is the variable's own hash)
fieldAccess: { file: 'field-access.csv', columns: [0, 1, 2, 3, 4, 5] },
},
ir: {
// No signature and no owner qualified name: JavaScript declares neither.
Expand Down
60 changes: 60 additions & 0 deletions graph/javascript/engine/call-edge-generation/field_access.dl
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// ============================================================================
// CALL-EDGE-GEN · A MODULE VARIABLE READ BY NAME, AS A DATA EDGE
//
// `CFG.name`, `use(SCHEMA)`, `${TOKENS.store}`: the identifier is bound by the binder
// (expr_binding) to a variable -- the module's own top-level one, or an import binding.
// This engine follows an import to the VALUE the export holds (import_value), which is
// what a call needs, and never to the exported VARIABLE, which is what "who reads this
// const" needs. So a read of an imported const was answered by name, and the name
// matched every same-named const in every sibling package.
//
// The export surface is walked here once more, keeping the variable: export_decl names
// it (`export const x` and `export { x }` carry target kind VARIABLE), and a re-export
// (`export * from`, `export { x } from`) passes it on exactly as module_export_value
// passes a value on. A variable holding a function or a class is left out: its uses
// are calls and constructions, which call edges already carry.
//
// field_access(Site, Caller, Variable, "client", "known_edge", Access), the shape the
// Java and TypeScript engines export, so the bundle reads one relation for all three.
// ============================================================================

js_module_var(v) :- var_decl("client", _, reg, _, _, _, v), !var_owner_method("client", _, v),
!js_not_a_data_binding(reg),
!var_init("client", "FUNCTION", _, v), !var_init("client", "CLASS", _, v),
!var_import("client", _, v).
js_not_a_data_binding("IMPORT_BINDING").
js_not_a_data_binding("FUNCTION_DECLARATION_HOISTED").
js_not_a_data_binding("CLASS_TDZ").
js_not_a_data_binding("CATCH_PARAMETER").

// ── export_var(Module, ExportedName, Variable) — the surface, by variable ───
export_var(mod, n, v) :- export_decl("client", n, _, _, _, "VARIABLE", v, _, mod, _), n != "", js_module_var(v).
export_var(mod, n, v) :- export_decl("client", en, _, "EXPORT_ALL", _, _, _, _, mod, x), export_all_is_spread(en),
export_reexport(_, _, imp, x), import_module(imp, src),
export_var(src, n, v), n != "default".
export_var(mod, en, v) :- export_decl("client", en, ln, form, _, _, _, _, mod, x), form != "EXPORT_ALL",
export_reexport(_, _, imp, x), import_module(imp, src), ln != "",
export_var(src, ln, v).
// ── import_var(ImportHash, Variable) — what a named import binding stands for ─
import_var(imp, v) :- import_decl("client", _, _, bf, n, _, _, _, imp), import_binding_is_named(bf), n != "",
!contains(".", n), import_module(imp, mod), export_var(mod, n, v).

// ── the site ────────────────────────────────────────────────────────────────
js_var_read_target(e, v) :- expr_kind("client", "IDENTIFIER", _, e), expr_binding("client", v, e), js_module_var(v).
js_var_read_target(e, v) :- expr_kind("client", "IDENTIFIER", _, e), expr_binding("client", lv, e),
var_import("client", imp, lv), import_var(imp, v).
// A plain `=` never reads the old value; `+=` and `++` read and then write.
js_var_written(e) :- js_var_read_target(e, _), expr_kind(_, "ASSIGNMENT", _, a), expr_child(_, a, "ASSIGNMENT_TARGET", _, e).
js_var_written(e) :- js_var_read_target(e, _), expr_kind(_, "UNARY", _, u), expr_operator(_, op, u), update_operator(op),
expr_child(_, u, _, _, e).
js_var_write_only(e) :- js_var_read_target(e, _), expr_kind(_, "ASSIGNMENT", _, a), expr_operator(_, "=", a),
expr_child(_, a, "ASSIGNMENT_TARGET", _, e).
js_var_access(e, "write") :- js_var_written(e), js_var_write_only(e).
js_var_access(e, "readwrite") :- js_var_written(e), !js_var_write_only(e).
js_var_access(e, "read") :- js_var_read_target(e, _), !js_var_written(e).
// the caller: the callable the expression is written in, else the module initializer
js_var_read_from(e, m) :- js_var_read_target(e, _), expr_owner("client", m, _, e), m != "".
js_var_read_from(e, init) :- js_var_read_target(e, _), expr_owner("client", "", mod, e), module_init(_, init, mod).

field_access(e, caller, v, "client", "known_edge", acc) :- js_var_read_target(e, v),
js_var_read_from(e, caller), js_var_access(e, acc).
12 changes: 12 additions & 0 deletions graph/javascript/souffle/decls_all.dl
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,18 @@
.decl var_owner_method(c0:symbol, c1:symbol, c2:symbol)
.decl var_type_name(c0:symbol, c1:symbol, c2:symbol, c3:symbol)
.decl var_pattern_root(c0:symbol, c1:symbol, c2:symbol)

// ── call-edge-generation/field_access.dl ──
.decl js_module_var(c0:symbol)
.decl js_not_a_data_binding(c0:symbol)
.decl export_var(c0:symbol, c1:symbol, c2:symbol)
.decl import_var(c0:symbol, c1:symbol)
.decl js_var_read_target(c0:symbol, c1:symbol)
.decl js_var_written(c0:symbol)
.decl js_var_write_only(c0:symbol)
.decl js_var_access(c0:symbol, c1:symbol)
.decl js_var_read_from(c0:symbol, c1:symbol)
.decl field_access(c0:symbol, c1:symbol, c2:symbol, c3:symbol, c4:symbol, c5:symbol)
.decl var_binding_form(c0:symbol, c1:symbol, c2:symbol)

// ── resolution/module-graph.dl ──
Expand Down
1 change: 1 addition & 0 deletions graph/javascript/souffle/export_manifest.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ package_entry package-entry.csv
import_staged_package_unreached import-staged-package-unreached.csv
member_write_refused member-write-refused.csv
jsx_renders jsx-renders.csv
field_access field-access.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// A module-scope const read through member access, as a call argument, in a template and
// under a type query: every use of CFG / S / TOKENS below binds to THIS file's declaration.
export const CFG = { name: 'a', port: 1 } as const;
export const S = mk({});
export const TOKENS = { Store: 'store' } as const;
export let counter = 0;
// a const that holds a function is a function: its uses are calls, not reads (control)
export const make = (n: number) => n + 1;

export function mk(o: object): object {
return o;
}

export function localUse(): string {
return CFG.name;
}

export function bump(): void {
counter += 1;
counter = 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './cfg';
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { CFG, S, TOKENS, make } from '../src';

function use(x: unknown): void {}

export function viaMember(): void {
use(CFG.name);
}

export function asArgument(): void {
use(S);
}

export function inTemplate(): string {
return `${CFG.name}-${TOKENS.Store}`;
}

export function callsTheFunction(): number {
return make(1);
}

export function shadowed(): string {
// a local of the same name: the read is the local's, not the module's (control)
const CFG = { name: 'local' };
return CFG.name;
}

// a read in a type: at module level it is the module's, in a signature the function's
type T = typeof S;

export function annotated(x: typeof CFG): string {
return x.name;
}

// a parameter of the same name: `typeof TOKENS` here is the parameter's type (control)
export function shadowedInType(TOKENS: number): typeof TOKENS {
return TOKENS;
}

function Route(x: unknown): MethodDecorator { return () => {}; }
function Body(x: unknown): ParameterDecorator { return () => {}; }
class Pipe { constructor(readonly s: unknown) {} }

export class Handler {
// a read in a decorator is the decorated method's: on the method, and on its parameter
@Route(TOKENS.Store)
handle(@Body(new Pipe(S)) body: unknown): void {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// The same names, declared again in a sibling package: reads here bind here (control).
export const CFG = { name: 'b', port: 2 } as const;
export const S = {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { CFG, S } from '../src/settings';

export function siblingRead(): unknown {
return [CFG.port, S];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"experimentalDecorators": true,
"noEmit": true
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
ambiguous_unknown read consumer#ownConstraint(T) -> -
ambiguous_unknown read consumer#ownConstraintNamed(T) -> -
ambiguous_unknown read curried-local#widest(unknown) -> -
known_edge read consumer#dataFirst() -> consumer#rows
known_edge read consumer#localDataFirst() -> consumer#rows
known_edge read consumer#localWidest() -> consumer#rows
known_edge read consumer#widestFirst() -> consumer#rows
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
17-constrained-generics [fields]
precision 0.0000 (0 correct, 0 wrong)
recall 0.0000 (0 of 0 the compiler resolved)
sites 3 resolved 0 (0.0%)
tiers ambiguous_unknown=3
access read=3
not scored: 3 rows whose target is not a client declaration
sites 7 resolved 4 (57.1%)
tiers ambiguous_unknown=3 known_edge=4
access read=7
not scored: 7 rows whose target is not a client declaration
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
known_edge read r229#callObjectLiteral(number) -> r229#handlers
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
36-function-value-containers [fields]
precision 0.0000 (0 correct, 0 wrong)
recall 0.0000 (0 of 2 the compiler resolved)
sites 0 resolved 0
tiers
access
not scored: 0 rows whose target is not a client declaration
sites 2 resolved 2 (100.0%)
tiers known_edge=2
access read=2
not scored: 2 rows whose target is not a client declaration
MISSING r229#callClassMembers(number) READ Holder#viaField
MISSING r229#callObjectLiteral(number) READ r229#viaArrow
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
known_edge read main#getDog() -> Zoo#dog
known_edge read main#getDog() -> main#zoo
known_edge read main#getDogPlain() -> Zoo#dog
known_edge read main#getDogPlain() -> main#zoo
known_edge read main#pick(T) -> main#zoo
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
49-indexed-access-return [fields]
precision 1.0000 (2 correct, 0 wrong)
recall 1.0000 (2 of 2 the compiler resolved)
sites 2 resolved 2 (100.0%)
tiers known_edge=2
access read=2
not scored: 0 rows whose target is not a client declaration
sites 5 resolved 5 (100.0%)
tiers known_edge=5
access read=5
not scored: 3 rows whose target is not a client declaration
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ known_edge read Box#run() -> Box#label
known_edge read Box#staticRead() -> Box#static total
known_edge read Box#viaInterface(Shaped) -> Shaped#width
known_edge read Narrow#own() -> Narrow#width
known_edge read members#Shaped2() -> members#Marker2
known_edge readwrite Box#compound(number) -> Box#width
known_edge readwrite Box#decrement() -> Box#hidden
known_edge readwrite Box#increment() -> Box#hidden
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
62-field-access-and-type-use [fields]
precision 1.0000 (20 correct, 0 wrong)
recall 1.0000 (20 of 20 the compiler resolved)
sites 19 resolved 18 (94.7%)
tiers ambiguous_unknown=1 known_edge=18
access read=11 readwrite=3 write=5
not scored: 1 rows whose target is not a client declaration
sites 20 resolved 19 (95.0%)
tiers ambiguous_unknown=1 known_edge=19
access read=12 readwrite=3 write=5
not scored: 2 rows whose target is not a client declaration
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
known_edge read members#callVarAmbient() -> members#varAmbient
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
63-annotated-callable-member [fields]
precision 0.0000 (0 correct, 0 wrong)
recall 0.0000 (0 of 2 the compiler resolved)
sites 0 resolved 0
tiers
access
not scored: 0 rows whose target is not a client declaration
sites 1 resolved 1 (100.0%)
tiers known_edge=1
access read=1
not scored: 1 rows whose target is not a client declaration
MISSING members#callPropInline(PropInline) READ PropInline#run
MISSING members#callPropNamed(PropNamed) READ PropNamed#run
3 changes: 3 additions & 0 deletions graph/test/typescript/expected/64-array-dispatch.fields
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
known_edge read pipeline#runByName(Node) -> pipeline#byName
known_edge read pipeline#runIndexed(Node) -> pipeline#steps
known_edge read pipeline#runIterated(Node) -> pipeline#steps
known_edge read steps#measure(Node) -> { width: number }#width
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
64-array-dispatch [fields]
precision 0.0000 (0 correct, 1 wrong)
recall 0.0000 (0 of 2 the compiler resolved)
sites 1 resolved 1 (100.0%)
tiers known_edge=1
access read=1
not scored: 0 rows whose target is not a client declaration
sites 4 resolved 4 (100.0%)
tiers known_edge=4
access read=4
not scored: 3 rows whose target is not a client declaration
WRONG steps#measure(Node) READ { width: number }#width
MISSING pipeline#runByName(Node) READ pipeline#square
MISSING steps#measure(Node) READ steps#width
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
known_edge read fewer#callAmbient() -> fewer#ambient
known_edge read fewer#callSiblingsOne() -> fewer#siblings
known_edge read fewer#callSiblingsTwo() -> fewer#siblings
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
65-annotated-callable-fewer-parameters [fields]
precision 0.0000 (0 correct, 0 wrong)
recall 0.0000 (0 of 1 the compiler resolved)
sites 0 resolved 0
tiers
access
not scored: 0 rows whose target is not a client declaration
sites 3 resolved 3 (100.0%)
tiers known_edge=3
access read=3
not scored: 3 rows whose target is not a client declaration
MISSING fewer#callHolderRun(Holder) READ Holder#run
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
known_edge read routes#<module-init>() -> routes#app
known_edge read routes#<module-init>() -> routes#cache
known_edge read routes#<module-init>() -> routes#headers
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
69-named-and-inline-handlers [fields]
precision 0.0000 (0 correct, 0 wrong)
recall 0.0000 (0 of 0 the compiler resolved)
sites 0 resolved 0
tiers
access
not scored: 0 rows whose target is not a client declaration
sites 4 resolved 4 (100.0%)
tiers known_edge=4
access read=4
not scored: 4 rows whose target is not a client declaration
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
known_edge read fetcher#get(string,Options) -> Options#fetch
known_edge read pool#main() -> pool#fixed
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
72-object-literal-and-expression-callees [fields]
precision 1.0000 (1 correct, 0 wrong)
recall 0.5000 (1 of 2 the compiler resolved)
sites 1 resolved 1 (100.0%)
tiers known_edge=1
access read=1
not scored: 0 rows whose target is not a client declaration
sites 2 resolved 2 (100.0%)
tiers known_edge=2
access read=2
not scored: 1 rows whose target is not a client declaration
MISSING registry#price(number) READ Registry#static format
Loading
Loading