mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-17 05:07:37 +08:00
* fix(resolution): resolve this.#field.method() on the field's type (#1987) A call through an ES private field fell outside the `this.<field>` shape (#1496): the extractor accepted only a property_identifier, so `this.#items.add(x)` was emitted as the bare `add` and exact-matched whichever project method shared the name, often the calling method itself. The extractor and its kernel mirror now keep `this.#items.add`, and the resolver matches that shape and reads the field's type off the class declaration. The field regexes open with a lookbehind instead of `\b`, which never matches before `#`. A builtin or external field type (`new Set()`, a `Map`, an array) now yields no edge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(resolution): require receiver evidence for built-in method calls (#1987) Unknown JavaScript receivers could match unrelated methods by name or class-name similarity. Require validated receiver evidence for built-in method names while retaining typed, imported, object-literal and class resolution. Preserve private-field receivers in TypeScript and Rust extraction and keep public/private field declarations distinct. Build on PR #1990 with regression coverage across all four JS/TS variants and kernel/WASM parity. Co-authored-by: danusha2345 <ewidusoc498@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: danusha2345 <ewidusoc498@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -221,6 +221,14 @@ export class FieldDelegator {
|
||||
direct(): void { this.send('x'); super.toString(); }
|
||||
}
|
||||
|
||||
// --- call through an ES private field (#1987) --------------------------------
|
||||
export class PrivateDelegator {
|
||||
#mailer = new FieldDelegator({ send: (m: string) => m }, []);
|
||||
#items = new Set<string>();
|
||||
send(msg: string): string { return this.#mailer.send(msg); }
|
||||
add(x: string): void { this.#items.add(x); }
|
||||
}
|
||||
|
||||
// --- const-bound functions inside a body (#1669) -----------------------------
|
||||
export function NestedHandlers({ items, onPick }: { items: string[]; onPick: (a: unknown, b: unknown) => void }) {
|
||||
const handleClear = () => { onPick(null, null); };
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CodeGraph } from '../src';
|
||||
|
||||
let dir: string;
|
||||
let cg: CodeGraph | undefined;
|
||||
afterEach(() => {
|
||||
cg?.destroy();
|
||||
cg = undefined;
|
||||
if (dir) fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const families = {
|
||||
array: ['map', 'filter', 'reduce', 'forEach', 'find', 'findIndex', 'some', 'every', 'push', 'pop', 'slice', 'splice', 'sort', 'flatMap', 'includes', 'join'],
|
||||
collection: ['get', 'set', 'has', 'add', 'delete', 'clear', 'keys', 'values', 'entries'],
|
||||
string: ['trim', 'split', 'replace', 'replaceAll', 'match', 'matchAll', 'search', 'startsWith', 'endsWith', 'substring', 'toLowerCase', 'toUpperCase'],
|
||||
promise: ['then', 'catch', 'finally'],
|
||||
function: ['call', 'apply', 'bind'],
|
||||
event: ['on', 'once', 'off', 'emit', 'addListener', 'removeListener', 'removeAllListeners', 'addEventListener', 'removeEventListener', 'dispatchEvent'],
|
||||
iterator: ['next', 'return', 'throw'],
|
||||
};
|
||||
|
||||
describe.each(['ts', 'tsx', 'js', 'jsx'])('built-in method calls in %s (#1987)', (ext) => {
|
||||
it('does not guess project methods for unknown receivers, including class-name overlap', async () => {
|
||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-1987-'));
|
||||
const declarations: string[] = [];
|
||||
const calls: string[] = [];
|
||||
for (const [family, methods] of Object.entries(families)) {
|
||||
// Receiver and class share a word; this is still no evidence of its type.
|
||||
declarations.push(`export class ${family}Wrapper {\n${methods.map(m => ` ${m}() {}\n`).join('')}}\n`);
|
||||
methods.forEach(m => calls.push(`export function ${family}_${m}(${family}) { ${family}.${m}(); }\n`));
|
||||
}
|
||||
fs.writeFileSync(path.join(dir, `decoys.${ext}`), declarations.join(''));
|
||||
fs.writeFileSync(path.join(dir, `calls.${ext}`), calls.join(''));
|
||||
cg = await CodeGraph.init(dir, { index: true });
|
||||
for (const caller of cg.getNodesByKind('function').filter(n => n.filePath === `calls.${ext}`)) {
|
||||
expect(cg.getCallees(caller.id).map(c => c.node.qualifiedName), caller.name).toEqual([]);
|
||||
}
|
||||
expect(cg.getNodesByKind('function').filter(n => n.filePath === `calls.${ext}`)).toHaveLength(calls.length);
|
||||
});
|
||||
|
||||
it('retains every method family on a constructed or typed project receiver', async () => {
|
||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-1987-'));
|
||||
const methods = [...new Set(Object.values(families).flat())];
|
||||
const typed = ext === 'ts' || ext === 'tsx';
|
||||
fs.writeFileSync(path.join(dir, `project.${ext}`), `
|
||||
export class Project {
|
||||
${methods.map(m => ` ${m}() {}\n`).join('')}
|
||||
}
|
||||
export function useConstructed() {
|
||||
const receiver = new Project();
|
||||
${methods.map(m => ` receiver.${m}();\n`).join('')}
|
||||
}
|
||||
${typed ? `export function useTyped(receiver: Project) {
|
||||
${methods.map(m => ` receiver.${m}();\n`).join('')}
|
||||
}` : ''}
|
||||
`);
|
||||
cg = await CodeGraph.init(dir, { index: true });
|
||||
for (const name of typed ? ['useConstructed', 'useTyped'] : ['useConstructed']) {
|
||||
const caller = cg.getNodesByKind('function').find(n => n.name === name)!;
|
||||
expect(cg.getCallees(caller.id).filter(c => c.node.kind === 'method').map(c => c.node.name).sort())
|
||||
.toEqual([...methods].sort());
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects built-in values but retains constructed, imported, literal and class receivers', async () => {
|
||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-1987-'));
|
||||
const typed = ext === 'ts' || ext === 'tsx';
|
||||
fs.writeFileSync(path.join(dir, `cart.${ext}`), `
|
||||
export class Cart {
|
||||
map() {}
|
||||
add() {}
|
||||
get() {}
|
||||
static bind() {}
|
||||
self() { this.add(); }
|
||||
}
|
||||
export const api = { map() {} };
|
||||
export class LRUCache { get() {} }
|
||||
`);
|
||||
fs.writeFileSync(path.join(dir, `calls.${ext}`), `
|
||||
import { Cart, api } from './cart';
|
||||
export function tidy(list${typed ? ': string[]' : ''}) { list.map(); }
|
||||
export function cached() {
|
||||
const cache = new Map();
|
||||
cache.get('x');
|
||||
}
|
||||
export function unknownCache(cache) { cache.get('x'); }
|
||||
export function literalArray() { const cart = []; cart.map(); }
|
||||
export function literalString() { const cart = 'x'; cart.trim(); }
|
||||
export function constructed() {
|
||||
const cart = new Cart();
|
||||
cart.add();
|
||||
}
|
||||
export function imported() { Cart.bind(); }
|
||||
export function objectLiteral() { api.map(); }
|
||||
export const local = { map() {} };
|
||||
export function localLiteral() { local.map(); }
|
||||
${typed ? 'export function typed(cart: Cart) { cart.map(); }' : ''}
|
||||
`);
|
||||
cg = await CodeGraph.init(dir, { index: true });
|
||||
const callees = (name: string) => {
|
||||
const fn = cg!.getNodesByKind('function').find(n => n.name === name)!;
|
||||
expect(fn, name).toBeDefined();
|
||||
return cg!.getCallees(fn.id).filter(({ edge }) => edge.kind === 'calls').map(c => c.node.qualifiedName);
|
||||
};
|
||||
for (const name of ['tidy', 'cached', 'unknownCache', 'literalArray', 'literalString']) expect(callees(name), name).toEqual([]);
|
||||
expect(callees('constructed')).toContain('Cart::add');
|
||||
expect(callees('imported')).toContain('Cart::bind');
|
||||
expect(callees('objectLiteral')).toEqual(['map']);
|
||||
expect(callees('localLiteral')).toEqual(['map']);
|
||||
if (typed) expect(callees('typed')).toEqual(['Cart::map']);
|
||||
const self = cg.getNodesByKind('method').find(n => n.qualifiedName === 'Cart::self')!;
|
||||
expect(cg.getCallees(self.id).map(c => c.node.qualifiedName)).toEqual(['Cart::add']);
|
||||
});
|
||||
});
|
||||
@@ -124,6 +124,26 @@ function nested(holder) {
|
||||
expect(result.unresolvedReferences.some((r) => r.referenceName === 'values.get')).toBe(true);
|
||||
});
|
||||
|
||||
describe.each([
|
||||
['ts', 'typescript'], ['tsx', 'tsx'], ['js', 'javascript'], ['jsx', 'jsx'],
|
||||
] as const)('private field receivers: %s (#1987)', (ext, language) => {
|
||||
it.each(['LF', 'CRLF'])('preserves private fields and optional calls (%s)', (ending) => {
|
||||
const source = `
|
||||
class Mailer { send() {} }
|
||||
class Vault {
|
||||
#mailer = new Mailer();
|
||||
#items = new Set();
|
||||
notify() { this.#mailer?.send(); }
|
||||
optional() { this.#mailer.send?.(); }
|
||||
put() { this.#items?.add('x'); }
|
||||
}
|
||||
`;
|
||||
const result = assertParity(`vault.${ext}`, ending === 'CRLF' ? source.replace(/\n/g, '\r\n') : source, language);
|
||||
expect(result.unresolvedReferences.filter(r => r.referenceKind === 'calls').map(r => r.referenceName))
|
||||
.toEqual(['this.#mailer.send', 'this.#mailer.send', 'this.#items.add']);
|
||||
});
|
||||
});
|
||||
|
||||
it('torture fixture (tsx): components, stores, RTK, fn-refs, value-refs, decorators', () => {
|
||||
const file = path.join(FIXTURE_DIR, 'torture.tsx');
|
||||
assertParity('fixtures/torture.tsx', fs.readFileSync(file, 'utf8'), 'tsx');
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* shape a delegating wrapper takes. The identical call resolved correctly
|
||||
* whenever the wrapper had any other name.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
@@ -65,6 +65,23 @@ beforeAll(async () => {
|
||||
' async settings(): Promise<object> { return this.storage.getSettings(); }\n' +
|
||||
'}\n'
|
||||
);
|
||||
// ES private fields (#1987). `Outbox::send` and `Cart::add` sit in the same
|
||||
// file, so a bare-name guess would pick them over the field's real type.
|
||||
w(
|
||||
'vault.ts',
|
||||
"import { Mailer } from './mailer';\n" +
|
||||
'export class Outbox {\n send(msg: string): string { return msg; }\n}\n' +
|
||||
'export class Cart {\n add(item: string): void {}\n}\n' +
|
||||
'export class Vault {\n' +
|
||||
' #mailer: Mailer;\n' +
|
||||
' #backup = new Mailer();\n' +
|
||||
' #items = new Set<string>();\n' +
|
||||
' constructor(m: Mailer) { this.#mailer = m; }\n' +
|
||||
' notify(msg: string): string { return this.#mailer.send(msg); }\n' +
|
||||
' fallback(msg: string): string { return this.#backup.send(msg); }\n' +
|
||||
' put(x: string): void { this.#items.add(x); }\n' +
|
||||
'}\n'
|
||||
);
|
||||
cg = CodeGraph.initSync(dir);
|
||||
await cg.indexAll();
|
||||
});
|
||||
@@ -103,4 +120,52 @@ describe('this.<field>.<method>() (#1496)', () => {
|
||||
const self = cg.getCallers(method('Keeper::get').id).some(({ node }) => node.id === method('Keeper::get').id);
|
||||
expect(self).toBe(false);
|
||||
});
|
||||
|
||||
it('resolves an ES private field on its declared or constructed type (#1987)', () => {
|
||||
expect(calleesOf('Vault::notify')).toEqual(['Mailer::send']);
|
||||
expect(calleesOf('Vault::fallback')).toEqual(['Mailer::send']);
|
||||
});
|
||||
|
||||
it('leaves a builtin-typed ES private field unresolved (#1987)', () => {
|
||||
// `this.#items.add()` on a Set must not bind to the project's `Cart::add`.
|
||||
expect(calleesOf('Vault::put')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(['ts', 'tsx', 'js', 'jsx'])('private field receivers in %s (#1987)', (ext) => {
|
||||
let temp: string;
|
||||
let graph: CodeGraph | undefined;
|
||||
afterEach(() => {
|
||||
graph?.destroy();
|
||||
graph = undefined;
|
||||
if (temp) fs.rmSync(temp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it.each(['LF', 'CRLF'])('keeps optional receivers distinct from public fields (%s)', async (ending) => {
|
||||
temp = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-private-1987-'));
|
||||
const source = `
|
||||
export class Mailer { send() {} }
|
||||
export class Cart { add() {} }
|
||||
export class Vault {
|
||||
#mailer = new Mailer();
|
||||
#items = new Set();
|
||||
items = new Cart();
|
||||
notify() { this.#mailer?.send(); }
|
||||
optional() { this.#mailer.send?.(); }
|
||||
put() { this.#items?.add('x'); }
|
||||
publicPut() { this.items.add('x'); }
|
||||
}
|
||||
`;
|
||||
fs.writeFileSync(path.join(temp, `vault.${ext}`), ending === 'CRLF' ? source.replace(/\n/g, '\r\n') : source);
|
||||
graph = await CodeGraph.init(temp, { index: true });
|
||||
const callees = (name: string) => {
|
||||
const caller = graph!.getNodesByKind('method').find(n => n.qualifiedName === `Vault::${name}`)!;
|
||||
expect(caller).toBeDefined();
|
||||
return graph!.getCallees(caller.id).map(c => c.node.qualifiedName);
|
||||
};
|
||||
expect(callees('notify')).toEqual(['Mailer::send']);
|
||||
expect(callees('optional')).toEqual(['Mailer::send']);
|
||||
expect(callees('put')).toEqual([]);
|
||||
expect(callees('publicPut')).toEqual(['Cart::add']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1237,14 +1237,17 @@ impl<'t> Walker<'t> {
|
||||
|
||||
// --- extractInstantiation -----------------------------------------------------------
|
||||
|
||||
/// `this.<field>` as a member_expression receiver → Some(field) (#1496).
|
||||
/// `this.<field>` as a member_expression receiver → Some(field) (#1496, #1987).
|
||||
fn this_field_of(&self, receiver: Node<'t>) -> Option<String> {
|
||||
if receiver.kind() != "member_expression" {
|
||||
return None;
|
||||
}
|
||||
let object = receiver.child_by_field_name("object")?;
|
||||
let property = receiver.child_by_field_name("property")?;
|
||||
if object.kind() != "this" || property.kind() != "property_identifier" {
|
||||
// An ES private field (`this.#items`) keeps its `#` (#1987).
|
||||
if object.kind() != "this"
|
||||
|| !matches!(property.kind(), "property_identifier" | "private_property_identifier")
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(self.text(property).to_string())
|
||||
|
||||
@@ -409,6 +409,8 @@ const TS_JS_CHAIN_LANGUAGES = new Set(['typescript', 'tsx', 'javascript', 'jsx']
|
||||
|
||||
/** Receiver node types (TS/JS grammars) that continue a member chain downward. */
|
||||
const TS_JS_CHAIN_RECEIVER_TYPES = new Set(['member_expression', 'subscript_expression']);
|
||||
/** The field of a `this.<field>.<method>()` receiver: public or ES private (#1496, #1987). */
|
||||
const THIS_FIELD_PROPERTY_TYPES = new Set(['property_identifier', 'private_property_identifier']);
|
||||
|
||||
/**
|
||||
* Identifier-rooted member chains have no inferred property type (#1566),
|
||||
@@ -4842,7 +4844,7 @@ export class TreeSitterExtractor {
|
||||
receiver &&
|
||||
receiver.type === 'member_expression' &&
|
||||
getChildByField(receiver, 'object')?.type === 'this' &&
|
||||
getChildByField(receiver, 'property')?.type === 'property_identifier'
|
||||
THIS_FIELD_PROPERTY_TYPES.has(getChildByField(receiver, 'property')?.type ?? '')
|
||||
) {
|
||||
// TS/JS call through a field of the enclosing class —
|
||||
// `this.mailer.send()` (#1496). Keep the `this.<field>` prefix:
|
||||
@@ -4855,6 +4857,8 @@ export class TreeSitterExtractor {
|
||||
// itself when the two share a name, a self-edge not in the
|
||||
// source. Same discipline as Rust's `self.<field>` (#1585).
|
||||
// Mirrored in the kernel's extract_call (tsjs/extractors.rs).
|
||||
// An ES private field (`this.#items.add()`) is the same shape;
|
||||
// its name keeps the `#` (#1987).
|
||||
const fieldName = getNodeText(getChildByField(receiver, 'property')!, this.source);
|
||||
calleeName = `this.${fieldName}.${methodName}`;
|
||||
} else if (
|
||||
|
||||
@@ -7,6 +7,30 @@ export const JS_BUILT_INS = new Set([
|
||||
'fetch', 'require', 'module', 'exports', '__dirname', '__filename',
|
||||
]);
|
||||
|
||||
/** Method names that require receiver evidence before linking to project code. */
|
||||
export const JS_BUILTIN_METHODS = new Set([
|
||||
// Array / typed arrays and collections.
|
||||
'at', 'concat', 'copyWithin', 'entries', 'every', 'fill', 'filter', 'find',
|
||||
'findIndex', 'findLast', 'findLastIndex', 'flat', 'flatMap', 'forEach',
|
||||
'includes', 'indexOf', 'join', 'keys', 'lastIndexOf', 'map', 'pop', 'push',
|
||||
'reduce', 'reduceRight', 'reverse', 'shift', 'slice', 'some', 'sort', 'splice',
|
||||
'toReversed', 'toSorted', 'toSpliced', 'unshift', 'values', 'with', 'subarray',
|
||||
'get', 'set', 'has', 'add', 'delete', 'clear',
|
||||
// String.
|
||||
'charAt', 'charCodeAt', 'codePointAt', 'endsWith', 'localeCompare', 'match',
|
||||
'matchAll', 'normalize', 'padEnd', 'padStart', 'repeat', 'replace', 'replaceAll',
|
||||
'search', 'split', 'startsWith', 'substring', 'substr', 'toLowerCase',
|
||||
'toUpperCase', 'toLocaleLowerCase', 'toLocaleUpperCase', 'trim', 'trimStart',
|
||||
'trimEnd', 'trimLeft', 'trimRight', 'toString', 'toLocaleString', 'valueOf',
|
||||
// Promise, Function, EventTarget / EventEmitter and iterators.
|
||||
'then', 'catch', 'finally', 'call', 'apply', 'bind',
|
||||
'addEventListener', 'removeEventListener', 'dispatchEvent', 'on', 'once',
|
||||
'off', 'emit', 'addListener', 'removeListener', 'removeAllListeners',
|
||||
'prependListener', 'prependOnceListener', 'listeners', 'rawListeners',
|
||||
'listenerCount', 'eventNames', 'setMaxListeners', 'getMaxListeners',
|
||||
'next', 'return', 'throw', 'drop', 'take', 'toArray',
|
||||
]);
|
||||
|
||||
/**
|
||||
* TypeScript primitive type names. Distinct from JS_BUILT_INS on purpose: those
|
||||
* are runtime globals a receiver can be constructed from, these only ever come
|
||||
|
||||
@@ -8,7 +8,7 @@ import * as path from 'path';
|
||||
import { Language, Node } from '../types';
|
||||
import { UnresolvedRef, ResolvedRef, ResolutionContext, SUPERTYPE_TARGET_KINDS, isInheritanceRef, isImportableKind } from './types';
|
||||
import { blankStringContents, stripCommentsForRegex } from './strip-comments';
|
||||
import { JS_BUILT_INS, TS_PRIMITIVE_TYPES } from './js-builtins';
|
||||
import { JS_BUILT_INS, JS_BUILTIN_METHODS, TS_PRIMITIVE_TYPES } from './js-builtins';
|
||||
|
||||
/**
|
||||
* Ceiling on how many same-named definitions a FUZZY name-match strategy will
|
||||
@@ -2363,6 +2363,15 @@ export function matchMethodCall(
|
||||
);
|
||||
}
|
||||
|
||||
// A TS/JS call through an ES private field of the enclosing class —
|
||||
// `this.#items.add()`, emitted as `this.#items.add` (#1987) — resolves
|
||||
// exactly like `this.<field>` below (#1496). `#` is outside dotMatch's
|
||||
// receiver class, so the shape is matched here.
|
||||
if (ref.language === 'typescript' || ref.language === 'javascript' || ref.language === 'tsx' || ref.language === 'jsx') {
|
||||
const privateField = ref.referenceName.match(/^this\.(#[\w$]+)\.(\w+)$/);
|
||||
if (privateField) return matchTsThisFieldCall(privateField[1]!, privateField[2]!, ref, context);
|
||||
}
|
||||
|
||||
const match = dotMatch || colonMatch || luaColonMatch || rDollarMatch;
|
||||
if (!match) {
|
||||
return null;
|
||||
@@ -2572,6 +2581,14 @@ export function matchMethodCall(
|
||||
});
|
||||
if (strat1) return strat1;
|
||||
|
||||
// Built-in method names need a validated receiver (#1987). Typed, imported,
|
||||
// object-literal and direct class receivers have had their chance above;
|
||||
// capitalization, word overlap or a unique method name are not evidence
|
||||
// that `list.map()` / `cache.get()` calls a project class.
|
||||
if (ref.referenceKind === 'calls' && JS_FAMILY.has(ref.language) &&
|
||||
objectOrClass !== 'this' && objectOrClass !== 'super' &&
|
||||
JS_BUILTIN_METHODS.has(methodName!)) return null;
|
||||
|
||||
// Strategy 2: Instance variable receiver - try capitalized form to find class
|
||||
// e.g., "permissionEngine" → look for classes containing "PermissionEngine"
|
||||
const capitalizedReceiver = objectOrClass!.charAt(0).toUpperCase() + objectOrClass!.slice(1);
|
||||
@@ -2949,6 +2966,9 @@ function matchTsThisFieldCall(
|
||||
(n) => (n.kind === 'class' || n.kind === 'component') && sameLanguageFamily(n.language, ref.language)
|
||||
);
|
||||
const fieldEsc = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
// A word boundary cannot open a private name; it also lets a public
|
||||
// `items` match `#items`. Keep the two field namespaces distinct (#1987).
|
||||
const fieldStart = '(?<![\\w$#])';
|
||||
const patterns: Array<{ re: RegExp; valueType: boolean }> = [
|
||||
// `storage: typeof DraftHubStorage` — the type OF a value: an object
|
||||
// literal used as a namespace. Its members are bare-named functions inside
|
||||
@@ -2956,18 +2976,18 @@ function matchTsThisFieldCall(
|
||||
// `Type::method`. Tried first: the declared-type pattern below would
|
||||
// otherwise capture the word `typeof`.
|
||||
{
|
||||
re: new RegExp(`\\b${fieldEsc}\\b\\s*[?!]?\\s*:\\s*(?:readonly\\s+)?typeof\\s+([A-Za-z_$][\\w.$]*)`),
|
||||
re: new RegExp(`${fieldStart}${fieldEsc}\\b\\s*[?!]?\\s*:\\s*(?:readonly\\s+)?typeof\\s+([A-Za-z_$][\\w.$]*)`),
|
||||
valueType: true,
|
||||
},
|
||||
// `private readonly mailer?: Mailer` — a class field or a constructor
|
||||
// parameter property; the capture stops at `<`, `[` or `|`, so a generic
|
||||
// or union type yields its head and resolveMethodOnType decides.
|
||||
{
|
||||
re: new RegExp(`\\b${fieldEsc}\\b\\s*[?!]?\\s*:\\s*(?:readonly\\s+)?([A-Za-z_$][\\w.$]*)`),
|
||||
re: new RegExp(`${fieldStart}${fieldEsc}\\b\\s*[?!]?\\s*:\\s*(?:readonly\\s+)?([A-Za-z_$][\\w.$]*)`),
|
||||
valueType: false,
|
||||
},
|
||||
// `mailer = new Mailer()` / `this.mailer = new Mailer()`
|
||||
{ re: new RegExp(`\\b${fieldEsc}\\b\\s*=\\s*new\\s+([A-Za-z_$][\\w.$]*)`), valueType: false },
|
||||
{ re: new RegExp(`${fieldStart}${fieldEsc}\\b\\s*=\\s*new\\s+([A-Za-z_$][\\w.$]*)`), valueType: false },
|
||||
];
|
||||
for (const cls of owners) {
|
||||
const source = context.readFile(cls.filePath);
|
||||
|
||||
Reference in New Issue
Block a user