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
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// ============================================================================
// CASE 82 — `call` / `apply` / `bind` in a project whose tsconfigs DISAGREE
// ============================================================================
// Cases 21 and 22 each compile under ONE setting of `strictBindCallApply`. This one
// has two programs: `src/tsconfig.json` (strict) governs this file and `strict-site.ts`,
// and `src/loose/tsconfig.json` (strictBindCallApply: false) governs `loose/`. The
// same call written in each answers with a different declaration:
//
// strict-site.ts -> CallableFunction#bind / #call / #apply
// loose/loose-site.ts -> Function#bind / #call / #apply
//
// The flag is read off the module the CALL is written in, never off the module that
// declares `bind` (the library) and never as one answer for the whole project.
//
// The receivers are TYPED through a method's return, the shape that made the regime
// decide at all: `factory.get(k).bind(r)`.

export type Handler = (req: string) => string;

export class HandlerFactory {
get(key: string): Handler {
return (req: string) => key + req;
}
}

export function plain(value: string): string {
return value;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// ============================================================================
// The case's OWN standard library — the minimum the checker needs under `noLib`.
// ============================================================================
// WHY noLib. This mechanism's targets are declared in the REAL `lib.es5.d.ts`, and a
// case never has it: the case oracle only records a target inside the case's own files,
// and the engine is handed only `--library <this dir's IR>`. So with the real library
// neither side can name the target and the case measures nothing. Declaring `Function`
// and `CallableFunction` HERE puts them on both sides of the comparison.
//
// This is not a mock of the behaviour. The checker resolves `CallableFunction` by NAME
// from whatever is in scope and then applies `strictBindCallApply` itself, so these
// declarations exercise the real decision. Verified: the same two declarations answer
// CallableFunction under `strict: true` and Function under
// `strict: false, strictBindCallApply: false`.
//
// Everything other than Function/CallableFunction is here only because `noLib` removes
// it and the checker requires it. Keep this file minimal: an interface added here is a
// declaration the engine can resolve to, and an accidental one is an accidental answer.

interface Object { toString(): string; }
interface Boolean {}
interface Number {}
interface String { padStart(width: number): string; padEnd(width: number): string; trim(): string; }
interface Array<T> { length: number; }
interface ReadonlyArray<T> { length: number; }
interface IArguments {}
interface RegExp {}

// ── the two declarations the flag chooses between ───────────────────────────
// Mirrors lib.es5.d.ts: `Function` declares all three, and `CallableFunction extends
// Function` REDECLARES the same three with precise generic signatures. Both are in
// scope at every site; only the flag decides.
// NO explicit `this` parameter on these. lib.es5.d.ts declares them with one, but the
// case oracle's edge label includes a `this` parameter in the signature while the engine
// (correctly) does not treat it as a parameter at all — so `this: Function` renders as
// `Function#apply(Function,any,any)` on one side and `Function#apply(any,any)` on the
// other, and every site scores as a disagreement the engine did not actually make.
interface Function {
apply(thisArg: any, argArray?: any): any;
call(thisArg: any, ...argArray: any[]): any;
bind(thisArg: any, ...argArray: any[]): any;
readonly name: string;
toString(): string;
}

interface CallableFunction extends Function {
// Simply typed on purpose. lib.es5.d.ts gives these precise generic signatures
// (`this: (this: T, ...args: A) => R`), and this fixture does not need them: what it
// tests is that TWO declarations of the same member names exist and that
// `strictBindCallApply` picks between them, which the checker decides from the flag
// and not from how well a signature fits. A function-typed parameter also renders
// badly in the case oracle's edge labels — the label splits on the commas inside
// `(this: T, ...args: A) => R` and emits `apply(args: A) =,T,T)`, which can never
// match the engine's label, so every site scored as a disagreement.
//
// Arguments are optional and returns are `any` so every call form in the case source
// typechecks. The two declarations stay distinguishable without help: an edge label
// carries the OWNER, so `CallableFunction#call` and `Function#call` never collide, and
// that owner is exactly what the flag decides.
call(thisArg: any, arg0?: any, arg1?: any): any;
apply(thisArg: any, args?: any): any;
bind(thisArg: any, arg0?: any): any;
}


interface NewableFunction extends Function {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Governed by src/loose/tsconfig.json (strictBindCallApply: false) -> Function's
// declarations, although the project's other program is strict.
import { HandlerFactory, plain } from '../factory';

export function looseSites(factory: HandlerFactory, r: object): string {
const bound = factory.get('a').bind(r);
factory.get('b').call(r, 'x');
plain.apply(null, ['y']);
return bound('z');
}

// CONTROL — a type that declares its own `bind` keeps it under either regime.
interface OwnBindLoose {
(x: number): number;
bind(label: string): string;
}

export function ownBindLoose(ob: OwnBindLoose): string {
return ob.bind('own');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"strict": false,
"strictBindCallApply": false,
"noLib": true
},
"include": ["**/*", "../globals.d.ts"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Governed by src/tsconfig.json (strict) -> CallableFunction's declarations.
import { HandlerFactory, plain } from './factory';

export function strictSites(factory: HandlerFactory, r: object): string {
const bound = factory.get('a').bind(r);
factory.get('b').call(r, 'x');
plain.apply(null, ['y']);
return bound('z');
}

// CONTROL — a type that declares its own `bind` keeps it under either regime.
interface OwnBind {
(x: number): number;
bind(label: string): string;
}

export function ownBindStrict(ob: OwnBind): string {
return ob.bind('own');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"strict": true,
"noLib": true
},
"exclude": ["loose"]
}
14 changes: 14 additions & 0 deletions graph/test/typescript/expected/82-mixed-bind-call-apply.edges
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
ambiguous_unknown FUNCTION_CALL loose/loose-site#looseSites(HandlerFactory,object) @L9 -> -
ambiguous_unknown FUNCTION_CALL strict-site#strictSites(HandlerFactory,object) @L8 -> -
known_edge METHOD_CALL loose/loose-site#looseSites(HandlerFactory,object) @L6 -> Function#bind(any,any[])
known_edge METHOD_CALL loose/loose-site#looseSites(HandlerFactory,object) @L6 -> HandlerFactory#get(string)
known_edge METHOD_CALL loose/loose-site#looseSites(HandlerFactory,object) @L7 -> Function#call(any,any[])
known_edge METHOD_CALL loose/loose-site#looseSites(HandlerFactory,object) @L7 -> HandlerFactory#get(string)
known_edge METHOD_CALL loose/loose-site#looseSites(HandlerFactory,object) @L8 -> Function#apply(any,any)
known_edge METHOD_CALL loose/loose-site#ownBindLoose(OwnBindLoose) @L19 -> OwnBindLoose#bind(string)
known_edge METHOD_CALL strict-site#ownBindStrict(OwnBind) @L18 -> OwnBind#bind(string)
known_edge METHOD_CALL strict-site#strictSites(HandlerFactory,object) @L5 -> CallableFunction#bind(any,any)
known_edge METHOD_CALL strict-site#strictSites(HandlerFactory,object) @L5 -> HandlerFactory#get(string)
known_edge METHOD_CALL strict-site#strictSites(HandlerFactory,object) @L6 -> CallableFunction#call(any,any,any)
known_edge METHOD_CALL strict-site#strictSites(HandlerFactory,object) @L6 -> HandlerFactory#get(string)
known_edge METHOD_CALL strict-site#strictSites(HandlerFactory,object) @L7 -> CallableFunction#apply(any,any)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
── entry_point (7) ──
exported_from_entry_module loose/loose-site#looseSites loose-site.ts:5
exported_from_entry_module loose/loose-site#ownBindLoose loose-site.ts:18
exported_from_entry_module strict-site#ownBindStrict strict-site.ts:17
exported_from_entry_module strict-site#strictSites strict-site.ts:4
unimported_module globals#<module> globals.d.ts:1
unimported_module loose/loose-site#<module> loose-site.ts:1
unimported_module strict-site#<module> strict-site.ts:1
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
nominal globals#Function.apply -> globals#CallableFunction.apply
nominal globals#Function.bind -> globals#CallableFunction.bind
nominal globals#Function.call -> globals#CallableFunction.call
value factory#<function-type>@18:23 -> factory#HandlerFactory.<arrow>
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
82-mixed-bind-call-apply [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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
oracle=10 engine=10 agree=10 missing=0 (known 0, NEW 0) extra=0
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
known_edge METHOD_PARAM 0 loose/loose-site [METHOD_PARAM] -> HandlerFactory
known_edge METHOD_PARAM 0 loose/loose-site [METHOD_PARAM] -> OwnBindLoose
known_edge METHOD_PARAM 0 strict-site [METHOD_PARAM] -> HandlerFactory
known_edge METHOD_PARAM 0 strict-site [METHOD_PARAM] -> OwnBind
known_edge METHOD_RETURN 0 HandlerFactory [METHOD] -> Handler
known_edge SUPER_TYPE 0 CallableFunction [HERITAGE] -> Function
known_edge SUPER_TYPE 0 NewableFunction [HERITAGE] -> Function
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
82-mixed-bind-call-apply [types]
precision 1.0000 (7 correct, 0 wrong)
recall 1.0000 (7 of 7 the compiler resolved)
sites 7 resolved 7 (100.0%)
tiers known_edge=7
contexts METHOD_PARAM=4 METHOD_RETURN=1 SUPER_TYPE=2
not scored: 0 rows whose target is not a client declaration
56 changes: 33 additions & 23 deletions graph/test/typescript/ground-truth/tsc-program.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* them is meaningless. Copying ninety lines of labelling is how they would drift apart.
* The existing per-case goldens prove the lift changed nothing.
*
* loadProgram(srcDir, libDir, toolName)
* loadProgram(srcDir, libDir, toolName[, programDir])
* -> { ts, program, checker, own, reachable, root, libRoot,
* moduleName, simple, labelOf, callerOf, diagnostics }
*
Expand All @@ -18,7 +18,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { loadTypeScript } from './load-typescript.mjs';

export function loadProgram(srcDir, libDir, toolName) {
export function loadProgram(srcDir, libDir, toolName, programDir) {
const root = path.resolve(srcDir);
// Through the shared loader like the rest of the stack: a bare `import ts from
// 'typescript'` resolves to whatever is nearest and dies on a property access if that
Expand Down Expand Up @@ -70,29 +70,39 @@ export function loadProgram(srcDir, libDir, toolName) {
// The PARSER already reads the case's tsconfig (it must, to emit the resolved flag at
// ts_module c27), so honouring it here is what makes the two sides describe the same
// program.
const caseConfig = path.join(root, 'tsconfig.json');
if (fs.existsSync(caseConfig)) {
const read = ts.readConfigFile(caseConfig, ts.sys.readFile);
if (read.error) {
console.error(`case tsconfig is unreadable: ${caseConfig}`);
process.exit(1);
}
const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, root);
if (parsed.errors.length) {
console.error(`case tsconfig is invalid: ${caseConfig}`);
for (const e of parsed.errors) {
console.error(` ${ts.flattenDiagnosticMessageText(e.messageText, ' ')}`);
// A NESTED tsconfig (`programDir`) is a second program in the same case, as the parser
// treats it: its options are merged over the case root's, so one case can hold a strict
// program and a loose one side by side (#416). The caller picks, per file, the program
// whose directory governs it.
const configDirs = [root];
if (programDir !== undefined && path.resolve(programDir) !== root) {
configDirs.push(path.resolve(programDir));
}
for (const configDir of configDirs) {
const caseConfig = path.join(configDir, 'tsconfig.json');
if (fs.existsSync(caseConfig)) {
const read = ts.readConfigFile(caseConfig, ts.sys.readFile);
if (read.error) {
console.error(`case tsconfig is unreadable: ${caseConfig}`);
process.exit(1);
}
const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, configDir);
if (parsed.errors.length) {
console.error(`case tsconfig is invalid: ${caseConfig}`);
for (const e of parsed.errors) {
console.error(` ${ts.flattenDiagnosticMessageText(e.messageText, ' ')}`);
}
process.exit(1);
}
process.exit(1);
// Merged, not replaced: a case states the ONE option it is about and inherits the
// rest, so a case tsconfig cannot silently drop `lib` and change every other answer.
Object.assign(options, parsed.options);
// `noLib` and the default `lib` list contradict each other, and the default is ours,
// not the case's — so a case asking for noLib gets it rather than getting both.
if (options.noLib) delete options.lib;
// `files`/`include` are ignored on purpose — the file set is the directory walk above,
// which is what the parser is handed too.
}
// Merged, not replaced: a case states the ONE option it is about and inherits the
// rest, so a case tsconfig cannot silently drop `lib` and change every other answer.
Object.assign(options, parsed.options);
// `noLib` and the default `lib` list contradict each other, and the default is ours,
// not the case's — so a case asking for noLib gets it rather than getting both.
if (options.noLib) delete options.lib;
// `files`/`include` are ignored on purpose — the file set is the directory walk above,
// which is what the parser is handed too.
}

const program = ts.createProgram(files, options);
Expand Down
Loading
Loading