fix(cpp): code that calls operator++ by its name no longer stops an index or sync (#2769)

Indexing or syncing a C++ project stopped at "Resolving refs" with
"Invalid regular expression: /\boperator++\s*(?:<[^<>()]*>)?\s*\(/:
Nothing to repeat" as soon as its code called an overloaded operator by
name, as `this->operator++();` inside `operator++(int)` does. Choosing a
method's overload reads the argument list after the name, and
cppParenListIn and cppOverloadParameters wrote the name into that
pattern with only `~` escaped. `operator++` did not compile. `operator()`
matched the name's own empty parentheses as the arguments, `operator[]`
never matched, and `operator+=` matched the next `operator=`. The same
helper serves every language that overloads by arity, so Scala `++`
overloads and Kotlin names in backticks that hold a `(` failed the same
way.

The fix adds one escape helper, escapeRegExp in src/utils.ts, the same
body as the private copies elsewhere. It is used at every place in src/
where a node name, a reference name, a piece of one, or a word read
from source goes into a RegExp unchecked: 114 interpolations in 13
files. 79 were raw, 31 escaped only `$`, 1 `$` and `\`, 1 only `.`, and
the 2 overload readers only `~`. An AST pass over all 490 dynamic
RegExp constructions followed each interpolated value to its sources.
The 72 it still flags were each read by hand. They are
identifier-checked first, built from escaped names, constants, or the
CLI/MCP glob converters. The kernel's one dynamic regex already
escapes.

With operator arguments now read, nlohmann/json's `p->operator[](0)`
reached the argument-type scorer and picked `template <typename T>
operator[](T* key)`: a `size_type` parameter scored as an unknown
class, a `0` as a null pointer. `size_type` and `difference_type` now
count as numbers, as `size_t` does.

Validation:
- New __tests__/operator-method-names.test.ts (5 tests: init and sync of
  an operator fixture, operator() and operator+= overload choice, the
  json operator[](0) shape, Scala and Kotlin). With main's sources,
  4 fail (3 throw "Nothing to repeat"); the json shape fails only with
  size_type taken out of the number types. All pass on the kernel and
  with CODEGRAPH_KERNEL=0.
- Census with a preload that logs every throwing RegExp: main threw
  only in cppParenListIn (CTRE 27, folly 3); the fix threw 0 times in
  all 12 repos below.
- A/B on main efff7457, graph dumps:
  - fmt, glm, rocksdb, protobuf: byte-identical.
  - json: 9 calls retargeted, all right (`j.at("name")` to the key
    overload, `v.at(i)` to size_type).
  - abseil-cpp: 3 retargeted, all right (operator() self-call to the
    two-argument overload, assign(3, 10) to the count overload).
  - CTRE and folly crash on main; both index now. CTRE: 10 of 12 calls
    retargeted against main's census run are right; 2 single-header
    calls reach the identical copy in include/. folly: 4 of 6 are right;
    2 small_vector assign(int, int) calls link the count overload where
    C++ takes the unconstrained template.
  - Controls gin (Go), bulletproof-react (TS), ktlint (Kotlin, 86
    backtick names with pattern characters) and cats (Scala symbolic
    methods): byte-identical.
- Cost: none measurable. Medians of 3 interleaved init runs: fmt -1.5%,
  json +2.1%, glm +0.2%, abseil -2.5%, gin -2.1%, bulletproof-react
  -0.3%, rocksdb +0.4% (45.2 s vs 45.3 s).
- kernel-parity on fmt + json (cpp/c): 458/669 at parity, 0 diffs,
  both arms.

Fixes #2457
Fixes #2482
Fixes #2636

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-22 01:39:08 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 1ef8866aae
commit 6bd217a263
16 changed files with 416 additions and 105 deletions
+1
View File
@@ -148,6 +148,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In C++, a call made with `->` on an `absl::StatusOr`, an `absl::optional` or an `absl::NoDestructor`, like protocolbuffers/protobuf's `resolver->MergeFeatures(…)` on an `absl::StatusOr<FeatureResolver>`, now links to the method of the class it holds, as one on a `std::unique_ptr` or `std::optional` does. A call written without a receiver in a class template now also finds a member of a base that depends on the template's parameters when the class brings it in with a using-declaration, like facebook/rocksdb's `using BasicTypedCacheHelperFns<TValue>::DownCastValue;`, and so does a call on such a member. Re-index C++ projects after upgrading.
- In C and C++, a call or a type name no longer links to something only another source file can see: a class, struct or alias a `.c` or `.cc` file declares for itself, its members, or anything inside that file's unnamed `namespace { … }`. In facebook/rocksdb, other test files' bare `Put(…)`, `Flush()` and `dbfull()` calls had linked to the methods of a test fixture declared in `column_family_test.cc`, and calls on an `Env*` had linked to a test-only environment class in `memenv.cc`. When a header and a source file each declare a class of the same name, like RocksDB's `SharedState`, a call now links to the header's. A source file's own declarations are still found from the file itself, from a file that `#include`s it, and from a header it includes below them. Re-index C and C++ projects after upgrading.
- In C++, a call to an overloaded method now links to the overload its arguments fit: by how many there are, and then by their types. That covers a call on a variable or member of a known class, like facebook/rocksdb's `db->Get(ro, key, &value)` and `db->Put(write_opts, key, value)`, as well as a call written without a receiver, like `Insert(ch)` on a `char ch`. Before, a call on a variable always linked to the first overload declared, so RocksDB's convenience overloads of `DB::Get`, `DB::Put` and `DB::NewIterator` and protocolbuffers/protobuf's `io::Printer::Print`, `Emit` and `Annotate` had almost no callers, and a call without a receiver picked by argument count alone. A literal, a variable the calling function declares, `&variable`, an element read by subscript, like `container[i]` or a map's `values[key]`, and the result of a call are read for their types; a parameter's default value is read from the method's declaration in its class, and comments in argument lists are skipped. Re-index C++ projects after upgrading.
- Indexing or syncing a project no longer stops at "Resolving refs" with `Invalid regular expression … Nothing to repeat` when its code calls a method whose name is not a plain word, like a C++ `operator++` written as `this->operator++()` inside `operator++(int)`, an overloaded Scala `++`, or a Kotlin name in backticks. In C++, a call to another overload of an operator, like `this->operator()(x, 2)` or `total.operator+=(other)`, now links to the overload its arguments fit; before, `operator()` read the empty parentheses of its own name as the call's arguments and stayed linked to itself. A container's `size_type` now counts as a number when overloads are compared, so nlohmann/json's `j.at("name")` and `j.at(i)` each reach the overload that takes them. Re-index C++ projects after upgrading. Thanks @Sven-lxw, @ArronLin and @daheix for the reports and @danusha2345. (#2457, #2482, #2636)
- In C++, a nested class defined outside the class it belongs to, like google/leveldb's `struct SkipList<Key, Comparator>::Node { … }` or facebook/rocksdb's `struct BlockBasedTable::Rep { … }`, is now indexed under its own name, `SkipList::Node`, with its methods and members. Before, it was indexed as an unnamed class, so it was missing from search and `codegraph_explore`, its methods belonged to no class you could name, and a call like `x->Next(level)` on a `Node*` was matched by the method's name alone. Creating an object, as in `new log::Writer(…)` or `Writer w(…)`, now links to the class C++ itself finds for the type as written there: leveldb's `new log::Writer(…)` in `DBImpl` methods no longer links to `DBImpl`'s own `Writer`, RocksDB's tests' `Random rnd(301)` links to RocksDB's `Random` instead of googletest's, an object created through an alias links to the class the alias names, and one links to its class rather than to the class's constructor. An object of a template parameter's type, or of a type the function names with its own `using`, like protocolbuffers/protobuf's `using Map = std::map<…>`, no longer links to a project class that only shares the name. Re-index C++ projects after upgrading.
- In C++, a call whose receiver's type can't be read no longer links to a method of a class nested in the class the receiver's name suggests, or of a class whose name only contains that name. Before, such a guess took any method of the call's name whose qualified name contained the class's name: protocolbuffers/protobuf's `descriptor->file()` in a template reached `Descriptor::ExtensionRange::file`, and facebook/rocksdb's `CompositeEnv::IsInstanceOf(name)` reached `CompositeEnvWrapper::IsInstanceOf`. A call written through a class, like `PessimisticTransaction::PopSavePoint()` in an override, now links to the method that class inherits, here `TransactionBaseImpl::PopSavePoint`. Re-index C++ projects after upgrading.
- In C++, an alias that names a class through a class derived from the one that declares it, like protocolbuffers/protobuf's `using ConformanceRequestSetting = BinaryAndJsonConformanceSuite::ConformanceRequestSetting;`, now leads to that class, `ConformanceTestSuite::ConformanceRequestSetting`. Before, an alias was followed only to a class declared under exactly the name it spells, so objects created through this one linked to the alias rather than the class. Re-index C++ projects after upgrading.
+285
View File
@@ -0,0 +1,285 @@
/**
* A method whose name is not a plain word: C++'s `operator++` and
* `operator()`, Scala's `++`, a Kotlin name written in backticks. The
* resolver writes names into the patterns it reads source with, and these
* went in as pattern syntax:
*
* - `operator++` is "Nothing to repeat". Choosing between a method's
* overloads reads the call's argument list after its name, so one
* `this->operator++();` inside `operator++(int)` stopped a whole index,
* and a sync after an edit, at "Resolving refs" (#2457, #2482, #2636).
* - `operator()` matched the empty `()` of its own name, so a call to the
* two-argument overload read as one with no arguments and stayed linked
* to the overload it was written in.
*/
import { describe, it, expect, afterEach } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import CodeGraph from '../src/index';
import type { Node } from '../src/types';
const lines = (...l: string[]): string => [...l, ''].join('\n');
/** The 1-based line of `text` that contains `needle` (exactly one must). */
const lineOf = (text: string, needle: string): number => {
const at = text.split('\n').map((l, i) => (l.includes(needle) ? i + 1 : 0)).filter(Boolean);
if (at.length !== 1) throw new Error(`${JSON.stringify(needle)} is on ${at.length} lines`);
return at[0]!;
};
const COUNTER_HPP = lines(
'#pragma once',
'#include <cstddef>',
'#include <compare>',
'',
'struct Counter {',
' int value = 0;',
' Counter operator++(int) {',
' Counter before = *this;',
' this->operator++();',
' return before;',
' }',
' Counter& operator++() {',
' ++value;',
' return *this;',
' }',
' Counter& operator+=(int n) {',
' value += n;',
' return *this;',
' }',
' Counter& operator+=(const Counter& other) {',
' value += other.value;',
' return *this;',
' }',
' auto operator<=>(const Counter& other) const = default;',
' explicit operator bool() const { return value != 0; }',
'};',
'',
'struct Scale {',
' int operator()(int x) {',
' return this->operator()(x, 2);',
' }',
' int operator()(int x, int factor) {',
' return x * factor;',
' }',
'};',
'',
'struct Table {',
' int cells[8] = {};',
' int& operator[](std::size_t i) { return cells[i]; }',
'};',
'',
'struct Pointee {',
' int field = 0;',
' int read() const { return field; }',
'};',
'',
'struct Handle {',
' Pointee* target = nullptr;',
' Pointee* operator->() const { return target; }',
' int& operator->*(int Pointee::*m) const { return target->*m; }',
'};',
'',
'struct Pool {',
' static void* operator new[](std::size_t n);',
' static void operator delete(void* p);',
' static void operator delete[](void* p);',
' static void* grab(std::size_t n) { return Pool::operator new[](n); }',
' static void drop(void* p) { Pool::operator delete(p); }',
'};',
'',
'struct Distance { long double km; };',
'Distance operator"" _km(long double v);',
);
const COUNTER_CPP = lines(
'#include "counter.hpp"',
'#include <cstdlib>',
'',
'void* Pool::operator new[](std::size_t n) { return std::malloc(n); }',
'void Pool::operator delete(void* p) { std::free(p); }',
'void Pool::operator delete[](void* p) { Pool::operator delete(p); }',
'Distance operator"" _km(long double v) { return Distance{v}; }',
'',
'int useAll() {',
' Counter c;',
' c.operator++();',
' c.operator++(0);',
' c.operator+=(3);',
' Counter d;',
' d.operator+=(c);',
' bool nonzero = c.operator bool();',
' auto order = c.operator<=>(d);',
' (void)order;',
' Scale s;',
' int scaled = s.operator()(4);',
' Table t;',
' t.operator[](1) = 7;',
' Pointee pointee;',
' Handle h{&pointee};',
' int got = h.operator->()->read() + h.operator->*(&Pointee::field);',
' void* arr = Pool::operator new[](32);',
' Pool::operator delete[](arr);',
' Distance near = operator"" _km(2.0L);',
' return scaled + got + (nonzero ? 1 : 0) + static_cast<int>(near.km);',
'}',
);
describe('a method whose name is not a plain word', () => {
const roots: string[] = [];
const graphs: CodeGraph[] = [];
afterEach(() => {
for (const cg of graphs.splice(0)) cg.close();
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
});
const project = (files: Record<string, string>): string => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-operator-names-'));
roots.push(root);
for (const [rel, text] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), text);
}
return root;
};
const index = async (root: string): Promise<CodeGraph> => {
const cg = await CodeGraph.init(root, { index: true });
graphs.push(cg);
return cg;
};
/** `<qualified name>@<file>:<line>` of each call from the node at `file`:`line`. */
const callsFrom = (cg: CodeGraph, file: string, line: number): string[] => {
const from = cg.getNodesInFile(file).find((n: Node) => n.startLine === line && (n.kind === 'method' || n.kind === 'function'));
expect(from, `${file}:${line}`).toBeDefined();
return cg.getCallees(from!.id).filter((r) => r.edge.kind === 'calls')
.map((r) => `${r.node.qualifiedName}@${path.basename(r.node.filePath)}:${r.node.startLine}`)
.sort();
};
const hpp = (needle: string): string => `counter.hpp:${lineOf(COUNTER_HPP, needle)}`;
const cpp = (needle: string): string => `counter.cpp:${lineOf(COUNTER_CPP, needle)}`;
it('C++: an index of overloaded operators that call each other by name finishes, and each call reaches its overload', async () => {
const cg = await index(project({ 'src/counter.hpp': COUNTER_HPP, 'src/counter.cpp': COUNTER_CPP }));
// `this->operator++();` inside the postfix `operator++(int)`: the prefix one.
expect(callsFrom(cg, 'src/counter.hpp', lineOf(COUNTER_HPP, 'Counter operator++(int)')))
.toEqual([`Counter::operator++@${hpp('Counter& operator++()')}`]);
// `this->operator()(x, 2)` inside the one-argument `operator()`: the two-argument one.
expect(callsFrom(cg, 'src/counter.hpp', lineOf(COUNTER_HPP, 'int operator()(int x) {')))
.toEqual([`Scale::operator()@${hpp('int operator()(int x, int factor)')}`]);
expect(callsFrom(cg, 'src/counter.hpp', lineOf(COUNTER_HPP, 'static void* grab')))
.toEqual([`Pool::operator new[]@${cpp('void* Pool::operator new[]')}`]);
expect(callsFrom(cg, 'src/counter.hpp', lineOf(COUNTER_HPP, 'static void drop')))
.toEqual([`Pool::operator delete@${cpp('void Pool::operator delete(void* p)')}`]);
const useAll = callsFrom(cg, 'src/counter.cpp', lineOf(COUNTER_CPP, 'int useAll()'));
expect(useAll).toEqual(expect.arrayContaining([
`Counter::operator++@${hpp('Counter& operator++()')}`,
`Counter::operator++@${hpp('Counter operator++(int)')}`,
`Counter::operator+=@${hpp('Counter& operator+=(int n)')}`,
`Counter::operator+=@${hpp('Counter& operator+=(const Counter& other)')}`,
`Counter::operator<=>@${hpp('auto operator<=>')}`,
`Scale::operator()@${hpp('int operator()(int x) {')}`,
`Table::operator[]@${hpp('int& operator[]')}`,
`Handle::operator->@${hpp('Pointee* operator->()')}`,
`Handle::operator->*@${hpp('int& operator->*')}`,
`Pool::operator new[]@${cpp('void* Pool::operator new[]')}`,
`Pool::operator delete[]@${cpp('void Pool::operator delete[]')}`,
`operator"" _km@${cpp('Distance operator"" _km(long double v) {')}`,
]));
}, 120_000);
it('C++: a sync that adds such calls to an indexed project finishes, and links them', async () => {
// Indexed first without a single call of `operator++` by its name.
const strip = (text: string): string => text
.replace(' this->operator++();\n', ' ++value;\n')
.replace(' c.operator++();\n', '')
.replace(' c.operator++(0);\n', '');
const root = project({ 'src/counter.hpp': strip(COUNTER_HPP), 'src/counter.cpp': strip(COUNTER_CPP) });
const cg = await index(root);
expect(callsFrom(cg, 'src/counter.hpp', lineOf(COUNTER_HPP, 'Counter operator++(int)'))).toEqual([]);
fs.writeFileSync(path.join(root, 'src/counter.hpp'), COUNTER_HPP);
fs.writeFileSync(path.join(root, 'src/counter.cpp'), COUNTER_CPP);
await cg.sync();
expect(callsFrom(cg, 'src/counter.hpp', lineOf(COUNTER_HPP, 'Counter operator++(int)')))
.toEqual([`Counter::operator++@${hpp('Counter& operator++()')}`]);
expect(callsFrom(cg, 'src/counter.cpp', lineOf(COUNTER_CPP, 'int useAll()'))).toEqual(expect.arrayContaining([
`Counter::operator++@${hpp('Counter& operator++()')}`,
`Counter::operator++@${hpp('Counter operator++(int)')}`,
]));
}, 120_000);
it('C++: the argument list of an `operator()` call is the one after the name, not the name\'s own parentheses', async () => {
// No `operator++` here: nothing threw before, the calls just went to the wrong overloads.
const hppText = lines(
'struct Scale {',
' int operator()(int x) { return this->operator()(x, 2); }',
' int operator()(int x, int factor) { return x * factor; }',
'};',
'struct Total {',
' int value = 0;',
' Total& operator+=(int n) { value += n; return *this; }',
' Total& operator+=(const Total& other) { value += other.value; return *this; }',
'};',
);
const cppText = lines(
'#include "scale.hpp"',
'int addUp() {',
' Total a;',
' Total b;',
' b.operator+=(a);',
' return b.value;',
'}',
);
const cg = await index(project({ 'src/scale.hpp': hppText, 'src/scale.cpp': cppText }));
expect(callsFrom(cg, 'src/scale.hpp', lineOf(hppText, 'int operator()(int x) {')))
.toEqual([`Scale::operator()@scale.hpp:${lineOf(hppText, 'int operator()(int x, int factor)')}`]);
expect(callsFrom(cg, 'src/scale.cpp', lineOf(cppText, 'int addUp()')))
.toEqual([`Total::operator+=@scale.hpp:${lineOf(hppText, 'Total& operator+=(const Total& other)')}`]);
}, 120_000);
it('C++: `operator[](0)` still reaches the `size_type` overload, now that its arguments are read', async () => {
// nlohmann/json's `p = &p->operator[](0);`: the index overload, as before, not the C-string key one.
// A container's `size_type` is a number, which a `0` fits better than it fits a pointer.
const hppText = lines(
'struct Json {',
' using size_type = unsigned long;',
' Json& operator[](size_type idx) { return *this; }',
' template <typename T>',
' JSON_NON_NULL(2)',
' Json& operator[](T* key) { return *this; }',
'};',
'Json* walk(Json* p) { return &p->operator[](0); }',
);
const cg = await index(project({ 'src/json.hpp': hppText }));
expect(callsFrom(cg, 'src/json.hpp', lineOf(hppText, 'Json* walk(')))
.toEqual([`Json::operator[]@json.hpp:${lineOf(hppText, 'operator[](size_type idx)')}`]);
}, 120_000);
it('Scala and Kotlin: overloads named `++` or in backticks no longer stop the index', async () => {
const scala = lines(
'class Vec(val x: Int) {',
' def ++(o: Vec): Vec = new Vec(x + o.x)',
' def ++(n: Int, m: Int): Vec = this.++(new Vec(n + m))',
' def twice: Vec = this.++(1, 2)',
'}',
);
const kotlin = lines(
'class Weird {',
' fun `reads (a value`(x: Int): Int = x',
' fun `reads (a value`(x: Int, y: Int): Int = `reads (a value`(x + y)',
' fun use(): Int = `reads (a value`(1, 2)',
'}',
);
const cg = await index(project({ 'src/Vec.scala': scala, 'src/Weird.kt': kotlin }));
expect(callsFrom(cg, 'src/Vec.scala', lineOf(scala, 'def twice'))).toEqual([`Vec::++@Vec.scala:${lineOf(scala, 'def ++(n: Int, m: Int)')}`]);
expect(callsFrom(cg, 'src/Weird.kt', lineOf(kotlin, 'fun use()'))).toEqual([`Weird::\`reads (a value\`@Weird.kt:${lineOf(kotlin, 'x: Int, y: Int')}`]);
}, 120_000);
});
+2 -1
View File
@@ -34,6 +34,7 @@ import type { Node as SyntaxNode, Tree } from 'web-tree-sitter';
import type { Language } from '../types';
import { getParser, loadGrammarsForLanguages } from '../extraction/grammars';
import { parseWithinBudget } from '../extraction/parse-budget';
import { escapeRegExp } from '../utils';
// =============================================================================
// Public shape
@@ -576,7 +577,7 @@ function statusSetBefore(call: SyntaxNode, callee: string): number | null {
const receiver = bare.split('.')[0]!;
let statement: SyntaxNode | null = call;
while (statement.parent && !STATEMENT_BLOCKS.has(statement.parent.type)) statement = statement.parent;
const re = new RegExp(`^\\s*(?:await\\s+)?${receiver}\\s*\\.\\s*(?:status|code)\\s*\\(\\s*([1-5]\\d{2})\\s*\\)\\s*;?\\s*$|^\\s*${receiver}\\s*\\.\\s*statusCode\\s*=\\s*([1-5]\\d{2})\\s*;?\\s*$`);
const re = new RegExp(`^\\s*(?:await\\s+)?${escapeRegExp(receiver)}\\s*\\.\\s*(?:status|code)\\s*\\(\\s*([1-5]\\d{2})\\s*\\)\\s*;?\\s*$|^\\s*${escapeRegExp(receiver)}\\s*\\.\\s*statusCode\\s*=\\s*([1-5]\\d{2})\\s*;?\\s*$`);
let prev: SyntaxNode | null = statement.previousNamedSibling;
for (let i = 0; prev && i < STATUS_LOOKBACK; i++, prev = prev.previousNamedSibling) {
if (prev.type === 'comment') continue;
+3 -2
View File
@@ -26,6 +26,7 @@
import type { Node } from '../types';
import type { ResolutionContext } from './types';
import { directiveLines } from './cpp-macro-visibility';
import { escapeRegExp } from '../utils';
/** Three-valued: true / false / undefined = the build may go either way. */
type Truth = boolean | undefined;
@@ -130,7 +131,7 @@ function isOff(groups: Directive[][], file: FileDirectives, scope: string[], con
if (name.startsWith('_') || PLATFORM_FLAGS.has(name) || file.defines.has(name)) return undefined;
let named = memo.named.get(name);
// A word of its own, or a compiler flag's `-DX` / `/DX`.
if (!named) memo.named.set(name, (named = new RegExp(`(?:^|[^\\w]|[-/]D)${name}\\b`, 'm')));
if (!named) memo.named.set(name, (named = new RegExp(`(?:^|[^\\w]|[-/]D)${escapeRegExp(name)}\\b`, 'm')));
if (dirs.some((dir) => named!.test(buildTextOf(dir, context, memo)))) return undefined;
const components = new Set(scope.map((f) => componentOf(dirOf(f), context, memo)));
return [...components].some((component) => isDefinedIn(name, component, context, memo)) ? undefined : false;
@@ -299,7 +300,7 @@ function isDefinedIn(name: string, component: string, context: ResolutionContext
if (hit !== undefined) return hit;
memo.cFiles ??= context.getAllFiles().filter((file) => C_FAMILY_FILE.test(file));
const prefix = component ? `${component}/` : '';
const define = new RegExp(`^[ \\t]*#[ \\t]*define[ \\t]+${name}\\b`, 'm');
const define = new RegExp(`^[ \\t]*#[ \\t]*define[ \\t]+${escapeRegExp(name)}\\b`, 'm');
const defined = memo.cFiles.some((file) =>
file.startsWith(prefix) &&
(context.fileContains ? context.fileContains(file, name) : true) &&
+4 -3
View File
@@ -26,6 +26,7 @@ import type { QueryBuilder } from '../db/queries';
import type { ResolutionContext } from './types';
import { isGeneratedFile } from '../extraction/generated-detection';
import { stripCommentsForRegex } from './strip-comments';
import { escapeRegExp } from '../utils';
import { cFnPointerDispatchEdges } from './c-fnptr-synthesizer';
import { goframeRouteEdges } from './goframe-synthesizer';
import { goFuncFieldEdges } from './go-func-field-synthesizer';
@@ -183,7 +184,7 @@ async function fieldChannelEdges(queries: QueryBuilder, ctx: ResolutionContext,
(d) => d.node.filePath === reg.node.filePath && d.field === reg.field
);
if (chDispatchers.length === 0) continue;
const argRe = new RegExp(`${reg.node.name}\\s*\\(\\s*(this\\.\\w+|\\w+)\\s*(?=[,)])`);
const argRe = new RegExp(`${escapeRegExp(reg.node.name)}\\s*\\(\\s*(this\\.\\w+|\\w+)\\s*(?=[,)])`);
let added = 0;
for (const e of queries.getIncomingEdges(reg.node.id, ['calls'])) {
if (added >= MAX_CALLBACKS_PER_CHANNEL) break;
@@ -2078,7 +2079,7 @@ function writtenBare(src: string, name: string): boolean {
* a parameter or local of a function elsewhere in the file binds nothing here.
*/
function ownLinesBind(name: string, src: string): boolean {
const n = name.replace(/\$/g, '\\$');
const n = escapeRegExp(name);
return new RegExp(`\\b(?:const|let|var)\\s+${n}\\b(?!\\s*[,\\]}])`).test(src);
}
@@ -2828,7 +2829,7 @@ function constantEventName(expr: string, file: string, ctx: ResolutionContext, m
const from = importedFile(owner);
decls = from
? declaredIn(from)
: ctx.getNodesByName(name).filter((n) => CONSTANT_KINDS.has(n.kind) && new RegExp(`(?:^|[.:])${owner}(?:[.:]|$)`).test(n.qualifiedName.slice(0, n.qualifiedName.lastIndexOf(name))));
: ctx.getNodesByName(name).filter((n) => CONSTANT_KINDS.has(n.kind) && new RegExp(`(?:^|[.:])${escapeRegExp(owner)}(?:[.:]|$)`).test(n.qualifiedName.slice(0, n.qualifiedName.lastIndexOf(name))));
}
const values = new Set(decls.map(read).filter((v): v is string => v !== null));
value = values.size === 1 ? [...values][0]! : null;
+10 -9
View File
@@ -46,6 +46,7 @@ import type { Node } from '../../types';
import type { FrameworkExtractionResult, FrameworkResolver, ResolutionContext, ResolvedRef, UnresolvedRef } from '../types';
import { resolveImportPath } from '../import-resolver';
import { stripCommentsForRegex } from '../strip-comments';
import { escapeRegExp } from '../../utils';
import { matchBracket, readFields, skipString, topLevelObjects } from './object-literal';
import { dependsOn } from './package-deps';
import {
@@ -516,22 +517,22 @@ export function constantText(expr: string, fromFile: string, context: Resolution
}
return null;
};
const decl = new RegExp(String.raw`\b(?:const|let)\s+${root}\s*(?::[^=]+)?=\s*\{`).exec(safe);
const enumDecl = decl ? null : new RegExp(String.raw`\benum\s+${root}\s*\{`).exec(safe);
const classDecl = decl || enumDecl ? null : new RegExp(String.raw`\bclass\s+${root}\b[^{]*\{`).exec(safe);
const decl = new RegExp(String.raw`\b(?:const|let)\s+${escapeRegExp(root)}\s*(?::[^=]+)?=\s*\{`).exec(safe);
const enumDecl = decl ? null : new RegExp(String.raw`\benum\s+${escapeRegExp(root)}\s*\{`).exec(safe);
const classDecl = decl || enumDecl ? null : new RegExp(String.raw`\bclass\s+${escapeRegExp(root)}\b[^{]*\{`).exec(safe);
if (decl) value = readChain(decl.index + decl[0].length - 1, chain);
else if (enumDecl && chain.length === 1) {
// `enum Paths { Board = 'board' }`
const open = enumDecl.index + enumDecl[0].length - 1;
const body = safe.slice(open, Math.max(open, matchBracket(safe, open)));
value = new RegExp(String.raw`\b${chain[0]}\s*=\s*(['"\`][^'"\`]*['"\`])`).exec(body)?.[1] ?? null;
value = new RegExp(String.raw`\b${escapeRegExp(chain[0]!)}\s*=\s*(['"\`][^'"\`]*['"\`])`).exec(body)?.[1] ?? null;
} else if (classDecl) {
// `class ProjectConst { static readonly IssueId = 'issueId' }`, and
// `class RouterUtil { static Configuration = { Visualizer: 'visualizer' } }`.
const open = classDecl.index + classDecl[0].length - 1;
const close = matchBracket(safe, open);
const body = close > open ? safe.slice(open, close) : '';
const field = new RegExp(String.raw`\bstatic\s+(?:readonly\s+)?${chain[0]}\s*(?::[^=;]+)?=\s*`).exec(body);
const field = new RegExp(String.raw`\bstatic\s+(?:readonly\s+)?${escapeRegExp(chain[0]!)}\s*(?::[^=;]+)?=\s*`).exec(body);
if (field) {
const at = open + field.index + field[0].length;
if (safe[at] === '{') value = chain.length > 1 ? readChain(at, chain.slice(1)) : null;
@@ -550,12 +551,12 @@ export function constantText(expr: string, fromFile: string, context: Resolution
function declarationSource(root: string, file: string, context: ResolutionContext, hops: number): string | null {
const content = context.readFile(file);
if (!content) return null;
if (new RegExp(String.raw`\b(?:const|let|class|enum)\s+${root}\b`).test(content)) return content;
if (new RegExp(String.raw`\b(?:const|let|class|enum)\s+${escapeRegExp(root)}\b`).test(content)) return content;
if (hops >= 3) return null;
const reexports = /\bexport\s+(\*|\{[^}]*\})\s*from\s+(['"])([^'"]+)\2/g;
let m: RegExpExecArray | null;
while ((m = reexports.exec(content)) !== null) {
if (m[1] !== '*' && !new RegExp(String.raw`\b${root}\b`).test(m[1]!)) continue;
if (m[1] !== '*' && !new RegExp(String.raw`\b${escapeRegExp(root)}\b`).test(m[1]!)) continue;
const target = resolveImportPath(m[3]!, file, 'typescript', context);
const found = target ? declarationSource(root, target, context, hops + 1) : null;
if (found) return found;
@@ -577,7 +578,7 @@ function localAlias(expr: string, fromFile: string, context: ResolutionContext):
if ((local ?? prop) === root && prop) return [`${m[2]!.replace(/\s+/g, '')}.${prop}`, ...rest].join('.');
}
}
const simple = new RegExp(String.raw`\b(?:const|let)\s+${root}\s*=\s*([A-Za-z_$][\w$]*(?:\s*\.\s*[A-Za-z_$][\w$]*)+)\s*;`).exec(content);
const simple = new RegExp(String.raw`\b(?:const|let)\s+${escapeRegExp(root)}\s*=\s*([A-Za-z_$][\w$]*(?:\s*\.\s*[A-Za-z_$][\w$]*)+)\s*;`).exec(content);
return simple ? [simple[1]!.replace(/\s+/g, ''), ...rest].join('.') : null;
}
@@ -868,7 +869,7 @@ export function namesSomewhere(href: HrefLiteral | null): HrefLiteral | null {
/** A class property's initializer, read from its body: `routerLinkAbout = publicRoutes.about.routerLink;`. */
export function propertyInitializer(owner: Node, name: string, content: string): string | null {
const body = content.split('\n').slice(owner.startLine - 1, owner.endLine).join('\n');
const decl = new RegExp(String.raw`(?:^|[\s;{])(?:(?:public|private|protected|readonly|static|override)\s+)*${name}\s*(?::[^=;\n]+)?=\s*`).exec(body);
const decl = new RegExp(String.raw`(?:^|[\s;{])(?:(?:public|private|protected|readonly|static|override)\s+)*${escapeRegExp(name)}\s*(?::[^=;\n]+)?=\s*`).exec(body);
if (!decl) return null;
let i = decl.index + decl[0].length;
const start = i;
+3 -2
View File
@@ -48,6 +48,7 @@ import {
} from '../types';
import { resolveImportPath } from '../import-resolver';
import { stripCommentsForRegex } from '../strip-comments';
import { escapeRegExp } from '../../utils';
/**
* Match `Function("name")`, `AsyncFunction("name")`, or `Property("name")`
@@ -234,7 +235,7 @@ function nativeBinding(name: string, file: string, context: ResolutionContext):
let found: NativeBinding | null = null;
const own = context.readFile(file);
if (own && own.includes('NativeModule')) {
const local = new RegExp(String.raw`\b(?:const|let|var)\s+${name}\s*(?::[^=]+)?=\s*` + REQUIRE_NATIVE.source).exec(stripCommentsForRegex(own, 'typescript'));
const local = new RegExp(String.raw`\b(?:const|let|var)\s+${escapeRegExp(name)}\s*(?::[^=]+)?=\s*` + REQUIRE_NATIVE.source).exec(stripCommentsForRegex(own, 'typescript'));
if (local) found = { type: local[1] ?? null, module: local[2]!, file };
}
if (!found) {
@@ -246,7 +247,7 @@ function nativeBinding(name: string, file: string, context: ResolutionContext):
const safe = stripCommentsForRegex(content, 'typescript');
const pattern = binding!.isDefault
? new RegExp(String.raw`\bexport\s+default\s+` + REQUIRE_NATIVE.source)
: new RegExp(String.raw`\bexport\s+(?:const|let)\s+${binding!.exportedName}\s*(?::[^=]+)?=\s*` + REQUIRE_NATIVE.source);
: new RegExp(String.raw`\bexport\s+(?:const|let)\s+${escapeRegExp(binding!.exportedName)}\s*(?::[^=]+)?=\s*` + REQUIRE_NATIVE.source);
const m = pattern.exec(safe);
if (m) found = { type: m[1] ?? null, module: m[2]!, file: target };
}
+6 -5
View File
@@ -11,6 +11,7 @@ import { dependsOn } from './package-deps';
import { resolveImportPath } from '../import-resolver';
import { defaultExportBinding, extractLocalExportAliases } from '../alias-binding';
import { stripCommentsForRegex } from '../strip-comments';
import { escapeRegExp } from '../../utils';
import { FN_KINDS, makeLineAt } from '../synth-utils';
import { isTestPath } from '../../search/query-utils';
import { matchBracket, skipString } from './object-literal';
@@ -316,7 +317,7 @@ function constantPathValue(expr: string, fromFile: string, context: ResolutionCo
if (!decl) return null;
const lines = context.readFile(file)?.split('\n') ?? [];
const text = lines.slice(decl.startLine - 1, decl.endLine).join('\n');
const open = text.indexOf('{', Math.max(0, text.search(new RegExp(`\\b${name}\\b`))));
const open = text.indexOf('{', Math.max(0, text.search(new RegExp(`\\b${escapeRegExp(name)}\\b`))));
return open < 0 ? null : readObjectPath(text, open, keys);
}
@@ -348,7 +349,7 @@ export function configHrefExpression(expr: string, fromFile: string, context: Re
if (!decl) return null;
const lines = context.readFile(file)?.split('\n') ?? [];
const text = lines.slice(decl.startLine - 1, decl.endLine).join('\n');
const open = text.indexOf('{', Math.max(0, text.search(new RegExp(`\\b${name}\\b`))));
const open = text.indexOf('{', Math.max(0, text.search(new RegExp(`\\b${escapeRegExp(name)}\\b`))));
if (open < 0) return null;
let value = readObjectValue(text, open, keys);
if (value === null) return null;
@@ -611,7 +612,7 @@ function ownExport(source: string, name: string): string | null {
if (name === 'default') {
const declared = defaultExportedName(source);
if (declared) return declared;
} else if (new RegExp(String.raw`\bexport\s+(?:async\s+)?(?:const|let|var|function|class)\s+${name.replace(/\$/g, '\\$')}(?![\w$])`).test(source)) {
} else if (new RegExp(String.raw`\bexport\s+(?:async\s+)?(?:const|let|var|function|class)\s+${escapeRegExp(name)}(?![\w$])`).test(source)) {
return name;
}
return extractLocalExportAliases(source).find((a) => a.exportedName === name)?.localName ?? null;
@@ -2112,7 +2113,7 @@ function exportedTable(file: string, exported: string, call: boolean, context: R
} else local = /\bexport\s*\{[^}]*?\b([A-Za-z_$][\w$]*)\s+as\s+default\b/.exec(safe)?.[1] ?? '';
} else {
// `export { appRoutes as routes }`
const alias = new RegExp(`\\bexport\\s*\\{[^}]*?\\b([A-Za-z_$][\\w$]*)\\s+as\\s+${exported.replace(/\$/g, '\\$')}\\b`).exec(safe);
const alias = new RegExp(`\\bexport\\s*\\{[^}]*?\\b([A-Za-z_$][\\w$]*)\\s+as\\s+${escapeRegExp(exported)}\\b`).exec(safe);
if (alias) local = alias[1]!;
}
// What a module exports is declared at its top level.
@@ -2143,7 +2144,7 @@ function declaredTable(
call: boolean,
visible: (keywords: number[]) => number | undefined
): TableSite[] {
const id = name.replace(/\$/g, '\\$');
const id = escapeRegExp(name);
const decls = [...safe.matchAll(new RegExp(`(?:^|[^\\w$.])((?:const|let|var)\\s+${id}\\s*(?::[^=;]*)?=\\s*)`, 'g'))];
const chosen = decls.length > 0 ? visible(decls.map((d) => d.index + d[0].length - d[1]!.length)) : undefined;
const decl = chosen === undefined ? undefined : decls[chosen];
+2 -1
View File
@@ -52,6 +52,7 @@ import type { QueryBuilder } from '../db/queries';
import type { ResolutionContext } from './types';
import type { MaybeYield } from './cooperative-yield';
import { enclosingFn } from './synth-utils';
import { escapeRegExp } from '../utils';
import { blankGoCode, goLocalDeclarationAt, GO_TYPE_KINDS } from './name-matcher';
/** One field of a Go struct, in declaration order. */
@@ -686,7 +687,7 @@ export async function goFuncFieldEdges(
// The literal's `{` is on its type's line: Go puts no line break between them.
const lineStart = f.lineStarts[lit.line - 1] ?? 0;
const lineText = f.text.slice(lineStart, f.lineStarts[lit.line] ?? f.text.length);
const typeRe = new RegExp(String.raw`(?<![\w.])(?:(${IDENT})\s*\.\s*)?${s.name}\s*(?:\[[^\]{}]*\])?\s*\{`, 'g');
const typeRe = new RegExp(String.raw`(?<![\w.])(?:(${IDENT})\s*\.\s*)?${escapeRegExp(s.name)}\s*(?:\[[^\]{}]*\])?\s*\{`, 'g');
let open = -1;
let nearest = Infinity;
for (let m = typeRe.exec(lineText); m; m = typeRe.exec(lineText)) {
+78 -76
View File
@@ -7,6 +7,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { Language, Node } from '../types';
import { escapeRegExp } from '../utils';
import { UnresolvedRef, ResolvedRef, ResolutionContext, ImportMapping, isSupertypeTarget, CPP_DEFINE_SIGNATURE, isInheritanceRef, isImportableKind } from './types';
import { blankStringContents, stripCommentsForRegex } from './strip-comments';
import { JS_ARRAY_METHODS, JS_BUILT_INS, JS_BUILTIN_METHODS, TS_PRIMITIVE_TYPES } from './js-builtins';
@@ -412,7 +413,7 @@ function pythonMembers(cls: Node, member: string, ref: UnresolvedRef, context: R
seen.add(cls.id);
// Instance assignments also shadow methods, even though they are not nodes.
const body = pythonMemberLines(cls.filePath, context).slice(cls.startLine - 1, cls.endLine).join('\n');
if (new RegExp(`^\\s*(?:(?:self|cls)\\.)?${member}\\s*(?:=|:)`, 'm').test(body)) return [cls];
if (new RegExp(`^\\s*(?:(?:self|cls)\\.)?${escapeRegExp(member)}\\s*(?:=|:)`, 'm').test(body)) return [cls];
const own = context.getNodesByQualifiedName(`${cls.qualifiedName}::${member}`).filter(n => n.filePath === cls.filePath);
if (own.length) return own;
return [...new Map(pythonBases(cls, ref, context).flatMap(p => pythonMembers(p, member, ref, context, seen)).map(n => [n.id, n])).values()];
@@ -563,7 +564,7 @@ function pythonLineBindings(lines: string[], index: number, statements: boolean[
return out;
}
if (!line.includes(name)) return out;
const word = new RegExp(`(?<![\\w.])${name}\\b(?!\\s*[.\\[])`);
const word = new RegExp(`(?<![\\w.])${escapeRegExp(name)}\\b(?!\\s*[.\\[])`);
if (!word.test(line)) return out;
const declared = line.match(/^\s*(global|nonlocal)\s+([\w\s,]+)$/);
if (declared) {
@@ -576,19 +577,19 @@ function pythonLineBindings(lines: string[], index: number, statements: boolean[
const assignment = pythonAssignment(line);
if (assignment) {
const target = assignment.targets.length === 1 && !assignment.augmented ? assignment.targets[0]!.trim() : '';
const annotated = target.match(new RegExp(`^${name}\\s*:\\s*["']?([\\w.]+)["']?$`));
const annotated = target.match(new RegExp(`^${escapeRegExp(name)}\\s*:\\s*["']?([\\w.]+)["']?$`));
if (target === name || annotated) {
out.push({ kind: 'assign', type: annotated?.[1] ?? null, value: assignment.value.trim(), line: index });
} else if (assignment.targets.some(t => word.test(t))) {
out.push({ kind: 'other' });
}
} else {
const annotated = line.match(new RegExp(`^\\s*${name}\\s*:\\s*["']?([\\w.]+)["']?\\s*$`));
const annotated = line.match(new RegExp(`^\\s*${escapeRegExp(name)}\\s*:\\s*["']?([\\w.]+)["']?\\s*$`));
if (annotated) out.push({ kind: 'assign', type: annotated[1]!, value: '', line: index });
else if (new RegExp(`^\\s*del\\b`).test(line)) out.push({ kind: 'other' });
}
}
if (new RegExp(`\\b${name}[ \\t]*:=|\\bas[ \\t]+${name}\\b`).test(line)) out.push({ kind: 'other' });
if (new RegExp(`\\b${escapeRegExp(name)}[ \\t]*:=|\\bas[ \\t]+${escapeRegExp(name)}\\b`).test(line)) out.push({ kind: 'other' });
for (const loop of line.matchAll(/\bfor\s+([^:]+?)\s+in\b/g)) if (word.test(loop[1]!)) out.push({ kind: 'other' });
for (const lambda of line.matchAll(/\blambda\b([^:]*):/g)) if (word.test(lambda[1]!)) out.push({ kind: 'other' });
return out;
@@ -603,7 +604,7 @@ function pythonLineBindings(lines: string[], index: number, statements: boolean[
function pythonBindsLocally(name: string, ref: UnresolvedRef, context: ResolutionContext, importsBind: boolean): boolean {
const lines = pythonMemberLines(ref.filePath, context);
const statements = pythonStatementStarts(ref.filePath, context);
const param = new RegExp(`[(,]\\s*\\*{0,2}${name}\\s*[:=,)]`);
const param = new RegExp(`[(,]\\s*\\*{0,2}${escapeRegExp(name)}\\s*[:=,)]`);
const scopes = context.getNodesInFile(ref.filePath).filter(n =>
(n.kind === 'function' || n.kind === 'method') && n.startLine <= ref.line && n.endLine >= ref.line)
.sort((a, b) => b.startLine - a.startLine);
@@ -785,7 +786,7 @@ function pythonModuleAliases(filePath: string, moduleFile: string, context: Reso
if (!files.includes(moduleFile)) continue;
// The mapping cannot tell `import a.b` from `import a.b as b`; the source line can.
// Exactly this module (not `other.a.b`), outside string literals.
const explicit = new RegExp(`\\bimport\\s[^\\n]*(?<![\\w.])${m.source.replace(/\./g, '\\.')}\\s+as\\s+${m.localName}\\b`);
const explicit = new RegExp(`\\bimport\\s[^\\n]*(?<![\\w.])${escapeRegExp(m.source)}\\s+as\\s+${escapeRegExp(m.localName)}\\b`);
const plainDotted = m.isNamespace && m.source.includes('.') && m.localName === m.source.split('.').pop() &&
!pythonMemberLines(filePath, context).some(line => explicit.test(blankPythonStrings(line)));
(files.length === 1 ? aliases : ambiguous).add(plainDotted ? m.source : m.localName);
@@ -858,9 +859,9 @@ function pythonExternalWrites(global: Node, context: ResolutionContext): { write
const spell = (names: string[]) => `(?:${names.map(escape).join('|')})`;
// Test files: any receiver (the double may sit on any spelling of the module).
const receiver = test ? '[\\w.]+' : spell([...aliases, ...ambiguous]);
const target = new RegExp(`(?<![\\w.])${receiver}\\.${name}\\b(?!\\s*[.\\[(])`);
const exact = new RegExp(`^${test ? receiver : spell(aliases.length ? aliases : ['\\0'])}\\.${name}(?:\\s*:[^=]*)?$`);
const dynamic = new RegExp(`\\b(?:setattr|patch\\.object)\\s*\\(\\s*${receiver}\\s*,\\s*['"]${name}['"]` +
const target = new RegExp(`(?<![\\w.])${receiver}\\.${escapeRegExp(name)}\\b(?!\\s*[.\\[(])`);
const exact = new RegExp(`^${test ? receiver : spell(aliases.length ? aliases : ['\\0'])}\\.${escapeRegExp(name)}(?:\\s*:[^=]*)?$`);
const dynamic = new RegExp(`\\b(?:setattr|patch\\.object)\\s*\\(\\s*${receiver}\\s*,\\s*['"]${escapeRegExp(name)}['"]` +
`|(?<![\\w.])${receiver}\\.__dict__\\s*(?:\\[|\\.\\s*update\\s*\\()`);
const lines = pythonMemberLines(file, context);
const statements = pythonStatementStarts(file, context);
@@ -1061,8 +1062,8 @@ function pythonConstructorCall(text: string): string | null {
function pythonFieldType(receiver: string, owner: Node, ref: UnresolvedRef, context: ResolutionContext): string | null {
const lines = pythonMemberLines(ref.filePath, context);
const field = receiver.split('.')[1]!;
const assignment = new RegExp(`^\\s*(?:self|cls)\\.${field}\\s*(?::\\s*["']?([\\w.]+)["']?)?\\s*=\\s*(.*)$`);
const annotation = new RegExp(`^\\s*(?:(?:self|cls)\\.)?${field}\\s*:\\s*["']?([\\w.]+)`);
const assignment = new RegExp(`^\\s*(?:self|cls)\\.${escapeRegExp(field)}\\s*(?::\\s*["']?([\\w.]+)["']?)?\\s*=\\s*(.*)$`);
const annotation = new RegExp(`^\\s*(?:(?:self|cls)\\.)?${escapeRegExp(field)}\\s*:\\s*["']?([\\w.]+)`);
const methods = context.getNodesInFile(ref.filePath).filter(n => n.kind === 'method' &&
n.qualifiedName.startsWith(`${owner.qualifiedName}::`));
const types = new Set<string>();
@@ -1998,7 +1999,7 @@ function luaLocalDecl(name: string, file: string, line: number, context: Resolut
.sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
if (fn && (!best || best.startLine < fn.startLine)) {
const lines = context.getFileLines?.(file) ?? context.readFile(file)?.split(/\r?\n/) ?? [];
const decl = new RegExp(`^\\s*local\\s+${name}\\s*=\\s*(.+?)\\s*$`);
const decl = new RegExp(`^\\s*local\\s+${escapeRegExp(name)}\\s*=\\s*(.+?)\\s*$`);
for (let at = line - 1; at > fn.startLine; at--) {
const m = decl.exec(lines[at - 1] ?? '');
if (!m) continue;
@@ -2093,7 +2094,7 @@ function luaMemberIn(file: string, path: string[], context: ResolutionContext, d
const source = (context.readFile(file) ?? '')
.replace(/--\[(=*)\[[\s\S]*?\]\1\]/g, (c) => c.replace(/[^\n]/g, ''))
.replace(/--[^\n]*/g, '');
const exported = new RegExp(`(?:^|[\\s{,.])${member}\\s*=\\s*([A-Za-z_]\\w*(?:\\s*\\.\\s*[A-Za-z_]\\w*)*)\\s*(?:[,;}]|$)`, 'm').exec(source);
const exported = new RegExp(`(?:^|[\\s{,.])${escapeRegExp(member)}\\s*=\\s*([A-Za-z_]\\w*(?:\\s*\\.\\s*[A-Za-z_]\\w*)*)\\s*(?:[,;}]|$)`, 'm').exec(source);
if (exported && exported[1] !== member) {
const rhs = exported[1]!;
const line = source.slice(0, exported.index).split('\n').length + 1;
@@ -3221,8 +3222,8 @@ function isJavaTypeVisible(candidate: Node, ref: UnresolvedRef, context: Resolut
const segs = candidate.qualifiedName.split('::');
const enumName = segs[segs.length - 2] ?? '';
const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1] ?? context.readFile(ref.filePath)?.split(/\r?\n/)[ref.line - 1] ?? '';
const name = ref.referenceName.replace(/\$/g, '\\$');
if (new RegExp(`\\bcase\\b[^:;]*\\b${name}\\b`).test(line) || new RegExp(`\\b${enumName}\\s*\\.\\s*${name}\\b`).test(line)) return true;
const name = escapeRegExp(ref.referenceName);
if (new RegExp(`\\bcase\\b[^:;]*\\b${name}\\b`).test(line) || new RegExp(`\\b${escapeRegExp(enumName)}\\s*\\.\\s*${name}\\b`).test(line)) return true;
const here = javaFileScope(ref.filePath, context);
const enumFqn = [javaFileScope(candidate.filePath, context).pkg, ...segs.slice(1, -1)].filter((p) => p !== '').join('.');
return here.single.has(`${enumFqn}.${ref.referenceName}`) || here.demand.has(enumFqn);
@@ -3238,9 +3239,9 @@ function isJavaTypeVisible(candidate: Node, ref: UnresolvedRef, context: Resolut
// inner class's `outer.new Inner()` — the qualifier says which: the owner
// (or the package) of this candidate.
const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1] ?? context.readFile(ref.filePath)?.split(/\r?\n/)[ref.line - 1] ?? '';
if (new RegExp(`\\.\\s*new\\s+${ref.referenceName.replace(/\$/g, '\\$')}\\b`).test(line)) return true;
if (new RegExp(`\\.\\s*new\\s+${escapeRegExp(ref.referenceName)}\\b`).test(line)) return true;
// (A type annotation may sit between them: jsoup's `Range.@Nullable Spans`.)
const qualifiers = [...line.matchAll(new RegExp(`([A-Za-z_$][\\w$.]*)\\s*\\.\\s*(?:@[\\w.]+(?:\\([^)]*\\))?\\s+)*${ref.referenceName.replace(/\$/g, '\\$')}\\b`, 'g'))].map((m) => m[1]!);
const qualifiers = [...line.matchAll(new RegExp(`([A-Za-z_$][\\w$.]*)\\s*\\.\\s*(?:@[\\w.]+(?:\\([^)]*\\))?\\s+)*${escapeRegExp(ref.referenceName)}\\b`, 'g'))].map((m) => m[1]!);
// Nested only inside a type the file declares; a class local to a method is
// the lexical rule's to judge.
const ownerQn = segs.slice(0, -1).join('::');
@@ -3304,7 +3305,7 @@ function isScalaPackageObjectMemberVisible(candidate: Node, ref: UnresolvedRef,
const here = scalaPackageClauses(text).join('.');
if (here === objectPkg || here.startsWith(`${objectPkg}.`)) return true;
const last = objectPkg.split('.').pop()!;
return new RegExp(`^\\s*import\\s+[^\\n]*\\b${last.replace(/\$/g, '\\$')}\\b`, 'm').test(text);
return new RegExp(`^\\s*import\\s+[^\\n]*\\b${escapeRegExp(last)}\\b`, 'm').test(text);
}
const SFC_SCRIPT_RANGES = new WeakMap<ResolutionContext, Map<string, Array<{ start: number; end: number; exported: boolean }>>>();
@@ -3575,7 +3576,7 @@ function isCollapsedSelfRecursion(chain: { root: string; links: string[] }, ref:
if (!cls) return false;
const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/) ?? [];
const body = lines.slice(cls.startLine - 1, cls.endLine).join('\n');
const f = field.replace(/\$/g, '\\$');
const f = escapeRegExp(field);
const declared = new RegExp(`(?:^|[\\s(,])#?${f}\\s*[?!]?\\s*:\\s*([A-Za-z_$][\\w$]*)`, 'm').exec(body)?.[1] ??
new RegExp(`\\bthis\\.${f}\\s*=\\s*new\\s+([A-Za-z_$][\\w$]*)`).exec(body)?.[1];
return declared === owner;
@@ -3890,7 +3891,7 @@ function pythonFixtureReturnType(receiver: string, ref: UnresolvedRef, context:
// The receiver must be the test's own parameter.
const signature = caller && (caller.kind === 'function' || caller.kind === 'method')
? lines.slice(caller.startLine - 1, caller.startLine + 4).join(' ').split(/\)\s*(?:->[^:]*)?:/)[0] ?? '' : '';
if (new RegExp(`[(,]\\s*${receiver}\\s*(?:[:=,)]|$)`).test(signature)) {
if (new RegExp(`[(,]\\s*${escapeRegExp(receiver)}\\s*(?:[:=,)]|$)`).test(signature)) {
const fixtures = context.getNodesByName(receiver)
.filter((n) => n.kind === 'function' && n.language === 'python' && isFixtureInReach(n, ref.filePath, context))
.sort((a, b) => (a.filePath === ref.filePath ? -1 : 0) - (b.filePath === ref.filePath ? -1 : 0) || b.filePath.length - a.filePath.length);
@@ -3974,7 +3975,7 @@ function pythonLocalBinding(name: string, ref: UnresolvedRef, context: Resolutio
PY_LOCAL_FILE.set(context, (file = { filePath: ref.filePath, lines, module: new Map() }));
}
const lines = file.lines;
const n = name;
const n = escapeRegExp(name);
const assigns = new RegExp(`^\\s*(?:[\\w\\s,*()\\[\\]]*,\\s*)?\\(?\\*?${n}\\)?\\s*(?:,[\\w\\s,*()\\[\\]]*)?(?::[^=]+)?=(?!=)`);
const targets = new RegExp(`\\bfor\\s+[\\w\\s,()]*\\b${n}\\b[\\w\\s,()]*\\s+in\\b|\\bas\\s+${n}\\b`);
const params = new RegExp(`[(,]\\s*\\*{0,2}${n}\\s*(?:[:=,)]|$)`);
@@ -4220,9 +4221,8 @@ function isDartConstructor(n: Node, context: ResolutionContext): boolean {
const owner = cut > 0 ? n.qualifiedName.slice(0, cut).split('::').pop()! : '';
const lines = context.getFileLines?.(n.filePath) ?? context.readFile(n.filePath)?.split(/\r?\n/) ?? [];
const head = (lines[n.startLine - 1] ?? '').slice(n.startColumn);
const escape = (name: string): string => name.replace(/\$/g, '\\$');
const constructor = owner !== '' &&
new RegExp(String.raw`^(?:(?:const|factory|external)\s+)*${escape(owner)}\s*\.\s*${escape(n.name)}(?![\w$])`).test(head);
new RegExp(String.raw`^(?:(?:const|factory|external)\s+)*${escapeRegExp(owner)}\s*\.\s*${escapeRegExp(n.name)}(?![\w$])`).test(head);
memo.set(n.id, constructor);
return constructor;
}
@@ -4439,8 +4439,8 @@ function dartTypeCallPrefix(typeName: string, member: string, ref: UnresolvedRef
if (prefixes.size === 0) return null;
const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1] ?? context.readFile(ref.filePath)?.split(/\r?\n/)[ref.line - 1];
if (line === undefined) return null;
const written = new RegExp(String.raw`(?<![\w$.])([A-Za-z_$][\w$]*)\s*\.\s*${typeName.replace(/\$/g, '\\$')}\s*` +
String.raw`(?:<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?\s*\.\s*${member.replace(/\$/g, '\\$')}(?![\w$])`, 'g');
const written = new RegExp(String.raw`(?<![\w$.])([A-Za-z_$][\w$]*)\s*\.\s*${escapeRegExp(typeName)}\s*` +
String.raw`(?:<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?\s*\.\s*${escapeRegExp(member)}(?![\w$])`, 'g');
// The one at the reference's column, else the first: a column counted in bytes misses after non-ASCII text.
const found = [...line.matchAll(written)].filter((m) => prefixes.has(m[1]!));
return (found.find((m) => m.index === ref.column) ?? found[0])?.[1] ?? null;
@@ -5182,7 +5182,7 @@ function dartNameUses(code: DartCode, filePath: string, name: string, context: R
const key = `${filePath}|${name}`;
const hit = memo.get(key);
if (hit) return hit;
const uses = [...code.text.matchAll(new RegExp(`(?<![\\w$.])${name.replace(/\$/g, '\\$')}(?![\\w$])`, 'g'))].map((m) => m.index!);
const uses = [...code.text.matchAll(new RegExp(`(?<![\\w$.])${escapeRegExp(name)}(?![\\w$])`, 'g'))].map((m) => m.index!);
if (memo.size >= 4096) memo.delete(memo.keys().next().value!);
memo.set(key, uses);
return uses;
@@ -5223,7 +5223,7 @@ function dartConstantType(constant: Node, ref: UnresolvedRef, context: Resolutio
const code = dartCodeOf(constant.filePath, context);
const from = code?.lineStarts[constant.startLine - 1];
if (!code || from === undefined) return null;
const head = new RegExp(`\\b(?:const|final|var)\\s+(?:([A-Za-z_$][\\w$.]*)\\s*(?:<[^;=]*>)?\\s*\\??\\s+)?${constant.name.replace(/\$/g, '\\$')}\\s*=(?![=>])`)
const head = new RegExp(`\\b(?:const|final|var)\\s+(?:([A-Za-z_$][\\w$.]*)\\s*(?:<[^;=]*>)?\\s*\\??\\s+)?${escapeRegExp(constant.name)}\\s*=(?![=>])`)
.exec(code.text.slice(from, from + 600));
if (!head) return null;
if (head[1] && !DART_NON_TYPE_WORDS.has(head[1])) return dartSimpleTypeName(head[1]);
@@ -5244,7 +5244,7 @@ function dartFieldType(typeName: string, name: string, ref: UnresolvedRef, conte
const declared = classMemberType(decl, name, context);
if (declared) return dartSimpleTypeName(declared);
// An abstract getter mints no node: `ProviderElement? get _listenedElement;`.
const getter = new RegExp(String.raw`(?:^|[\s;{}])([A-Za-z_$][\w$]*)\s*(?:<[^;{}=]*>)?\s*\??\s+get\s+${name.replace(/\$/g, '\\$')}\s*;`);
const getter = new RegExp(String.raw`(?:^|[\s;{}])([A-Za-z_$][\w$]*)\s*(?:<[^;{}=]*>)?\s*\??\s+get\s+${escapeRegExp(name)}\s*;`);
for (const { text } of classMemberLines(decl, context)) {
const m = text.includes(name) ? getter.exec(text) : null;
if (m && !DART_NON_TYPE_WORDS.has(m[1]!)) return dartSimpleTypeName(m[1]!);
@@ -5253,7 +5253,7 @@ function dartFieldType(typeName: string, name: string, ref: UnresolvedRef, conte
const from = code?.lineStarts[decl.startLine - 1];
const to = code?.lineStarts[decl.endLine];
if (!code || from === undefined) continue;
const init = new RegExp(`\\b(?:final|var|late)\\s+${name.replace(/\$/g, '\\$')}\\s*=(?![=>])`).exec(code.text.slice(from, to));
const init = new RegExp(`\\b(?:final|var|late)\\s+${escapeRegExp(name)}\\s*=(?![=>])`).exec(code.text.slice(from, to));
if (init) {
const site: UnresolvedRef = { ...ref, filePath: decl.filePath, line: decl.startLine, column: 0 };
return dartInitializerType(code.text, from + init.index + init[0].length, site, context, depth + 1);
@@ -5379,7 +5379,7 @@ function isDartGetter(n: Node, context: ResolutionContext): boolean {
const lines = context.getFileLines?.(n.filePath) ?? context.readFile(n.filePath)?.split(/\r?\n/) ?? [];
// The declaration from its start, and the next line for a return type that wraps.
const head = `${(lines[n.startLine - 1] ?? '').slice(n.startColumn)} ${lines[n.startLine] ?? ''}`;
const getter = new RegExp(String.raw`^[^={;]*?\bget\s+${n.name.replace(/\$/g, '\\$')}(?![\w$])`).test(head);
const getter = new RegExp(String.raw`^[^={;]*?\bget\s+${escapeRegExp(n.name)}(?![\w$])`).test(head);
memo.set(n.id, getter);
return getter;
}
@@ -5394,7 +5394,7 @@ function isDartSetter(n: Node, context: ResolutionContext): boolean {
const hit = memo.get(n.id);
if (hit !== undefined) return hit;
const line = (context.getFileLines?.(n.filePath) ?? context.readFile(n.filePath)?.split(/\r?\n/) ?? [])[n.startLine - 1] ?? '';
const setter = new RegExp(String.raw`^[^={;]*?\bset\s+${n.name.replace(/\$/g, '\\$')}\s*\(`).test(line.slice(n.startColumn));
const setter = new RegExp(String.raw`^[^={;]*?\bset\s+${escapeRegExp(n.name)}\s*\(`).test(line.slice(n.startColumn));
memo.set(n.id, setter);
return setter;
}
@@ -5678,7 +5678,7 @@ function rustGoCallShape(ref: UnresolvedRef, context: ResolutionContext): { shap
let line = lines?.[ref.line - 1];
if (line === undefined) return null;
const name = ref.referenceName;
const at = new RegExp(`(?<![\\w$])${name}\\s*(?:\\(|::<|!)`);
const at = new RegExp(`(?<![\\w$])${escapeRegExp(name)}\\s*(?:\\(|::<|!)`);
let start = line.startsWith(name, ref.column) ? ref.column : -1;
if (start < 0) {
const m = at.exec(line);
@@ -5981,13 +5981,13 @@ function kotlinChainReceiver(ref: UnresolvedRef, context: ResolutionContext): st
const name = ref.referenceName;
let start = line.startsWith(name, ref.column) ? ref.column : -1;
if (start < 0) {
const m = new RegExp(`(?<![\\w$])${name}\\s*[({]`).exec(line);
const m = new RegExp(`(?<![\\w$])${escapeRegExp(name)}\\s*[({]`).exec(line);
start = m ? m.index : -1;
}
if (start < 0) {
// An infix call on an expression — `alias(libs.x) apply false` — whose
// receiver the extractor could not name.
const infix = new RegExp(`(?<=[\\w)\\]"'}]\\s+)${name}\\s+(?=[^\\s=])`).exec(line);
const infix = new RegExp(`(?<=[\\w)\\]"'}]\\s+)${escapeRegExp(name)}\\s+(?=[^\\s=])`).exec(line);
return infix ? rustGoReceiverName(line.slice(0, infix.index).trimEnd()) : null;
}
const before = line.slice(0, start);
@@ -6017,7 +6017,7 @@ function isReceiverLessKotlinCall(ref: UnresolvedRef, context: ResolutionContext
const name = ref.referenceName;
let start = line.startsWith(name, ref.column) && !/[\w$]/.test(line[ref.column - 1] ?? '') ? ref.column : -1;
if (start < 0) {
const m = new RegExp(`(?<![\\w$])${name.replace(/\$/g, '\\$')}\\s*[({<]`).exec(line);
const m = new RegExp(`(?<![\\w$])${escapeRegExp(name)}\\s*[({<]`).exec(line);
start = m ? m.index : -1;
}
return start >= 0 && !/[.:]\s*$/.test(line.slice(0, start));
@@ -6371,7 +6371,7 @@ function swiftCallShape(ref: UnresolvedRef, context: ResolutionContext): SwiftCa
const name = ref.referenceName;
let start = line.startsWith(name, ref.column) && !/[\w$]/.test(line[ref.column - 1] ?? '') ? ref.column : -1;
if (start < 0) {
const m = new RegExp(`(?<![\\w$])${name}\\s*[({<[]`).exec(line);
const m = new RegExp(`(?<![\\w$])${escapeRegExp(name)}\\s*[({<[]`).exec(line);
start = m ? m.index : -1;
}
if (start < 0) return null;
@@ -6407,7 +6407,7 @@ function swiftDeclaresLabel(n: Node, label: string, context: ResolutionContext):
if (label === '' || SWIFT_STD_LABELS.has(label)) return false;
const lines = context.getFileLines?.(n.filePath) ?? context.readFile(n.filePath)?.split(/\r?\n/) ?? [];
const head = lines.slice(n.startLine - 1, n.startLine + 3).join(' ');
return new RegExp(`\\bfunc\\s+${n.name}\\s*(?:<[^>]*>)?\\s*\\(\\s*${label}\\b`).test(head);
return new RegExp(`\\bfunc\\s+${escapeRegExp(n.name)}\\s*(?:<[^>]*>)?\\s*\\(\\s*${escapeRegExp(label)}\\b`).test(head);
}
/**
@@ -6685,7 +6685,7 @@ function isScalaMemberInScope(n: Node, ref: UnresolvedRef, context: ResolutionCo
const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1] ?? context.readFile(ref.filePath)?.split('\n')[ref.line - 1];
if (line === undefined) return true;
const name = ref.referenceName;
const at = new RegExp(`(?<![\\w$])${name.replace(/[$]/g, '\\$')}\\b`).exec(line);
const at = new RegExp(`(?<![\\w$])${escapeRegExp(name)}\\b`).exec(line);
if (!at) return true;
const before = line.slice(0, at.index);
if (/\.\s*$/.test(before)) {
@@ -6807,7 +6807,7 @@ function scalaLocalBinder(name: string, ref: UnresolvedRef, context: ResolutionC
if (!scope) return null;
const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split('\n') ?? [];
const text = lines.slice(scope.startLine - 1, ref.line).join('\n');
const n = name.replace(/[$]/g, '\\$');
const n = escapeRegExp(name);
const binder = new RegExp(`(?:[(,\\[]\\s*(?:implicit\\s+|using\\s+)?${n}\\s*:)|(?:\\b(?:val|var|def|lazy\\s+val)\\s+${n}\\b)|(?:(?<![\\w$.])${n}\\s*(?:=>|<-))|(?:\\(\\s*${n}\\s*(?:,[^)]*)?\\)\\s*=>)`, 'g');
if (fn) return binder.test(text) ? fn : null;
// In a class body, a binder counts only inside a block still open at the site —
@@ -6986,7 +6986,7 @@ function csharpSupertypesOf(typeName: string, context: ResolutionContext): strin
else if (ch === '>' || ch === ')') depth = Math.max(0, depth - 1);
else if (depth === 0) flat += ch;
}
const bases = new RegExp(`\\b${typeName}\\s*:\\s*(.*?)(?:\\bwhere\\b|$)`).exec(flat)?.[1] ?? '';
const bases = new RegExp(`\\b${escapeRegExp(typeName)}\\s*:\\s*(.*?)(?:\\bwhere\\b|$)`).exec(flat)?.[1] ?? '';
for (const m of bases.matchAll(/([A-Za-z_][\w.]*)/g)) names.push(m[1]!.split('.').pop()!);
}
memo.set(typeName, names);
@@ -7233,7 +7233,7 @@ function objcCallShape(ref: UnresolvedRef, context: ResolutionContext): 'c-call'
if (line === undefined) return null;
const name = ref.referenceName.split(':')[0]!;
if (!name) return null;
const at = new RegExp(`(?<![\\w.])${name}\\b`, 'g');
const at = new RegExp(`(?<![\\w.])${escapeRegExp(name)}\\b`, 'g');
let m: RegExpExecArray | null;
let shape: 'c-call' | 'self-send' | 'super-send' | null = null;
while ((m = at.exec(line))) {
@@ -7353,7 +7353,7 @@ function objcReceiverReaches(receiver: string, method: Node, context: Resolution
/** The classes the `@property … Type *name` declarations of `name` give. */
function objcPropertyTypes(name: string, context: ResolutionContext): string[] {
const types = new Set<string>();
const decl = new RegExp(`@property\\s*(?:\\([^)]*\\)\\s*)?([A-Z]\\w*)\\s*(?:<[^;]*>\\s*)?\\*\\s*(?:_Nullable\\s+|_Nonnull\\s+)?${name}\\b`);
const decl = new RegExp(`@property\\s*(?:\\([^)]*\\)\\s*)?([A-Z]\\w*)\\s*(?:<[^;]*>\\s*)?\\*\\s*(?:_Nullable\\s+|_Nonnull\\s+)?${escapeRegExp(name)}\\b`);
for (const n of context.getNodesByName(name)) {
if (n.kind !== 'property' || n.language !== 'objc') continue;
const line = context.getFileLines?.(n.filePath)?.[n.startLine - 1] ?? context.readFile(n.filePath)?.split('\n')[n.startLine - 1] ?? '';
@@ -7504,7 +7504,7 @@ function objcClassFiles(name: string, ref: UnresolvedRef, context: ResolutionCon
if (hit) return hit;
const decls = context.getNodesByName(name).filter((n) => n.kind === 'class' && n.language === 'objc');
const primary = new Set<string>();
const own = new RegExp(`@interface\\s+${name}\\b(?!\\s*\\()`);
const own = new RegExp(`@interface\\s+${escapeRegExp(name)}\\b(?!\\s*\\()`);
for (const decl of decls) {
const lines = context.getFileLines?.(decl.filePath) ?? context.readFile(decl.filePath)?.split(/\r?\n/) ?? [];
if (lines.slice(decl.startLine - 1, decl.endLine).some((l) => own.test(l))) primary.add(decl.filePath);
@@ -8890,7 +8890,7 @@ export function isRustNameInScope(candidate: Node, ref: UnresolvedRef, context:
// whatever path the line writes elsewhere.
const pathed = line === undefined || viaSelf ? null
: (line.startsWith(name, ref.column) && /::\s*$/.test(line.slice(0, ref.column)) ? /((?:[A-Za-z_]\w*\s*::\s*)*)([A-Za-z_]\w*)?\s*::\s*$/.exec(line.slice(0, ref.column))
: !new RegExp(`(?<![\\w$:])${name}\\b`).test(line) ? new RegExp(`((?:[A-Za-z_]\\w*\\s*::\\s*)*)([A-Za-z_]\\w*)\\s*::\\s*${name}\\b`).exec(line) : null);
: !new RegExp(`(?<![\\w$:])${escapeRegExp(name)}\\b`).test(line) ? new RegExp(`((?:[A-Za-z_]\\w*\\s*::\\s*)*)([A-Za-z_]\\w*)\\s*::\\s*${escapeRegExp(name)}\\b`).exec(line) : null);
if (pathed) {
// Through a path: `crate::` / `self::` / `super::` look it up relatively;
// `io::Result` is the `io` module's — tokio's `runtime/task` alias is not —
@@ -8967,7 +8967,7 @@ export function isRustNameInScope(candidate: Node, ref: UnresolvedRef, context:
// `runtime::context::Context`. (A method call `.env(…)` is no imported name.)
if (uses.external.has(name) && !uses.bound.has(name) && line !== undefined) {
// Not on its line at all: a later link of a chain written across lines (`Arg::new(…)\n.env(…)`).
const at = new RegExp(`(?<![\\w$])${name}\\b`).exec(line.slice(Math.max(0, ref.column)));
const at = new RegExp(`(?<![\\w$])${escapeRegExp(name)}\\b`).exec(line.slice(Math.max(0, ref.column)));
if (at && !/\.\s*$/.test(line.slice(0, Math.max(0, ref.column) + at.index))) return false;
}
if (!RUST_PRELUDE.has(name)) {
@@ -9187,7 +9187,7 @@ function rustFirstArgIs(type: string, ref: UnresolvedRef, context: ResolutionCon
const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/) ?? [];
const method = ref.referenceName.slice(Math.max(ref.referenceName.lastIndexOf(':'), ref.referenceName.lastIndexOf('.')) + 1);
const text = [(lines[ref.line - 1] ?? '').slice(Math.max(0, ref.column)), ...lines.slice(ref.line, ref.line + 4)].join(' ');
const open = new RegExp(`\\b${method}\\s*\\(`).exec(text);
const open = new RegExp(`\\b${escapeRegExp(method)}\\s*\\(`).exec(text);
// Not where the reference says: nothing to read, so nothing to veto.
if (!open) return true;
let depth = 0;
@@ -9201,10 +9201,10 @@ function rustFirstArgIs(type: string, ref: UnresolvedRef, context: ResolutionCon
} else if (ch === ',' && depth === 0) break;
}
const arg = text.slice(open.index + open[0].length, end).trim().replace(/^(?:&\s*(?:mut\s+)?|\*)+/, '').replace(/\.clone\(\)$/, '');
if (new RegExp(`^(?:[A-Za-z_]\\w*::)*${type}\\b`).test(arg)) return true;
if (new RegExp(`^(?:[A-Za-z_]\\w*::)*${escapeRegExp(type)}\\b`).test(arg)) return true;
if (!/^[A-Za-z_]\w*$/.test(arg)) return false;
const declared = new RegExp(`\\b${arg}\\s*:\\s*&?\\s*(?:'\\w+\\s+)?(?:mut\\s+)?(?:[A-Za-z_]\\w*::)*${type}\\b`);
const built = new RegExp(`\\blet\\s+(?:mut\\s+)?${arg}\\s*=\\s*&?\\s*(?:[A-Za-z_]\\w*::)*${type}\\b`);
const declared = new RegExp(`\\b${arg}\\s*:\\s*&?\\s*(?:'\\w+\\s+)?(?:mut\\s+)?(?:[A-Za-z_]\\w*::)*${escapeRegExp(type)}\\b`);
const built = new RegExp(`\\blet\\s+(?:mut\\s+)?${arg}\\s*=\\s*&?\\s*(?:[A-Za-z_]\\w*::)*${escapeRegExp(type)}\\b`);
for (let i = ref.line - 1; i >= 0; i--) {
const line = lines[i] ?? '';
if (arg === 'self' && /^\s*(?:pub(?:\([^)]*\))?\s+)?(?:unsafe\s+)?impl\b/.test(line)) {
@@ -9510,7 +9510,7 @@ function isRustTypeParam(name: string, ref: UnresolvedRef, context: ResolutionCo
let from = open.index + open[0].length;
for (let i = from - 1; i < header.length; i++) {
const ch = header[i];
const param = (): boolean => new RegExp(`^\\s*(?:const\\s+)?${name}\\b`).test(header.slice(from, i));
const param = (): boolean => new RegExp(`^\\s*(?:const\\s+)?${escapeRegExp(name)}\\b`).test(header.slice(from, i));
if (ch === '<') depth++;
else if (ch === '>' && header[i - 1] !== '-' && --depth === 0) return param();
else if (ch === ',' && depth === 1) {
@@ -10312,7 +10312,7 @@ function rustIsAsyncFn(fn: Node, context: ResolutionContext): boolean {
if (!scopes) return false;
const start = rustNodeOffset(fn, context);
const head = scopes.code.slice(start, start + 300);
const name = head.search(new RegExp(`\\bfn\\s+(?:r#)?${fn.name.replace(/^r#/, '')}\\b`));
const name = head.search(new RegExp(`\\bfn\\s+(?:r#)?${escapeRegExp(fn.name.replace(/^r#/, ''))}\\b`));
return name >= 0 && /\basync\b/.test(head.slice(0, name));
}
@@ -10618,7 +10618,7 @@ function readRustDeclaredFields(decl: Node, context: ResolutionContext, generics
if (!scopes) return null;
const code = scopes.code;
const start = rustNodeOffset(decl, context);
const name = new RegExp(`\\b${decl.name}\\b`).exec(code.slice(start, start + 512));
const name = new RegExp(`\\b${escapeRegExp(decl.name)}\\b`).exec(code.slice(start, start + 512));
if (!name) return null;
let p = start + name.index + decl.name.length;
while (p < code.length && /\s/.test(code[p]!)) p++;
@@ -10854,7 +10854,7 @@ function rustHasBody(method: Node, context: ResolutionContext): boolean {
/** What `impl Deref for Name { type Target = … }` makes a value of the type `name` hand calls on to. */
function rustDerefTarget(name: string, typeFiles: Iterable<string>, context: ResolutionContext): RustWrittenType | null {
const header = new RegExp(String.raw`\bimpl\b\s*(?:<[^{;]*?>)?\s*(?:(?:std|core)\s*::\s*ops\s*::\s*|ops\s*::\s*)?Deref\s+for\s+(?:[\w:]+::)?${name}\b[^{;]*\{`, 'g');
const header = new RegExp(String.raw`\bimpl\b\s*(?:<[^{;]*?>)?\s*(?:(?:std|core)\s*::\s*ops\s*::\s*|ops\s*::\s*)?Deref\s+for\s+(?:[\w:]+::)?${escapeRegExp(name)}\b[^{;]*\{`, 'g');
for (const file of typeFiles) {
const code = rustScopesOf(file, context)?.code ?? '';
if (!code.includes('Deref')) continue;
@@ -10872,7 +10872,7 @@ function rustAliasTarget(alias: Node, context: ResolutionContext): RustWrittenTy
const scopes = rustScopesOf(alias.filePath, context);
if (!scopes) return null;
const start = rustNodeOffset(alias, context);
const m = new RegExp(String.raw`\btype\s+${alias.name}\b\s*(?:<([^=;]*?)>)?\s*=\s*([^;]+);`).exec(scopes.code.slice(Math.max(0, start - 64), start + 2000));
const m = new RegExp(String.raw`\btype\s+${escapeRegExp(alias.name)}\b\s*(?:<([^=;]*?)>)?\s*=\s*([^;]+);`).exec(scopes.code.slice(Math.max(0, start - 64), start + 2000));
return m ? { text: m[2]!.trim(), file: alias.filePath, at: start, generics: m[1] ?? '' } : null;
}
@@ -10890,7 +10890,7 @@ function rustTraitMethods(traits: Node[], method: string, context: ResolutionCon
// `trait Name<…>: Super + Other where … {`
const scopes = rustScopesOf(trait.filePath, context);
const start = rustNodeOffset(trait, context);
const header = scopes ? new RegExp(String.raw`\btrait\s+${trait.name}\b\s*(?:<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?\s*:([^{;]*?)(?:\bwhere\b[^{]*)?\{`).exec(scopes.code.slice(start, start + 2000)) : null;
const header = scopes ? new RegExp(String.raw`\btrait\s+${escapeRegExp(trait.name)}\b\s*(?:<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?\s*:([^{;]*?)(?:\bwhere\b[^{]*)?\{`).exec(scopes.code.slice(start, start + 2000)) : null;
if (!header) continue;
for (const bound of splitRustTopLevel(header[1]!, '+')) {
const head = rustPathHead(bound);
@@ -10932,7 +10932,7 @@ function rustDelegatesToOwn(method: Node, typeName: string, context: ResolutionC
if (!scopes) return false;
const start = scopes.lineStarts[method.startLine - 1] ?? 0;
const end = scopes.lineStarts[method.endLine ?? method.startLine] ?? scopes.code.length;
return new RegExp(String.raw`\b(?:Self|${typeName})\s*::\s*${method.name}\s*\(`).test(scopes.code.slice(start, end));
return new RegExp(String.raw`\b(?:Self|${escapeRegExp(typeName)})\s*::\s*${escapeRegExp(method.name)}\s*\(`).test(scopes.code.slice(start, end));
}
/**
@@ -11231,7 +11231,7 @@ export function jsCodeBindsName(name: string, fn: Node, filePath: string, line:
let binds = memo.get(key);
if (binds === undefined) {
const text = jsFunctionCodeThrough(fn, filePath, line, context);
const n = name.replace(/\$/g, '\\$');
const n = escapeRegExp(name);
// A plain declaration. Destructuring re-binds what a call returns under
// the same name — `const { t } = useI18n()`, `const { getLabel } =
// useProps(props)` — which is the same-named function more often than not.
@@ -11283,7 +11283,7 @@ function jsBindingEnclosesReference(name: string, fn: Node, ref: UnresolvedRef,
const chars = blankStringContents(cut, regexes).split('');
for (let r = 0; r + 1 < regexes.length; r += 2) chars.fill(' ', regexes[r]! + 1, regexes[r + 1]!);
const code = chars.join('');
const n = name.replace(/\$/g, '\\$');
const n = escapeRegExp(name);
const declaration = new RegExp(
`\\b(?:(?:const|let|var)\\s+${n}(?![\\w$])(?!\\s*[,\\]}])(\\s*=\\s*(?:await\\s+)?(?:require|import)\\s*\\()?|(?:function|class)\\s+${n}(?![\\w$]))`,
'g'
@@ -15936,6 +15936,8 @@ const CPP_CHAR_TYPES: ReadonlySet<string> = new Set(['char', 'wchar_t', 'char8_t
const CPP_NUMBER_TYPES: ReadonlySet<string> = new Set([
'int', 'unsigned', 'signed', 'short', 'long', 'float', 'double', 'size_t', 'ssize_t', 'ptrdiff_t',
'int8_t', 'int16_t', 'int32_t', 'int64_t', 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t', 'intptr_t', 'uintptr_t',
// A container's own integer aliases, by convention: nlohmann/json's `operator[](size_type idx)`.
'size_type', 'difference_type',
]);
const CPP_STRING_TYPES: ReadonlySet<string> = new Set(['string', 'wstring', 'string_view', 'wstring_view', 'basic_string', 'basic_string_view']);
@@ -16553,7 +16555,7 @@ function isCppConstructorThrough(namespace: string, method: Node, context: Resol
n.filePath === method.filePath && n.startLine <= method.startLine && (n.endLine ?? n.startLine) >= method.startLine);
const lines = holder ? context.getFileLines?.(holder.filePath) ?? context.readFile(holder.filePath)?.split(/\r?\n/) : undefined;
const head = holder && lines ? lines.slice(holder.startLine - 1, holder.startLine + 1).join(' ') : '';
return new RegExp(String.raw`\b(?:class|struct|union)\s+(?:[A-Za-z_]\w*\s+)*${method.name}\s*<`).test(head);
return new RegExp(String.raw`\b(?:class|struct|union)\s+(?:[A-Za-z_]\w*\s+)*${escapeRegExp(method.name)}\s*<`).test(head);
}
/** The names a C++ file makes something of itself, which the index holds no node for (see cppNamesBroughtIn). */
@@ -18090,7 +18092,7 @@ function bindsNameItself(fn: Node, name: string, context: ResolutionContext): bo
if (hit !== undefined) return hit;
const lines = context.getFileLines?.(fn.filePath) ?? context.readFile(fn.filePath)?.split(/\r?\n/) ?? [];
const body = lines.slice(fn.startLine - 1, fn.endLine).join('\n');
const r = name.replace(/\$/g, '\\$');
const r = escapeRegExp(name);
const binds = new RegExp(`\\b(?:var|val|let|out\\s+[\\w.<>?]+|foreach\\s*\\(\\s*[\\w.<>?,\\s]+?)\\s+${r}\\b|\\bfor\\s*\\([^;)]*\\s${r}\\s*:|\\b${r}\\s*=>|[(,]\\s*${r}\\s*(?:,[^()]*)?\\)\\s*=>|\\b${r}\\s*(?:,[^{}]*)?->`).test(body);
memo.set(key, binds);
return binds;
@@ -18169,7 +18171,7 @@ function angleArguments(text: string): string[] {
function declaredTypeParameters(decl: Node, context: ResolutionContext): string[] {
const lines = context.getFileLines?.(decl.filePath) ?? context.readFile(decl.filePath)?.split(/\r?\n/) ?? [];
const head = lines.slice(decl.startLine - 1, decl.startLine + 3).join(' ');
const at = new RegExp(`\\b${decl.name}\\s*<`).exec(head);
const at = new RegExp(`\\b${escapeRegExp(decl.name)}\\s*<`).exec(head);
if (!at) return [];
return angleArguments(head.slice(at.index)).map((p) => /([A-Za-z_]\w*)\s*(?:extends\b.*|:.*)?$/.exec(p.replace(/^(?:in|out|reified)\s+/, ''))?.[1] ?? '');
}
@@ -18178,7 +18180,7 @@ function declaredTypeParameters(decl: Node, context: ResolutionContext): string[
function headTypeArguments(cls: Node, sup: string, context: ResolutionContext): string[] {
const lines = context.getFileLines?.(cls.filePath) ?? context.readFile(cls.filePath)?.split(/\r?\n/) ?? [];
const head = lines.slice(cls.startLine - 1, cls.startLine + 8).join(' ').split('{')[0]!;
const at = new RegExp(`(?:[:,]|\\bextends|\\bimplements)\\s*(?:[\\w.]+\\.)?${sup}\\s*<`).exec(head);
const at = new RegExp(`(?:[:,]|\\bextends|\\bimplements)\\s*(?:[\\w.]+\\.)?${escapeRegExp(sup)}\\s*<`).exec(head);
if (!at) return [];
return angleArguments(head.slice(at.index)).map((a) => a.replace(/<[\s\S]*$/, '').split('.').pop()!.trim());
}
@@ -18318,7 +18320,7 @@ function classMemberType(cls: Node, name: string, context: ResolutionContext): s
}
const key = `${cls.id}|${name}`;
if (memo.has(key)) return memo.get(key)!;
const r = name.replace(/\$/g, '\\$');
const r = escapeRegExp(name);
const pattern = cls.language === 'kotlin' || cls.language === 'swift'
? new RegExp(`\\b(?:val|var|let)\\s+${r}\\s*:\\s*([A-Z][\\w.]*)`)
: new RegExp(`(?:^|[\\s(,])([A-Za-z_][\\w.]*)\\s*${TYPE_ARGS}\\??\\s+${r}\\s*(?:[=;,)]|\\{)`);
@@ -19839,7 +19841,7 @@ function isVueComponentMethod(n: Node): boolean {
function isThisCallInOwnFile(n: Node, ref: UnresolvedRef, context: ResolutionContext): boolean {
if (n.filePath !== ref.filePath) return false;
const line = (context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/))?.[ref.line - 1] ?? '';
return new RegExp(String.raw`\bthis\s*\??\.\s*${n.name.replace(/\$/g, '\\$')}\s*\(`).test(line);
return new RegExp(String.raw`\bthis\s*\??\.\s*${escapeRegExp(n.name)}\s*\(`).test(line);
}
/** Is the root of a member call's receiver (`CameraManager` in `CameraManager.x`) one of the file's imports? */
@@ -22002,7 +22004,7 @@ function pythonClassBaseNames(cls: Node, context: ResolutionContext): string[] {
const lines = pythonLines(context, cls.filePath);
if (!lines) return [];
const header = lines.slice(cls.startLine - 1, cls.startLine + 4).join(' ');
const bases = new RegExp(`\\bclass\\s+${cls.name}\\s*\\(([^)]*)\\)`).exec(header)?.[1];
const bases = new RegExp(`\\bclass\\s+${escapeRegExp(cls.name)}\\s*\\(([^)]*)\\)`).exec(header)?.[1];
return (bases ?? '')
.split(',')
.map((b) => b.trim())
@@ -22671,7 +22673,7 @@ function matchRustSelfCall(
// `(self.into_route)(state)` calls what a field holds, no method: axum's
// `MakeErasedHandler::into_route` went to itself.
const at = (context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/))?.[ref.line - 1]?.slice(ref.column);
if (at?.startsWith('(') && new RegExp(`^\\(\\s*self\\s*\\.\\s*${methodName}\\s*\\)`).test(at)) return null;
if (at?.startsWith('(') && new RegExp(`^\\(\\s*self\\s*\\.\\s*${escapeRegExp(methodName)}\\s*\\)`).test(at)) return null;
// In a trait impl, `fn wake(self) { self.wake() }` hands the call on to the
// type's own `wake`: its inherent one, or for a type from outside the
// project one the index doesn't hold (tokio's `impl WakerRef for Waker`) —
@@ -22858,7 +22860,7 @@ function matchRustAsMutChainCall(ref: UnresolvedRef, context: ResolutionContext)
// The call's expression starts at its column with the whole chain:
// `self.delay.as_mut().reset(` — `?` after `as_mut()` taking what an
// `Option` / `Result` holds back out (`self.client.as_mut()?.next()`).
const lent = new RegExp(`^(self|[a-z_]\\w*)(?:\\s*\\.\\s*([a-z_]\\w*))?\\s*\\.\\s*as_(?:mut|ref)\\s*\\(\\s*\\)\\s*(\\?)?\\s*\\.\\s*${name}\\s*(?:\\(|::<)`)
const lent = new RegExp(`^(self|[a-z_]\\w*)(?:\\s*\\.\\s*([a-z_]\\w*))?\\s*\\.\\s*as_(?:mut|ref)\\s*\\(\\s*\\)\\s*(\\?)?\\s*\\.\\s*${escapeRegExp(name)}\\s*(?:\\(|::<)`)
.exec(line.slice(Math.max(0, ref.column)));
if (!lent) return undefined;
const [, base, field, question] = lent;
@@ -22942,7 +22944,7 @@ function rustCallOnType(type: string, name: string, ref: UnresolvedRef, context:
function rustProjectsSelf(name: string, ref: UnresolvedRef, context: ResolutionContext): boolean {
const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/) ?? [];
const start = Math.max(0, enclosingScopeStartLine(ref, context) - 1);
const bound = new RegExp(`\\blet\\s+(?:mut\\s+)?${name}\\b\\s*=\\s*(.*)$`);
const bound = new RegExp(`\\blet\\s+(?:mut\\s+)?${escapeRegExp(name)}\\b\\s*=\\s*(.*)$`);
for (let i = Math.min(lines.length, ref.line) - 1; i >= start; i--) {
const m = bound.exec(lines[i] ?? '');
if (m) return /^self\s*(?:\.\s*as_mut\s*\(\s*\)\s*)?\.\s*project\s*\(\s*\)/.test(m[1]!);
@@ -22964,7 +22966,7 @@ function rustFieldDeclaration(owner: string, field: string, ref: UnresolvedRef,
.map((s) => fileLines(s.filePath).slice(s.startLine - 1, s.endLine));
if (spans.length === 0) {
const lines = fileLines(ref.filePath);
const header = new RegExp(`\\bstruct\\s+${owner}\\b`);
const header = new RegExp(`\\bstruct\\s+${escapeRegExp(owner)}\\b`);
const at = lines.findIndex((l) => header.test(l.replace(/\/\/.*$/, '')));
if (at >= 0) {
let depth = 0;
@@ -22978,7 +22980,7 @@ function rustFieldDeclaration(owner: string, field: string, ref: UnresolvedRef,
spans.push(lines.slice(at, end + 1));
}
}
const declared = new RegExp(`(?:^|[\\s{(,])(?:pub(?:\\([^)]*\\))?\\s+)?${field}\\s*:(?!:)\\s*(.*)$`);
const declared = new RegExp(`(?:^|[\\s{(,])(?:pub(?:\\([^)]*\\))?\\s+)?${escapeRegExp(field)}\\s*:(?!:)\\s*(.*)$`);
for (const span of spans) {
for (let i = 0; i < span.length; i++) {
const line = (span[i] ?? '').replace(/\/\/.*$/, '');
@@ -23649,7 +23651,7 @@ function tsIndexedPropertyType(
function tsClassTypeParameter(cls: Node, name: string, context: ResolutionContext): boolean {
const lines = context.getFileLines?.(cls.filePath) ?? context.readFile(cls.filePath)?.split(/\r?\n/) ?? [];
const head = lines.slice(cls.startLine - 1, cls.startLine + 2).join(' ').split('{')[0]!;
const open = new RegExp(`\\b${cls.name.replace(/\$/g, '\\$')}\\s*<`).exec(head);
const open = new RegExp(`\\b${escapeRegExp(cls.name)}\\s*<`).exec(head);
if (!open) return false;
// The parameters between the `<` after the name and its own `>`, split
// at their top level: `T`, `K extends keyof T = never`.
@@ -24344,7 +24346,7 @@ function destructuredKeyTarget(calleeName: string, key: string, ref: UnresolvedR
if (!callee || !sameLanguageFamily(callee.language, ref.language)) return null;
const calleeText = (context.getFileLines?.(callee.filePath) ?? context.readFile(callee.filePath)?.split('\n') ?? [])
.slice(callee.startLine - 1, callee.endLine).join('\n');
if (!new RegExp(`\\breturn\\s*\\{[^]*?\\b${key}\\b`).test(calleeText)) return null;
if (!new RegExp(`\\breturn\\s*\\{[^]*?\\b${escapeRegExp(key)}\\b`).test(calleeText)) return null;
const callable = (n: Node) => n.kind === 'function' || n.kind === 'method' || n.kind === 'constant' || n.kind === 'variable';
const inFile = context.getNodesInFile(callee.filePath);
const inner = inFile.filter((n) => n.name === key && callable(n) && n.id !== callee.id && rangeWithin(n, callee) &&
@@ -24943,7 +24945,7 @@ export function matchReference(
function otherSupertypeNamed(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
const name = ref.referenceName.split(/::|\./).pop()!;
const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1] ?? context.readFile(ref.filePath)?.split(/\r?\n/)[ref.line - 1] ?? '';
const written = new RegExp(`^\\s*((?:[\\w$]+\\.)+)${name.replace(/\$/g, '\\$')}\\b`).exec(line.slice(Math.max(0, ref.column)));
const written = new RegExp(`^\\s*((?:[\\w$]+\\.)+)${escapeRegExp(name)}\\b`).exec(line.slice(Math.max(0, ref.column)));
const qualifier = written ? written[1]!.slice(0, -1) : '';
const candidates = context.getNodesByName(name).filter((n) =>
n.id !== ref.fromNodeId && isSupertypeTarget(n) && sameLanguageFamily(n.language, ref.language) &&
@@ -25570,7 +25572,7 @@ function cppParenListAfter(file: string, line: number, column: number, name: str
/** The `( … )` that follows `name` in `text`, from `column` on, without its parentheses. */
function cppParenListIn(text: string, column: number, name: string): string | null {
const at = new RegExp(`\\b${name.replace(/[~]/g, '\\$&')}\\s*(?:<[^<>()]*>)?\\s*\\(`).exec(text.slice(column));
const at = new RegExp(`\\b${escapeRegExp(name)}\\s*(?:<[^<>()]*>)?\\s*\\(`).exec(text.slice(column));
if (!at) return null;
const open = column + at.index + at[0].length - 1;
let depth = 0;
@@ -25640,7 +25642,7 @@ function cppOverloadParameters(n: Node, name: string, context: ResolutionContext
.filter((p) => p !== 'void')
.map((p) => normalizeCppTypeName(cppParameterType(p)) ?? p);
const want = types(own);
const word = new RegExp(`\\b${name.replace(/[~]/g, '\\$&')}\\s*\\(`);
const word = new RegExp(`\\b${escapeRegExp(name)}\\s*\\(`);
search: for (const cls of classes) {
const lines = context.getFileLines?.(cls.filePath) ?? context.readFile(cls.filePath)?.split(/\r?\n/) ?? [];
for (let line = cls.startLine; line <= Math.min(lines.length, cls.endLine ?? cls.startLine); line++) {
+3 -2
View File
@@ -33,6 +33,7 @@ import type { Edge, Language } from '../types';
import type { ResolutionContext } from './types';
import type { MaybeYield } from './cooperative-yield';
import { stripCommentsForRegex } from './strip-comments';
import { escapeRegExp } from '../utils';
import { isTestPath } from '../search/query-utils';
import { parseHrefExpression, routesForFile, toHref, type HrefLiteral } from './frameworks/expo-router';
import { matchBracket } from './frameworks/object-literal';
@@ -50,7 +51,7 @@ const LINK_TAGS = ['Link', 'NavLink', 'Navigate', 'Redirect', 'LinkContainer', '
/** `<Tag … to=…>` for any of `tags`, the attribute anywhere in the tag. */
function linkTagPattern(tags: readonly string[]): RegExp {
// An attribute before `to` may hold an arrow (`onMouseEnter={() => …}`), whose `>` is not the tag's end.
return new RegExp(`<(${tags.map((t) => t.replace(/[$]/g, '\\$&')).join('|')})\\b((?:[^>]|=>)*?)\\bto\\s*=\\s*(?:"([^"]*)"|'([^']*)'|(?=\\{))`, 'g');
return new RegExp(`<(${tags.map(escapeRegExp).join('|')})\\b((?:[^>]|=>)*?)\\bto\\s*=\\s*(?:"([^"]*)"|'([^']*)'|(?=\\{))`, 'g');
}
const LINK_TAG = linkTagPattern(LINK_TAGS);
@@ -131,7 +132,7 @@ export async function reactRouterLinkEdges(ctx: ResolutionContext, onYield: Mayb
if (!source || !/\bto\s*=/.test(source)) continue;
const tags = linkTagsFor(file, wrappers, ctx);
const pattern = tags.length === LINK_TAGS.length ? LINK_TAG : linkTagPattern(tags);
if (!new RegExp(`<(?:${tags.join('|')})\\b`).test(source)) continue;
if (!new RegExp(`<(?:${tags.map(escapeRegExp).join('|')})\\b`).test(source)) continue;
const safe = stripCommentsForRegex(source, 'typescript');
const nodes = ctx.getNodesInFile(file);
const lineOf = makeLineAt(safe, 1);
+2 -1
View File
@@ -27,6 +27,7 @@
*/
import type { Node } from '../types';
import type { ResolutionContext, ResolvedRef, UnresolvedRef } from './types';
import { escapeRegExp } from '../utils';
const TYPE_KINDS: ReadonlySet<Node['kind']> = new Set<Node['kind']>(['class', 'struct', 'enum', 'interface', 'protocol', 'type_alias']);
@@ -229,7 +230,7 @@ function writtenName(ref: UnresolvedRef, context: ResolutionContext): string {
const before = QUALIFIER_BEFORE.exec(text.slice(Math.max(0, column - 240), column));
return before ? clean(before[1]!) : name;
}
const chain = new RegExp(String.raw`^((?:${QUALIFIER_SEGMENT})+)${name.replace(/[$\\]/g, '\\$&')}\b`).exec(text.slice(ref.column, ref.column + 240));
const chain = new RegExp(String.raw`^((?:${QUALIFIER_SEGMENT})+)${escapeRegExp(name)}\b`).exec(text.slice(ref.column, ref.column + 240));
return chain ? clean(chain[1]!) : name;
}
+2 -1
View File
@@ -16,6 +16,7 @@
*/
import type { Node } from '../types';
import type { ResolutionContext, ResolvedRef, UnresolvedRef } from './types';
import { escapeRegExp } from '../utils';
const GENERIC_LANGUAGES: ReadonlySet<string> = new Set([
'scala', 'java', 'kotlin', 'csharp', 'typescript', 'tsx', 'swift', 'rust', 'dart', 'go', 'cpp', 'vbnet', 'arkts',
@@ -143,7 +144,7 @@ function scalaMemberInScope(result: ResolvedRef, ref: UnresolvedRef, context: Re
// Imported: `import cats.data.FreeT.FlatMapped._`, `Owner.{A, B}`, `Owner.*`.
const text = context.readFile(ref.filePath) ?? '';
const imports = new RegExp(
String.raw`\bimport\s+[\w.]*\b${owner.name}\.(?:_|\*|given\b|\{[^}]*\b(?:_|\*|${target.name})\b[^}]*\}|${target.name}\b)`,
String.raw`\bimport\s+[\w.]*\b${escapeRegExp(owner.name)}\.(?:_|\*|given\b|\{[^}]*\b(?:_|\*|${escapeRegExp(target.name)})\b[^}]*\}|${escapeRegExp(target.name)}\b)`,
);
return /^[A-Za-z_]\w*$/.test(owner.name) && /^[A-Za-z_]\w*$/.test(target.name) ? imports.test(text) : true;
}
+2 -1
View File
@@ -35,6 +35,7 @@ import { resolveProjectFile } from '../security';
import { findIndexedFile, hasDriftedOnDisk } from './source';
import { createWhenReader } from './when';
import { toNodeRef, type WireNodeRef } from './wire';
import { escapeRegExp } from '../../utils';
// =============================================================================
// Wire shapes
@@ -586,7 +587,7 @@ function mentionsOf(cg: CodeGraph, projectRoot: string, value: Node): Edge[] {
.slice(0, MAX_MENTION_FILES);
if (importers.length === 0) return out;
const files = cg.getNodesByIds(importers);
const word = new RegExp(`(?<![\\w$.])${value.name.replace(/\$/g, '\\$')}(?![\\w$])`);
const word = new RegExp(`(?<![\\w$.])${escapeRegExp(value.name)}(?![\\w$])`);
for (const file of files.values()) {
if (file.kind !== 'file') continue;
const found = findIndexedFile(cg, file.filePath.replace(/\\/g, '/'));
+2 -1
View File
@@ -50,6 +50,7 @@ import { nextRouteForFile } from '../../resolution/frameworks/nextjs';
import { splitRouteName } from './routes';
import { HUB_THRESHOLD, UNCERTAIN_BELOW, toNodeRef, type WireNodeRef } from './wire';
import { isTestPath } from '../../search/query-utils';
import { escapeRegExp } from '../../utils';
// =============================================================================
// Wire shapes
@@ -507,7 +508,7 @@ export async function buildSteps(cg: CodeGraph, projectRoot: string, query: URLS
if ((n.kind !== 'field' && n.kind !== 'property' && n.kind !== 'variable' && n.kind !== 'parameter') || !n.signature) continue;
const sig = n.signature.replace(/\s+/g, ' ').trim();
// `OwnerRepository owners`, `private final OwnerRepository owners`, `owners: OwnerRepository`, `val owners: OwnerRepository`.
const typed = new RegExp(`(?:^|\\s)([A-Z][\\w<>,?. ]*?)\\s+${n.name}\\b`).exec(sig) ?? new RegExp(`\\b${n.name}\\s*:\\s*([A-Z][\\w<>,?. ]*)`).exec(sig);
const typed = new RegExp(`(?:^|\\s)([A-Z][\\w<>,?. ]*?)\\s+${escapeRegExp(n.name)}\\b`).exec(sig) ?? new RegExp(`\\b${escapeRegExp(n.name)}\\s*:\\s*([A-Z][\\w<>,?. ]*)`).exec(sig);
if (typed && !types.has(n.name)) types.set(n.name, typed[1]!.trim());
}
fileTypes.set(caller.filePath, types);
+11
View File
@@ -231,6 +231,17 @@ export function normalizePath(filePath: string): string {
return filePath.replace(/\\/g, '/');
}
/**
* Escape `text` to match itself literally inside a regular expression.
* Symbol names are not always plain words: C++'s `operator++` and
* `operator()`, Scala's `++`, a Kotlin name in backticks. Pass every name
* through this before writing it into a `new RegExp(…)` pattern, or the name
* is read as pattern syntax and may not compile at all.
*/
export function escapeRegExp(text: string): string {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Cross-process file lock using a lock file with PID tracking.
*