mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-23 06:50:54 +08:00
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:
co-authored by
Claude Opus 5.5
parent
1ef8866aae
commit
6bd217a263
@@ -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++, 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 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.
|
- 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 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++, 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.
|
- 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.
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
@@ -34,6 +34,7 @@ import type { Node as SyntaxNode, Tree } from 'web-tree-sitter';
|
|||||||
import type { Language } from '../types';
|
import type { Language } from '../types';
|
||||||
import { getParser, loadGrammarsForLanguages } from '../extraction/grammars';
|
import { getParser, loadGrammarsForLanguages } from '../extraction/grammars';
|
||||||
import { parseWithinBudget } from '../extraction/parse-budget';
|
import { parseWithinBudget } from '../extraction/parse-budget';
|
||||||
|
import { escapeRegExp } from '../utils';
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Public shape
|
// Public shape
|
||||||
@@ -576,7 +577,7 @@ function statusSetBefore(call: SyntaxNode, callee: string): number | null {
|
|||||||
const receiver = bare.split('.')[0]!;
|
const receiver = bare.split('.')[0]!;
|
||||||
let statement: SyntaxNode | null = call;
|
let statement: SyntaxNode | null = call;
|
||||||
while (statement.parent && !STATEMENT_BLOCKS.has(statement.parent.type)) statement = statement.parent;
|
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;
|
let prev: SyntaxNode | null = statement.previousNamedSibling;
|
||||||
for (let i = 0; prev && i < STATUS_LOOKBACK; i++, prev = prev.previousNamedSibling) {
|
for (let i = 0; prev && i < STATUS_LOOKBACK; i++, prev = prev.previousNamedSibling) {
|
||||||
if (prev.type === 'comment') continue;
|
if (prev.type === 'comment') continue;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
import type { Node } from '../types';
|
import type { Node } from '../types';
|
||||||
import type { ResolutionContext } from './types';
|
import type { ResolutionContext } from './types';
|
||||||
import { directiveLines } from './cpp-macro-visibility';
|
import { directiveLines } from './cpp-macro-visibility';
|
||||||
|
import { escapeRegExp } from '../utils';
|
||||||
|
|
||||||
/** Three-valued: true / false / undefined = the build may go either way. */
|
/** Three-valued: true / false / undefined = the build may go either way. */
|
||||||
type Truth = boolean | undefined;
|
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;
|
if (name.startsWith('_') || PLATFORM_FLAGS.has(name) || file.defines.has(name)) return undefined;
|
||||||
let named = memo.named.get(name);
|
let named = memo.named.get(name);
|
||||||
// A word of its own, or a compiler flag's `-DX` / `/DX`.
|
// 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;
|
if (dirs.some((dir) => named!.test(buildTextOf(dir, context, memo)))) return undefined;
|
||||||
const components = new Set(scope.map((f) => componentOf(dirOf(f), context, memo)));
|
const components = new Set(scope.map((f) => componentOf(dirOf(f), context, memo)));
|
||||||
return [...components].some((component) => isDefinedIn(name, component, context, memo)) ? undefined : false;
|
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;
|
if (hit !== undefined) return hit;
|
||||||
memo.cFiles ??= context.getAllFiles().filter((file) => C_FAMILY_FILE.test(file));
|
memo.cFiles ??= context.getAllFiles().filter((file) => C_FAMILY_FILE.test(file));
|
||||||
const prefix = component ? `${component}/` : '';
|
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) =>
|
const defined = memo.cFiles.some((file) =>
|
||||||
file.startsWith(prefix) &&
|
file.startsWith(prefix) &&
|
||||||
(context.fileContains ? context.fileContains(file, name) : true) &&
|
(context.fileContains ? context.fileContains(file, name) : true) &&
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import type { QueryBuilder } from '../db/queries';
|
|||||||
import type { ResolutionContext } from './types';
|
import type { ResolutionContext } from './types';
|
||||||
import { isGeneratedFile } from '../extraction/generated-detection';
|
import { isGeneratedFile } from '../extraction/generated-detection';
|
||||||
import { stripCommentsForRegex } from './strip-comments';
|
import { stripCommentsForRegex } from './strip-comments';
|
||||||
|
import { escapeRegExp } from '../utils';
|
||||||
import { cFnPointerDispatchEdges } from './c-fnptr-synthesizer';
|
import { cFnPointerDispatchEdges } from './c-fnptr-synthesizer';
|
||||||
import { goframeRouteEdges } from './goframe-synthesizer';
|
import { goframeRouteEdges } from './goframe-synthesizer';
|
||||||
import { goFuncFieldEdges } from './go-func-field-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
|
(d) => d.node.filePath === reg.node.filePath && d.field === reg.field
|
||||||
);
|
);
|
||||||
if (chDispatchers.length === 0) continue;
|
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;
|
let added = 0;
|
||||||
for (const e of queries.getIncomingEdges(reg.node.id, ['calls'])) {
|
for (const e of queries.getIncomingEdges(reg.node.id, ['calls'])) {
|
||||||
if (added >= MAX_CALLBACKS_PER_CHANNEL) break;
|
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.
|
* a parameter or local of a function elsewhere in the file binds nothing here.
|
||||||
*/
|
*/
|
||||||
function ownLinesBind(name: string, src: string): boolean {
|
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);
|
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);
|
const from = importedFile(owner);
|
||||||
decls = from
|
decls = from
|
||||||
? declaredIn(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));
|
const values = new Set(decls.map(read).filter((v): v is string => v !== null));
|
||||||
value = values.size === 1 ? [...values][0]! : null;
|
value = values.size === 1 ? [...values][0]! : null;
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import type { Node } from '../../types';
|
|||||||
import type { FrameworkExtractionResult, FrameworkResolver, ResolutionContext, ResolvedRef, UnresolvedRef } from '../types';
|
import type { FrameworkExtractionResult, FrameworkResolver, ResolutionContext, ResolvedRef, UnresolvedRef } from '../types';
|
||||||
import { resolveImportPath } from '../import-resolver';
|
import { resolveImportPath } from '../import-resolver';
|
||||||
import { stripCommentsForRegex } from '../strip-comments';
|
import { stripCommentsForRegex } from '../strip-comments';
|
||||||
|
import { escapeRegExp } from '../../utils';
|
||||||
import { matchBracket, readFields, skipString, topLevelObjects } from './object-literal';
|
import { matchBracket, readFields, skipString, topLevelObjects } from './object-literal';
|
||||||
import { dependsOn } from './package-deps';
|
import { dependsOn } from './package-deps';
|
||||||
import {
|
import {
|
||||||
@@ -516,22 +517,22 @@ export function constantText(expr: string, fromFile: string, context: Resolution
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
const decl = new RegExp(String.raw`\b(?:const|let)\s+${root}\s*(?::[^=]+)?=\s*\{`).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+${root}\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+${root}\b[^{]*\{`).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);
|
if (decl) value = readChain(decl.index + decl[0].length - 1, chain);
|
||||||
else if (enumDecl && chain.length === 1) {
|
else if (enumDecl && chain.length === 1) {
|
||||||
// `enum Paths { Board = 'board' }`
|
// `enum Paths { Board = 'board' }`
|
||||||
const open = enumDecl.index + enumDecl[0].length - 1;
|
const open = enumDecl.index + enumDecl[0].length - 1;
|
||||||
const body = safe.slice(open, Math.max(open, matchBracket(safe, open)));
|
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) {
|
} else if (classDecl) {
|
||||||
// `class ProjectConst { static readonly IssueId = 'issueId' }`, and
|
// `class ProjectConst { static readonly IssueId = 'issueId' }`, and
|
||||||
// `class RouterUtil { static Configuration = { Visualizer: 'visualizer' } }`.
|
// `class RouterUtil { static Configuration = { Visualizer: 'visualizer' } }`.
|
||||||
const open = classDecl.index + classDecl[0].length - 1;
|
const open = classDecl.index + classDecl[0].length - 1;
|
||||||
const close = matchBracket(safe, open);
|
const close = matchBracket(safe, open);
|
||||||
const body = close > open ? safe.slice(open, close) : '';
|
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) {
|
if (field) {
|
||||||
const at = open + field.index + field[0].length;
|
const at = open + field.index + field[0].length;
|
||||||
if (safe[at] === '{') value = chain.length > 1 ? readChain(at, chain.slice(1)) : null;
|
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 {
|
function declarationSource(root: string, file: string, context: ResolutionContext, hops: number): string | null {
|
||||||
const content = context.readFile(file);
|
const content = context.readFile(file);
|
||||||
if (!content) return null;
|
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;
|
if (hops >= 3) return null;
|
||||||
const reexports = /\bexport\s+(\*|\{[^}]*\})\s*from\s+(['"])([^'"]+)\2/g;
|
const reexports = /\bexport\s+(\*|\{[^}]*\})\s*from\s+(['"])([^'"]+)\2/g;
|
||||||
let m: RegExpExecArray | null;
|
let m: RegExpExecArray | null;
|
||||||
while ((m = reexports.exec(content)) !== 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 target = resolveImportPath(m[3]!, file, 'typescript', context);
|
||||||
const found = target ? declarationSource(root, target, context, hops + 1) : null;
|
const found = target ? declarationSource(root, target, context, hops + 1) : null;
|
||||||
if (found) return found;
|
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('.');
|
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;
|
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;`. */
|
/** A class property's initializer, read from its body: `routerLinkAbout = publicRoutes.about.routerLink;`. */
|
||||||
export function propertyInitializer(owner: Node, name: string, content: string): string | null {
|
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 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;
|
if (!decl) return null;
|
||||||
let i = decl.index + decl[0].length;
|
let i = decl.index + decl[0].length;
|
||||||
const start = i;
|
const start = i;
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import {
|
|||||||
} from '../types';
|
} from '../types';
|
||||||
import { resolveImportPath } from '../import-resolver';
|
import { resolveImportPath } from '../import-resolver';
|
||||||
import { stripCommentsForRegex } from '../strip-comments';
|
import { stripCommentsForRegex } from '../strip-comments';
|
||||||
|
import { escapeRegExp } from '../../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Match `Function("name")`, `AsyncFunction("name")`, or `Property("name")`
|
* 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;
|
let found: NativeBinding | null = null;
|
||||||
const own = context.readFile(file);
|
const own = context.readFile(file);
|
||||||
if (own && own.includes('NativeModule')) {
|
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 (local) found = { type: local[1] ?? null, module: local[2]!, file };
|
||||||
}
|
}
|
||||||
if (!found) {
|
if (!found) {
|
||||||
@@ -246,7 +247,7 @@ function nativeBinding(name: string, file: string, context: ResolutionContext):
|
|||||||
const safe = stripCommentsForRegex(content, 'typescript');
|
const safe = stripCommentsForRegex(content, 'typescript');
|
||||||
const pattern = binding!.isDefault
|
const pattern = binding!.isDefault
|
||||||
? new RegExp(String.raw`\bexport\s+default\s+` + REQUIRE_NATIVE.source)
|
? 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);
|
const m = pattern.exec(safe);
|
||||||
if (m) found = { type: m[1] ?? null, module: m[2]!, file: target };
|
if (m) found = { type: m[1] ?? null, module: m[2]!, file: target };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { dependsOn } from './package-deps';
|
|||||||
import { resolveImportPath } from '../import-resolver';
|
import { resolveImportPath } from '../import-resolver';
|
||||||
import { defaultExportBinding, extractLocalExportAliases } from '../alias-binding';
|
import { defaultExportBinding, extractLocalExportAliases } from '../alias-binding';
|
||||||
import { stripCommentsForRegex } from '../strip-comments';
|
import { stripCommentsForRegex } from '../strip-comments';
|
||||||
|
import { escapeRegExp } from '../../utils';
|
||||||
import { FN_KINDS, makeLineAt } from '../synth-utils';
|
import { FN_KINDS, makeLineAt } from '../synth-utils';
|
||||||
import { isTestPath } from '../../search/query-utils';
|
import { isTestPath } from '../../search/query-utils';
|
||||||
import { matchBracket, skipString } from './object-literal';
|
import { matchBracket, skipString } from './object-literal';
|
||||||
@@ -316,7 +317,7 @@ function constantPathValue(expr: string, fromFile: string, context: ResolutionCo
|
|||||||
if (!decl) return null;
|
if (!decl) return null;
|
||||||
const lines = context.readFile(file)?.split('\n') ?? [];
|
const lines = context.readFile(file)?.split('\n') ?? [];
|
||||||
const text = lines.slice(decl.startLine - 1, decl.endLine).join('\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);
|
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;
|
if (!decl) return null;
|
||||||
const lines = context.readFile(file)?.split('\n') ?? [];
|
const lines = context.readFile(file)?.split('\n') ?? [];
|
||||||
const text = lines.slice(decl.startLine - 1, decl.endLine).join('\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;
|
if (open < 0) return null;
|
||||||
let value = readObjectValue(text, open, keys);
|
let value = readObjectValue(text, open, keys);
|
||||||
if (value === null) return null;
|
if (value === null) return null;
|
||||||
@@ -611,7 +612,7 @@ function ownExport(source: string, name: string): string | null {
|
|||||||
if (name === 'default') {
|
if (name === 'default') {
|
||||||
const declared = defaultExportedName(source);
|
const declared = defaultExportedName(source);
|
||||||
if (declared) return declared;
|
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 name;
|
||||||
}
|
}
|
||||||
return extractLocalExportAliases(source).find((a) => a.exportedName === name)?.localName ?? null;
|
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 local = /\bexport\s*\{[^}]*?\b([A-Za-z_$][\w$]*)\s+as\s+default\b/.exec(safe)?.[1] ?? '';
|
||||||
} else {
|
} else {
|
||||||
// `export { appRoutes as routes }`
|
// `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]!;
|
if (alias) local = alias[1]!;
|
||||||
}
|
}
|
||||||
// What a module exports is declared at its top level.
|
// What a module exports is declared at its top level.
|
||||||
@@ -2143,7 +2144,7 @@ function declaredTable(
|
|||||||
call: boolean,
|
call: boolean,
|
||||||
visible: (keywords: number[]) => number | undefined
|
visible: (keywords: number[]) => number | undefined
|
||||||
): TableSite[] {
|
): 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 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 chosen = decls.length > 0 ? visible(decls.map((d) => d.index + d[0].length - d[1]!.length)) : undefined;
|
||||||
const decl = chosen === undefined ? undefined : decls[chosen];
|
const decl = chosen === undefined ? undefined : decls[chosen];
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import type { QueryBuilder } from '../db/queries';
|
|||||||
import type { ResolutionContext } from './types';
|
import type { ResolutionContext } from './types';
|
||||||
import type { MaybeYield } from './cooperative-yield';
|
import type { MaybeYield } from './cooperative-yield';
|
||||||
import { enclosingFn } from './synth-utils';
|
import { enclosingFn } from './synth-utils';
|
||||||
|
import { escapeRegExp } from '../utils';
|
||||||
import { blankGoCode, goLocalDeclarationAt, GO_TYPE_KINDS } from './name-matcher';
|
import { blankGoCode, goLocalDeclarationAt, GO_TYPE_KINDS } from './name-matcher';
|
||||||
|
|
||||||
/** One field of a Go struct, in declaration order. */
|
/** 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.
|
// 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 lineStart = f.lineStarts[lit.line - 1] ?? 0;
|
||||||
const lineText = f.text.slice(lineStart, f.lineStarts[lit.line] ?? f.text.length);
|
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 open = -1;
|
||||||
let nearest = Infinity;
|
let nearest = Infinity;
|
||||||
for (let m = typeRe.exec(lineText); m; m = typeRe.exec(lineText)) {
|
for (let m = typeRe.exec(lineText); m; m = typeRe.exec(lineText)) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { Language, Node } from '../types';
|
import { Language, Node } from '../types';
|
||||||
|
import { escapeRegExp } from '../utils';
|
||||||
import { UnresolvedRef, ResolvedRef, ResolutionContext, ImportMapping, isSupertypeTarget, CPP_DEFINE_SIGNATURE, isInheritanceRef, isImportableKind } from './types';
|
import { UnresolvedRef, ResolvedRef, ResolutionContext, ImportMapping, isSupertypeTarget, CPP_DEFINE_SIGNATURE, isInheritanceRef, isImportableKind } from './types';
|
||||||
import { blankStringContents, stripCommentsForRegex } from './strip-comments';
|
import { blankStringContents, stripCommentsForRegex } from './strip-comments';
|
||||||
import { JS_ARRAY_METHODS, JS_BUILT_INS, JS_BUILTIN_METHODS, TS_PRIMITIVE_TYPES } from './js-builtins';
|
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);
|
seen.add(cls.id);
|
||||||
// Instance assignments also shadow methods, even though they are not nodes.
|
// 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');
|
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);
|
const own = context.getNodesByQualifiedName(`${cls.qualifiedName}::${member}`).filter(n => n.filePath === cls.filePath);
|
||||||
if (own.length) return own;
|
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()];
|
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;
|
return out;
|
||||||
}
|
}
|
||||||
if (!line.includes(name)) 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;
|
if (!word.test(line)) return out;
|
||||||
const declared = line.match(/^\s*(global|nonlocal)\s+([\w\s,]+)$/);
|
const declared = line.match(/^\s*(global|nonlocal)\s+([\w\s,]+)$/);
|
||||||
if (declared) {
|
if (declared) {
|
||||||
@@ -576,19 +577,19 @@ function pythonLineBindings(lines: string[], index: number, statements: boolean[
|
|||||||
const assignment = pythonAssignment(line);
|
const assignment = pythonAssignment(line);
|
||||||
if (assignment) {
|
if (assignment) {
|
||||||
const target = assignment.targets.length === 1 && !assignment.augmented ? assignment.targets[0]!.trim() : '';
|
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) {
|
if (target === name || annotated) {
|
||||||
out.push({ kind: 'assign', type: annotated?.[1] ?? null, value: assignment.value.trim(), line: index });
|
out.push({ kind: 'assign', type: annotated?.[1] ?? null, value: assignment.value.trim(), line: index });
|
||||||
} else if (assignment.targets.some(t => word.test(t))) {
|
} else if (assignment.targets.some(t => word.test(t))) {
|
||||||
out.push({ kind: 'other' });
|
out.push({ kind: 'other' });
|
||||||
}
|
}
|
||||||
} else {
|
} 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 });
|
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' });
|
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 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' });
|
for (const lambda of line.matchAll(/\blambda\b([^:]*):/g)) if (word.test(lambda[1]!)) out.push({ kind: 'other' });
|
||||||
return out;
|
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 {
|
function pythonBindsLocally(name: string, ref: UnresolvedRef, context: ResolutionContext, importsBind: boolean): boolean {
|
||||||
const lines = pythonMemberLines(ref.filePath, context);
|
const lines = pythonMemberLines(ref.filePath, context);
|
||||||
const statements = pythonStatementStarts(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 =>
|
const scopes = context.getNodesInFile(ref.filePath).filter(n =>
|
||||||
(n.kind === 'function' || n.kind === 'method') && n.startLine <= ref.line && n.endLine >= ref.line)
|
(n.kind === 'function' || n.kind === 'method') && n.startLine <= ref.line && n.endLine >= ref.line)
|
||||||
.sort((a, b) => b.startLine - a.startLine);
|
.sort((a, b) => b.startLine - a.startLine);
|
||||||
@@ -785,7 +786,7 @@ function pythonModuleAliases(filePath: string, moduleFile: string, context: Reso
|
|||||||
if (!files.includes(moduleFile)) continue;
|
if (!files.includes(moduleFile)) continue;
|
||||||
// The mapping cannot tell `import a.b` from `import a.b as b`; the source line can.
|
// 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.
|
// 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() &&
|
const plainDotted = m.isNamespace && m.source.includes('.') && m.localName === m.source.split('.').pop() &&
|
||||||
!pythonMemberLines(filePath, context).some(line => explicit.test(blankPythonStrings(line)));
|
!pythonMemberLines(filePath, context).some(line => explicit.test(blankPythonStrings(line)));
|
||||||
(files.length === 1 ? aliases : ambiguous).add(plainDotted ? m.source : m.localName);
|
(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('|')})`;
|
const spell = (names: string[]) => `(?:${names.map(escape).join('|')})`;
|
||||||
// Test files: any receiver (the double may sit on any spelling of the module).
|
// Test files: any receiver (the double may sit on any spelling of the module).
|
||||||
const receiver = test ? '[\\w.]+' : spell([...aliases, ...ambiguous]);
|
const receiver = test ? '[\\w.]+' : spell([...aliases, ...ambiguous]);
|
||||||
const target = new RegExp(`(?<![\\w.])${receiver}\\.${name}\\b(?!\\s*[.\\[(])`);
|
const target = new RegExp(`(?<![\\w.])${receiver}\\.${escapeRegExp(name)}\\b(?!\\s*[.\\[(])`);
|
||||||
const exact = new RegExp(`^${test ? receiver : spell(aliases.length ? aliases : ['\\0'])}\\.${name}(?:\\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*['"]${name}['"]` +
|
const dynamic = new RegExp(`\\b(?:setattr|patch\\.object)\\s*\\(\\s*${receiver}\\s*,\\s*['"]${escapeRegExp(name)}['"]` +
|
||||||
`|(?<![\\w.])${receiver}\\.__dict__\\s*(?:\\[|\\.\\s*update\\s*\\()`);
|
`|(?<![\\w.])${receiver}\\.__dict__\\s*(?:\\[|\\.\\s*update\\s*\\()`);
|
||||||
const lines = pythonMemberLines(file, context);
|
const lines = pythonMemberLines(file, context);
|
||||||
const statements = pythonStatementStarts(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 {
|
function pythonFieldType(receiver: string, owner: Node, ref: UnresolvedRef, context: ResolutionContext): string | null {
|
||||||
const lines = pythonMemberLines(ref.filePath, context);
|
const lines = pythonMemberLines(ref.filePath, context);
|
||||||
const field = receiver.split('.')[1]!;
|
const field = receiver.split('.')[1]!;
|
||||||
const assignment = new RegExp(`^\\s*(?:self|cls)\\.${field}\\s*(?::\\s*["']?([\\w.]+)["']?)?\\s*=\\s*(.*)$`);
|
const assignment = new RegExp(`^\\s*(?:self|cls)\\.${escapeRegExp(field)}\\s*(?::\\s*["']?([\\w.]+)["']?)?\\s*=\\s*(.*)$`);
|
||||||
const annotation = new RegExp(`^\\s*(?:(?:self|cls)\\.)?${field}\\s*:\\s*["']?([\\w.]+)`);
|
const annotation = new RegExp(`^\\s*(?:(?:self|cls)\\.)?${escapeRegExp(field)}\\s*:\\s*["']?([\\w.]+)`);
|
||||||
const methods = context.getNodesInFile(ref.filePath).filter(n => n.kind === 'method' &&
|
const methods = context.getNodesInFile(ref.filePath).filter(n => n.kind === 'method' &&
|
||||||
n.qualifiedName.startsWith(`${owner.qualifiedName}::`));
|
n.qualifiedName.startsWith(`${owner.qualifiedName}::`));
|
||||||
const types = new Set<string>();
|
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];
|
.sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
|
||||||
if (fn && (!best || best.startLine < fn.startLine)) {
|
if (fn && (!best || best.startLine < fn.startLine)) {
|
||||||
const lines = context.getFileLines?.(file) ?? context.readFile(file)?.split(/\r?\n/) ?? [];
|
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--) {
|
for (let at = line - 1; at > fn.startLine; at--) {
|
||||||
const m = decl.exec(lines[at - 1] ?? '');
|
const m = decl.exec(lines[at - 1] ?? '');
|
||||||
if (!m) continue;
|
if (!m) continue;
|
||||||
@@ -2093,7 +2094,7 @@ function luaMemberIn(file: string, path: string[], context: ResolutionContext, d
|
|||||||
const source = (context.readFile(file) ?? '')
|
const source = (context.readFile(file) ?? '')
|
||||||
.replace(/--\[(=*)\[[\s\S]*?\]\1\]/g, (c) => c.replace(/[^\n]/g, ''))
|
.replace(/--\[(=*)\[[\s\S]*?\]\1\]/g, (c) => c.replace(/[^\n]/g, ''))
|
||||||
.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) {
|
if (exported && exported[1] !== member) {
|
||||||
const rhs = exported[1]!;
|
const rhs = exported[1]!;
|
||||||
const line = source.slice(0, exported.index).split('\n').length + 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 segs = candidate.qualifiedName.split('::');
|
||||||
const enumName = segs[segs.length - 2] ?? '';
|
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 line = context.getFileLines?.(ref.filePath)?.[ref.line - 1] ?? context.readFile(ref.filePath)?.split(/\r?\n/)[ref.line - 1] ?? '';
|
||||||
const name = ref.referenceName.replace(/\$/g, '\\$');
|
const name = escapeRegExp(ref.referenceName);
|
||||||
if (new RegExp(`\\bcase\\b[^:;]*\\b${name}\\b`).test(line) || new RegExp(`\\b${enumName}\\s*\\.\\s*${name}\\b`).test(line)) return true;
|
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 here = javaFileScope(ref.filePath, context);
|
||||||
const enumFqn = [javaFileScope(candidate.filePath, context).pkg, ...segs.slice(1, -1)].filter((p) => p !== '').join('.');
|
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);
|
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
|
// inner class's `outer.new Inner()` — the qualifier says which: the owner
|
||||||
// (or the package) of this candidate.
|
// (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] ?? '';
|
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`.)
|
// (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
|
// Nested only inside a type the file declares; a class local to a method is
|
||||||
// the lexical rule's to judge.
|
// the lexical rule's to judge.
|
||||||
const ownerQn = segs.slice(0, -1).join('::');
|
const ownerQn = segs.slice(0, -1).join('::');
|
||||||
@@ -3304,7 +3305,7 @@ function isScalaPackageObjectMemberVisible(candidate: Node, ref: UnresolvedRef,
|
|||||||
const here = scalaPackageClauses(text).join('.');
|
const here = scalaPackageClauses(text).join('.');
|
||||||
if (here === objectPkg || here.startsWith(`${objectPkg}.`)) return true;
|
if (here === objectPkg || here.startsWith(`${objectPkg}.`)) return true;
|
||||||
const last = objectPkg.split('.').pop()!;
|
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 }>>>();
|
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;
|
if (!cls) return false;
|
||||||
const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/) ?? [];
|
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 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] ??
|
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];
|
new RegExp(`\\bthis\\.${f}\\s*=\\s*new\\s+([A-Za-z_$][\\w$]*)`).exec(body)?.[1];
|
||||||
return declared === owner;
|
return declared === owner;
|
||||||
@@ -3890,7 +3891,7 @@ function pythonFixtureReturnType(receiver: string, ref: UnresolvedRef, context:
|
|||||||
// The receiver must be the test's own parameter.
|
// The receiver must be the test's own parameter.
|
||||||
const signature = caller && (caller.kind === 'function' || caller.kind === 'method')
|
const signature = caller && (caller.kind === 'function' || caller.kind === 'method')
|
||||||
? lines.slice(caller.startLine - 1, caller.startLine + 4).join(' ').split(/\)\s*(?:->[^:]*)?:/)[0] ?? '' : '';
|
? 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)
|
const fixtures = context.getNodesByName(receiver)
|
||||||
.filter((n) => n.kind === 'function' && n.language === 'python' && isFixtureInReach(n, ref.filePath, context))
|
.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);
|
.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() }));
|
PY_LOCAL_FILE.set(context, (file = { filePath: ref.filePath, lines, module: new Map() }));
|
||||||
}
|
}
|
||||||
const lines = file.lines;
|
const lines = file.lines;
|
||||||
const n = name;
|
const n = escapeRegExp(name);
|
||||||
const assigns = new RegExp(`^\\s*(?:[\\w\\s,*()\\[\\]]*,\\s*)?\\(?\\*?${n}\\)?\\s*(?:,[\\w\\s,*()\\[\\]]*)?(?::[^=]+)?=(?!=)`);
|
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 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*(?:[:=,)]|$)`);
|
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 owner = cut > 0 ? n.qualifiedName.slice(0, cut).split('::').pop()! : '';
|
||||||
const lines = context.getFileLines?.(n.filePath) ?? context.readFile(n.filePath)?.split(/\r?\n/) ?? [];
|
const lines = context.getFileLines?.(n.filePath) ?? context.readFile(n.filePath)?.split(/\r?\n/) ?? [];
|
||||||
const head = (lines[n.startLine - 1] ?? '').slice(n.startColumn);
|
const head = (lines[n.startLine - 1] ?? '').slice(n.startColumn);
|
||||||
const escape = (name: string): string => name.replace(/\$/g, '\\$');
|
|
||||||
const constructor = owner !== '' &&
|
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);
|
memo.set(n.id, constructor);
|
||||||
return constructor;
|
return constructor;
|
||||||
}
|
}
|
||||||
@@ -4439,8 +4439,8 @@ function dartTypeCallPrefix(typeName: string, member: string, ref: UnresolvedRef
|
|||||||
if (prefixes.size === 0) return null;
|
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];
|
const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1] ?? context.readFile(ref.filePath)?.split(/\r?\n/)[ref.line - 1];
|
||||||
if (line === undefined) return null;
|
if (line === undefined) return null;
|
||||||
const written = new RegExp(String.raw`(?<![\w$.])([A-Za-z_$][\w$]*)\s*\.\s*${typeName.replace(/\$/g, '\\$')}\s*` +
|
const written = new RegExp(String.raw`(?<![\w$.])([A-Za-z_$][\w$]*)\s*\.\s*${escapeRegExp(typeName)}\s*` +
|
||||||
String.raw`(?:<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?\s*\.\s*${member.replace(/\$/g, '\\$')}(?![\w$])`, 'g');
|
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.
|
// 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]!));
|
const found = [...line.matchAll(written)].filter((m) => prefixes.has(m[1]!));
|
||||||
return (found.find((m) => m.index === ref.column) ?? found[0])?.[1] ?? null;
|
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 key = `${filePath}|${name}`;
|
||||||
const hit = memo.get(key);
|
const hit = memo.get(key);
|
||||||
if (hit) return hit;
|
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!);
|
if (memo.size >= 4096) memo.delete(memo.keys().next().value!);
|
||||||
memo.set(key, uses);
|
memo.set(key, uses);
|
||||||
return uses;
|
return uses;
|
||||||
@@ -5223,7 +5223,7 @@ function dartConstantType(constant: Node, ref: UnresolvedRef, context: Resolutio
|
|||||||
const code = dartCodeOf(constant.filePath, context);
|
const code = dartCodeOf(constant.filePath, context);
|
||||||
const from = code?.lineStarts[constant.startLine - 1];
|
const from = code?.lineStarts[constant.startLine - 1];
|
||||||
if (!code || from === undefined) return null;
|
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));
|
.exec(code.text.slice(from, from + 600));
|
||||||
if (!head) return null;
|
if (!head) return null;
|
||||||
if (head[1] && !DART_NON_TYPE_WORDS.has(head[1])) return dartSimpleTypeName(head[1]);
|
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);
|
const declared = classMemberType(decl, name, context);
|
||||||
if (declared) return dartSimpleTypeName(declared);
|
if (declared) return dartSimpleTypeName(declared);
|
||||||
// An abstract getter mints no node: `ProviderElement? get _listenedElement;`.
|
// 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)) {
|
for (const { text } of classMemberLines(decl, context)) {
|
||||||
const m = text.includes(name) ? getter.exec(text) : null;
|
const m = text.includes(name) ? getter.exec(text) : null;
|
||||||
if (m && !DART_NON_TYPE_WORDS.has(m[1]!)) return dartSimpleTypeName(m[1]!);
|
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 from = code?.lineStarts[decl.startLine - 1];
|
||||||
const to = code?.lineStarts[decl.endLine];
|
const to = code?.lineStarts[decl.endLine];
|
||||||
if (!code || from === undefined) continue;
|
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) {
|
if (init) {
|
||||||
const site: UnresolvedRef = { ...ref, filePath: decl.filePath, line: decl.startLine, column: 0 };
|
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);
|
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/) ?? [];
|
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.
|
// 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 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);
|
memo.set(n.id, getter);
|
||||||
return getter;
|
return getter;
|
||||||
}
|
}
|
||||||
@@ -5394,7 +5394,7 @@ function isDartSetter(n: Node, context: ResolutionContext): boolean {
|
|||||||
const hit = memo.get(n.id);
|
const hit = memo.get(n.id);
|
||||||
if (hit !== undefined) return hit;
|
if (hit !== undefined) return hit;
|
||||||
const line = (context.getFileLines?.(n.filePath) ?? context.readFile(n.filePath)?.split(/\r?\n/) ?? [])[n.startLine - 1] ?? '';
|
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);
|
memo.set(n.id, setter);
|
||||||
return setter;
|
return setter;
|
||||||
}
|
}
|
||||||
@@ -5678,7 +5678,7 @@ function rustGoCallShape(ref: UnresolvedRef, context: ResolutionContext): { shap
|
|||||||
let line = lines?.[ref.line - 1];
|
let line = lines?.[ref.line - 1];
|
||||||
if (line === undefined) return null;
|
if (line === undefined) return null;
|
||||||
const name = ref.referenceName;
|
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;
|
let start = line.startsWith(name, ref.column) ? ref.column : -1;
|
||||||
if (start < 0) {
|
if (start < 0) {
|
||||||
const m = at.exec(line);
|
const m = at.exec(line);
|
||||||
@@ -5981,13 +5981,13 @@ function kotlinChainReceiver(ref: UnresolvedRef, context: ResolutionContext): st
|
|||||||
const name = ref.referenceName;
|
const name = ref.referenceName;
|
||||||
let start = line.startsWith(name, ref.column) ? ref.column : -1;
|
let start = line.startsWith(name, ref.column) ? ref.column : -1;
|
||||||
if (start < 0) {
|
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;
|
start = m ? m.index : -1;
|
||||||
}
|
}
|
||||||
if (start < 0) {
|
if (start < 0) {
|
||||||
// An infix call on an expression — `alias(libs.x) apply false` — whose
|
// An infix call on an expression — `alias(libs.x) apply false` — whose
|
||||||
// receiver the extractor could not name.
|
// 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;
|
return infix ? rustGoReceiverName(line.slice(0, infix.index).trimEnd()) : null;
|
||||||
}
|
}
|
||||||
const before = line.slice(0, start);
|
const before = line.slice(0, start);
|
||||||
@@ -6017,7 +6017,7 @@ function isReceiverLessKotlinCall(ref: UnresolvedRef, context: ResolutionContext
|
|||||||
const name = ref.referenceName;
|
const name = ref.referenceName;
|
||||||
let start = line.startsWith(name, ref.column) && !/[\w$]/.test(line[ref.column - 1] ?? '') ? ref.column : -1;
|
let start = line.startsWith(name, ref.column) && !/[\w$]/.test(line[ref.column - 1] ?? '') ? ref.column : -1;
|
||||||
if (start < 0) {
|
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;
|
start = m ? m.index : -1;
|
||||||
}
|
}
|
||||||
return start >= 0 && !/[.:]\s*$/.test(line.slice(0, start));
|
return start >= 0 && !/[.:]\s*$/.test(line.slice(0, start));
|
||||||
@@ -6371,7 +6371,7 @@ function swiftCallShape(ref: UnresolvedRef, context: ResolutionContext): SwiftCa
|
|||||||
const name = ref.referenceName;
|
const name = ref.referenceName;
|
||||||
let start = line.startsWith(name, ref.column) && !/[\w$]/.test(line[ref.column - 1] ?? '') ? ref.column : -1;
|
let start = line.startsWith(name, ref.column) && !/[\w$]/.test(line[ref.column - 1] ?? '') ? ref.column : -1;
|
||||||
if (start < 0) {
|
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;
|
start = m ? m.index : -1;
|
||||||
}
|
}
|
||||||
if (start < 0) return null;
|
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;
|
if (label === '' || SWIFT_STD_LABELS.has(label)) return false;
|
||||||
const lines = context.getFileLines?.(n.filePath) ?? context.readFile(n.filePath)?.split(/\r?\n/) ?? [];
|
const lines = context.getFileLines?.(n.filePath) ?? context.readFile(n.filePath)?.split(/\r?\n/) ?? [];
|
||||||
const head = lines.slice(n.startLine - 1, n.startLine + 3).join(' ');
|
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];
|
const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1] ?? context.readFile(ref.filePath)?.split('\n')[ref.line - 1];
|
||||||
if (line === undefined) return true;
|
if (line === undefined) return true;
|
||||||
const name = ref.referenceName;
|
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;
|
if (!at) return true;
|
||||||
const before = line.slice(0, at.index);
|
const before = line.slice(0, at.index);
|
||||||
if (/\.\s*$/.test(before)) {
|
if (/\.\s*$/.test(before)) {
|
||||||
@@ -6807,7 +6807,7 @@ function scalaLocalBinder(name: string, ref: UnresolvedRef, context: ResolutionC
|
|||||||
if (!scope) return null;
|
if (!scope) return null;
|
||||||
const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split('\n') ?? [];
|
const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split('\n') ?? [];
|
||||||
const text = lines.slice(scope.startLine - 1, ref.line).join('\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');
|
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;
|
if (fn) return binder.test(text) ? fn : null;
|
||||||
// In a class body, a binder counts only inside a block still open at the site —
|
// 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 (ch === '>' || ch === ')') depth = Math.max(0, depth - 1);
|
||||||
else if (depth === 0) flat += ch;
|
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()!);
|
for (const m of bases.matchAll(/([A-Za-z_][\w.]*)/g)) names.push(m[1]!.split('.').pop()!);
|
||||||
}
|
}
|
||||||
memo.set(typeName, names);
|
memo.set(typeName, names);
|
||||||
@@ -7233,7 +7233,7 @@ function objcCallShape(ref: UnresolvedRef, context: ResolutionContext): 'c-call'
|
|||||||
if (line === undefined) return null;
|
if (line === undefined) return null;
|
||||||
const name = ref.referenceName.split(':')[0]!;
|
const name = ref.referenceName.split(':')[0]!;
|
||||||
if (!name) return null;
|
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 m: RegExpExecArray | null;
|
||||||
let shape: 'c-call' | 'self-send' | 'super-send' | null = null;
|
let shape: 'c-call' | 'self-send' | 'super-send' | null = null;
|
||||||
while ((m = at.exec(line))) {
|
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. */
|
/** The classes the `@property … Type *name` declarations of `name` give. */
|
||||||
function objcPropertyTypes(name: string, context: ResolutionContext): string[] {
|
function objcPropertyTypes(name: string, context: ResolutionContext): string[] {
|
||||||
const types = new Set<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)) {
|
for (const n of context.getNodesByName(name)) {
|
||||||
if (n.kind !== 'property' || n.language !== 'objc') continue;
|
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] ?? '';
|
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;
|
if (hit) return hit;
|
||||||
const decls = context.getNodesByName(name).filter((n) => n.kind === 'class' && n.language === 'objc');
|
const decls = context.getNodesByName(name).filter((n) => n.kind === 'class' && n.language === 'objc');
|
||||||
const primary = new Set<string>();
|
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) {
|
for (const decl of decls) {
|
||||||
const lines = context.getFileLines?.(decl.filePath) ?? context.readFile(decl.filePath)?.split(/\r?\n/) ?? [];
|
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);
|
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.
|
// whatever path the line writes elsewhere.
|
||||||
const pathed = line === undefined || viaSelf ? null
|
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))
|
: (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) {
|
if (pathed) {
|
||||||
// Through a path: `crate::` / `self::` / `super::` look it up relatively;
|
// Through a path: `crate::` / `self::` / `super::` look it up relatively;
|
||||||
// `io::Result` is the `io` module's — tokio's `runtime/task` alias is not —
|
// `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.)
|
// `runtime::context::Context`. (A method call `.env(…)` is no imported name.)
|
||||||
if (uses.external.has(name) && !uses.bound.has(name) && line !== undefined) {
|
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(…)`).
|
// 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 (at && !/\.\s*$/.test(line.slice(0, Math.max(0, ref.column) + at.index))) return false;
|
||||||
}
|
}
|
||||||
if (!RUST_PRELUDE.has(name)) {
|
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 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 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 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.
|
// Not where the reference says: nothing to read, so nothing to veto.
|
||||||
if (!open) return true;
|
if (!open) return true;
|
||||||
let depth = 0;
|
let depth = 0;
|
||||||
@@ -9201,10 +9201,10 @@ function rustFirstArgIs(type: string, ref: UnresolvedRef, context: ResolutionCon
|
|||||||
} else if (ch === ',' && depth === 0) break;
|
} else if (ch === ',' && depth === 0) break;
|
||||||
}
|
}
|
||||||
const arg = text.slice(open.index + open[0].length, end).trim().replace(/^(?:&\s*(?:mut\s+)?|\*)+/, '').replace(/\.clone\(\)$/, '');
|
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;
|
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 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*::)*${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--) {
|
for (let i = ref.line - 1; i >= 0; i--) {
|
||||||
const line = lines[i] ?? '';
|
const line = lines[i] ?? '';
|
||||||
if (arg === 'self' && /^\s*(?:pub(?:\([^)]*\))?\s+)?(?:unsafe\s+)?impl\b/.test(line)) {
|
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;
|
let from = open.index + open[0].length;
|
||||||
for (let i = from - 1; i < header.length; i++) {
|
for (let i = from - 1; i < header.length; i++) {
|
||||||
const ch = header[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++;
|
if (ch === '<') depth++;
|
||||||
else if (ch === '>' && header[i - 1] !== '-' && --depth === 0) return param();
|
else if (ch === '>' && header[i - 1] !== '-' && --depth === 0) return param();
|
||||||
else if (ch === ',' && depth === 1) {
|
else if (ch === ',' && depth === 1) {
|
||||||
@@ -10312,7 +10312,7 @@ function rustIsAsyncFn(fn: Node, context: ResolutionContext): boolean {
|
|||||||
if (!scopes) return false;
|
if (!scopes) return false;
|
||||||
const start = rustNodeOffset(fn, context);
|
const start = rustNodeOffset(fn, context);
|
||||||
const head = scopes.code.slice(start, start + 300);
|
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));
|
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;
|
if (!scopes) return null;
|
||||||
const code = scopes.code;
|
const code = scopes.code;
|
||||||
const start = rustNodeOffset(decl, context);
|
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;
|
if (!name) return null;
|
||||||
let p = start + name.index + decl.name.length;
|
let p = start + name.index + decl.name.length;
|
||||||
while (p < code.length && /\s/.test(code[p]!)) p++;
|
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. */
|
/** 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 {
|
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) {
|
for (const file of typeFiles) {
|
||||||
const code = rustScopesOf(file, context)?.code ?? '';
|
const code = rustScopesOf(file, context)?.code ?? '';
|
||||||
if (!code.includes('Deref')) continue;
|
if (!code.includes('Deref')) continue;
|
||||||
@@ -10872,7 +10872,7 @@ function rustAliasTarget(alias: Node, context: ResolutionContext): RustWrittenTy
|
|||||||
const scopes = rustScopesOf(alias.filePath, context);
|
const scopes = rustScopesOf(alias.filePath, context);
|
||||||
if (!scopes) return null;
|
if (!scopes) return null;
|
||||||
const start = rustNodeOffset(alias, context);
|
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;
|
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 … {`
|
// `trait Name<…>: Super + Other where … {`
|
||||||
const scopes = rustScopesOf(trait.filePath, context);
|
const scopes = rustScopesOf(trait.filePath, context);
|
||||||
const start = rustNodeOffset(trait, 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;
|
if (!header) continue;
|
||||||
for (const bound of splitRustTopLevel(header[1]!, '+')) {
|
for (const bound of splitRustTopLevel(header[1]!, '+')) {
|
||||||
const head = rustPathHead(bound);
|
const head = rustPathHead(bound);
|
||||||
@@ -10932,7 +10932,7 @@ function rustDelegatesToOwn(method: Node, typeName: string, context: ResolutionC
|
|||||||
if (!scopes) return false;
|
if (!scopes) return false;
|
||||||
const start = scopes.lineStarts[method.startLine - 1] ?? 0;
|
const start = scopes.lineStarts[method.startLine - 1] ?? 0;
|
||||||
const end = scopes.lineStarts[method.endLine ?? method.startLine] ?? scopes.code.length;
|
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);
|
let binds = memo.get(key);
|
||||||
if (binds === undefined) {
|
if (binds === undefined) {
|
||||||
const text = jsFunctionCodeThrough(fn, filePath, line, context);
|
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
|
// A plain declaration. Destructuring re-binds what a call returns under
|
||||||
// the same name — `const { t } = useI18n()`, `const { getLabel } =
|
// the same name — `const { t } = useI18n()`, `const { getLabel } =
|
||||||
// useProps(props)` — which is the same-named function more often than not.
|
// 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('');
|
const chars = blankStringContents(cut, regexes).split('');
|
||||||
for (let r = 0; r + 1 < regexes.length; r += 2) chars.fill(' ', regexes[r]! + 1, regexes[r + 1]!);
|
for (let r = 0; r + 1 < regexes.length; r += 2) chars.fill(' ', regexes[r]! + 1, regexes[r + 1]!);
|
||||||
const code = chars.join('');
|
const code = chars.join('');
|
||||||
const n = name.replace(/\$/g, '\\$');
|
const n = escapeRegExp(name);
|
||||||
const declaration = new RegExp(
|
const declaration = new RegExp(
|
||||||
`\\b(?:(?:const|let|var)\\s+${n}(?![\\w$])(?!\\s*[,\\]}])(\\s*=\\s*(?:await\\s+)?(?:require|import)\\s*\\()?|(?:function|class)\\s+${n}(?![\\w$]))`,
|
`\\b(?:(?:const|let|var)\\s+${n}(?![\\w$])(?!\\s*[,\\]}])(\\s*=\\s*(?:await\\s+)?(?:require|import)\\s*\\()?|(?:function|class)\\s+${n}(?![\\w$]))`,
|
||||||
'g'
|
'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([
|
const CPP_NUMBER_TYPES: ReadonlySet<string> = new Set([
|
||||||
'int', 'unsigned', 'signed', 'short', 'long', 'float', 'double', 'size_t', 'ssize_t', 'ptrdiff_t',
|
'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',
|
'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']);
|
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);
|
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 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(' ') : '';
|
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). */
|
/** 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;
|
if (hit !== undefined) return hit;
|
||||||
const lines = context.getFileLines?.(fn.filePath) ?? context.readFile(fn.filePath)?.split(/\r?\n/) ?? [];
|
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 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);
|
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);
|
memo.set(key, binds);
|
||||||
return binds;
|
return binds;
|
||||||
@@ -18169,7 +18171,7 @@ function angleArguments(text: string): string[] {
|
|||||||
function declaredTypeParameters(decl: Node, context: ResolutionContext): string[] {
|
function declaredTypeParameters(decl: Node, context: ResolutionContext): string[] {
|
||||||
const lines = context.getFileLines?.(decl.filePath) ?? context.readFile(decl.filePath)?.split(/\r?\n/) ?? [];
|
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 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 [];
|
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] ?? '');
|
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[] {
|
function headTypeArguments(cls: Node, sup: string, context: ResolutionContext): string[] {
|
||||||
const lines = context.getFileLines?.(cls.filePath) ?? context.readFile(cls.filePath)?.split(/\r?\n/) ?? [];
|
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 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 [];
|
if (!at) return [];
|
||||||
return angleArguments(head.slice(at.index)).map((a) => a.replace(/<[\s\S]*$/, '').split('.').pop()!.trim());
|
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}`;
|
const key = `${cls.id}|${name}`;
|
||||||
if (memo.has(key)) return memo.get(key)!;
|
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'
|
const pattern = cls.language === 'kotlin' || cls.language === 'swift'
|
||||||
? new RegExp(`\\b(?:val|var|let)\\s+${r}\\s*:\\s*([A-Z][\\w.]*)`)
|
? 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*(?:[=;,)]|\\{)`);
|
: 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 {
|
function isThisCallInOwnFile(n: Node, ref: UnresolvedRef, context: ResolutionContext): boolean {
|
||||||
if (n.filePath !== ref.filePath) return false;
|
if (n.filePath !== ref.filePath) return false;
|
||||||
const line = (context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/))?.[ref.line - 1] ?? '';
|
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? */
|
/** 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);
|
const lines = pythonLines(context, cls.filePath);
|
||||||
if (!lines) return [];
|
if (!lines) return [];
|
||||||
const header = lines.slice(cls.startLine - 1, cls.startLine + 4).join(' ');
|
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 ?? '')
|
return (bases ?? '')
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((b) => b.trim())
|
.map((b) => b.trim())
|
||||||
@@ -22671,7 +22673,7 @@ function matchRustSelfCall(
|
|||||||
// `(self.into_route)(state)` calls what a field holds, no method: axum's
|
// `(self.into_route)(state)` calls what a field holds, no method: axum's
|
||||||
// `MakeErasedHandler::into_route` went to itself.
|
// `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);
|
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
|
// 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
|
// 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`) —
|
// 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:
|
// The call's expression starts at its column with the whole chain:
|
||||||
// `self.delay.as_mut().reset(` — `?` after `as_mut()` taking what an
|
// `self.delay.as_mut().reset(` — `?` after `as_mut()` taking what an
|
||||||
// `Option` / `Result` holds back out (`self.client.as_mut()?.next()`).
|
// `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)));
|
.exec(line.slice(Math.max(0, ref.column)));
|
||||||
if (!lent) return undefined;
|
if (!lent) return undefined;
|
||||||
const [, base, field, question] = lent;
|
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 {
|
function rustProjectsSelf(name: string, ref: UnresolvedRef, context: ResolutionContext): boolean {
|
||||||
const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/) ?? [];
|
const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/) ?? [];
|
||||||
const start = Math.max(0, enclosingScopeStartLine(ref, context) - 1);
|
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--) {
|
for (let i = Math.min(lines.length, ref.line) - 1; i >= start; i--) {
|
||||||
const m = bound.exec(lines[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]!);
|
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));
|
.map((s) => fileLines(s.filePath).slice(s.startLine - 1, s.endLine));
|
||||||
if (spans.length === 0) {
|
if (spans.length === 0) {
|
||||||
const lines = fileLines(ref.filePath);
|
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(/\/\/.*$/, '')));
|
const at = lines.findIndex((l) => header.test(l.replace(/\/\/.*$/, '')));
|
||||||
if (at >= 0) {
|
if (at >= 0) {
|
||||||
let depth = 0;
|
let depth = 0;
|
||||||
@@ -22978,7 +22980,7 @@ function rustFieldDeclaration(owner: string, field: string, ref: UnresolvedRef,
|
|||||||
spans.push(lines.slice(at, end + 1));
|
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 (const span of spans) {
|
||||||
for (let i = 0; i < span.length; i++) {
|
for (let i = 0; i < span.length; i++) {
|
||||||
const line = (span[i] ?? '').replace(/\/\/.*$/, '');
|
const line = (span[i] ?? '').replace(/\/\/.*$/, '');
|
||||||
@@ -23649,7 +23651,7 @@ function tsIndexedPropertyType(
|
|||||||
function tsClassTypeParameter(cls: Node, name: string, context: ResolutionContext): boolean {
|
function tsClassTypeParameter(cls: Node, name: string, context: ResolutionContext): boolean {
|
||||||
const lines = context.getFileLines?.(cls.filePath) ?? context.readFile(cls.filePath)?.split(/\r?\n/) ?? [];
|
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 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;
|
if (!open) return false;
|
||||||
// The parameters between the `<` after the name and its own `>`, split
|
// The parameters between the `<` after the name and its own `>`, split
|
||||||
// at their top level: `T`, `K extends keyof T = never`.
|
// 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;
|
if (!callee || !sameLanguageFamily(callee.language, ref.language)) return null;
|
||||||
const calleeText = (context.getFileLines?.(callee.filePath) ?? context.readFile(callee.filePath)?.split('\n') ?? [])
|
const calleeText = (context.getFileLines?.(callee.filePath) ?? context.readFile(callee.filePath)?.split('\n') ?? [])
|
||||||
.slice(callee.startLine - 1, callee.endLine).join('\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 callable = (n: Node) => n.kind === 'function' || n.kind === 'method' || n.kind === 'constant' || n.kind === 'variable';
|
||||||
const inFile = context.getNodesInFile(callee.filePath);
|
const inFile = context.getNodesInFile(callee.filePath);
|
||||||
const inner = inFile.filter((n) => n.name === key && callable(n) && n.id !== callee.id && rangeWithin(n, callee) &&
|
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 {
|
function otherSupertypeNamed(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||||
const name = ref.referenceName.split(/::|\./).pop()!;
|
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 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 qualifier = written ? written[1]!.slice(0, -1) : '';
|
||||||
const candidates = context.getNodesByName(name).filter((n) =>
|
const candidates = context.getNodesByName(name).filter((n) =>
|
||||||
n.id !== ref.fromNodeId && isSupertypeTarget(n) && sameLanguageFamily(n.language, ref.language) &&
|
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. */
|
/** The `( … )` that follows `name` in `text`, from `column` on, without its parentheses. */
|
||||||
function cppParenListIn(text: string, column: number, name: string): string | null {
|
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;
|
if (!at) return null;
|
||||||
const open = column + at.index + at[0].length - 1;
|
const open = column + at.index + at[0].length - 1;
|
||||||
let depth = 0;
|
let depth = 0;
|
||||||
@@ -25640,7 +25642,7 @@ function cppOverloadParameters(n: Node, name: string, context: ResolutionContext
|
|||||||
.filter((p) => p !== 'void')
|
.filter((p) => p !== 'void')
|
||||||
.map((p) => normalizeCppTypeName(cppParameterType(p)) ?? p);
|
.map((p) => normalizeCppTypeName(cppParameterType(p)) ?? p);
|
||||||
const want = types(own);
|
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) {
|
search: for (const cls of classes) {
|
||||||
const lines = context.getFileLines?.(cls.filePath) ?? context.readFile(cls.filePath)?.split(/\r?\n/) ?? [];
|
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++) {
|
for (let line = cls.startLine; line <= Math.min(lines.length, cls.endLine ?? cls.startLine); line++) {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import type { Edge, Language } from '../types';
|
|||||||
import type { ResolutionContext } from './types';
|
import type { ResolutionContext } from './types';
|
||||||
import type { MaybeYield } from './cooperative-yield';
|
import type { MaybeYield } from './cooperative-yield';
|
||||||
import { stripCommentsForRegex } from './strip-comments';
|
import { stripCommentsForRegex } from './strip-comments';
|
||||||
|
import { escapeRegExp } from '../utils';
|
||||||
import { isTestPath } from '../search/query-utils';
|
import { isTestPath } from '../search/query-utils';
|
||||||
import { parseHrefExpression, routesForFile, toHref, type HrefLiteral } from './frameworks/expo-router';
|
import { parseHrefExpression, routesForFile, toHref, type HrefLiteral } from './frameworks/expo-router';
|
||||||
import { matchBracket } from './frameworks/object-literal';
|
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. */
|
/** `<Tag … to=…>` for any of `tags`, the attribute anywhere in the tag. */
|
||||||
function linkTagPattern(tags: readonly string[]): RegExp {
|
function linkTagPattern(tags: readonly string[]): RegExp {
|
||||||
// An attribute before `to` may hold an arrow (`onMouseEnter={() => …}`), whose `>` is not the tag's end.
|
// 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);
|
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;
|
if (!source || !/\bto\s*=/.test(source)) continue;
|
||||||
const tags = linkTagsFor(file, wrappers, ctx);
|
const tags = linkTagsFor(file, wrappers, ctx);
|
||||||
const pattern = tags.length === LINK_TAGS.length ? LINK_TAG : linkTagPattern(tags);
|
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 safe = stripCommentsForRegex(source, 'typescript');
|
||||||
const nodes = ctx.getNodesInFile(file);
|
const nodes = ctx.getNodesInFile(file);
|
||||||
const lineOf = makeLineAt(safe, 1);
|
const lineOf = makeLineAt(safe, 1);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
*/
|
*/
|
||||||
import type { Node } from '../types';
|
import type { Node } from '../types';
|
||||||
import type { ResolutionContext, ResolvedRef, UnresolvedRef } 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']);
|
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));
|
const before = QUALIFIER_BEFORE.exec(text.slice(Math.max(0, column - 240), column));
|
||||||
return before ? clean(before[1]!) : name;
|
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;
|
return chain ? clean(chain[1]!) : name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
import type { Node } from '../types';
|
import type { Node } from '../types';
|
||||||
import type { ResolutionContext, ResolvedRef, UnresolvedRef } from './types';
|
import type { ResolutionContext, ResolvedRef, UnresolvedRef } from './types';
|
||||||
|
import { escapeRegExp } from '../utils';
|
||||||
|
|
||||||
const GENERIC_LANGUAGES: ReadonlySet<string> = new Set([
|
const GENERIC_LANGUAGES: ReadonlySet<string> = new Set([
|
||||||
'scala', 'java', 'kotlin', 'csharp', 'typescript', 'tsx', 'swift', 'rust', 'dart', 'go', 'cpp', 'vbnet', 'arkts',
|
'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.*`.
|
// Imported: `import cats.data.FreeT.FlatMapped._`, `Owner.{A, B}`, `Owner.*`.
|
||||||
const text = context.readFile(ref.filePath) ?? '';
|
const text = context.readFile(ref.filePath) ?? '';
|
||||||
const imports = new RegExp(
|
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;
|
return /^[A-Za-z_]\w*$/.test(owner.name) && /^[A-Za-z_]\w*$/.test(target.name) ? imports.test(text) : true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import { resolveProjectFile } from '../security';
|
|||||||
import { findIndexedFile, hasDriftedOnDisk } from './source';
|
import { findIndexedFile, hasDriftedOnDisk } from './source';
|
||||||
import { createWhenReader } from './when';
|
import { createWhenReader } from './when';
|
||||||
import { toNodeRef, type WireNodeRef } from './wire';
|
import { toNodeRef, type WireNodeRef } from './wire';
|
||||||
|
import { escapeRegExp } from '../../utils';
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Wire shapes
|
// Wire shapes
|
||||||
@@ -586,7 +587,7 @@ function mentionsOf(cg: CodeGraph, projectRoot: string, value: Node): Edge[] {
|
|||||||
.slice(0, MAX_MENTION_FILES);
|
.slice(0, MAX_MENTION_FILES);
|
||||||
if (importers.length === 0) return out;
|
if (importers.length === 0) return out;
|
||||||
const files = cg.getNodesByIds(importers);
|
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()) {
|
for (const file of files.values()) {
|
||||||
if (file.kind !== 'file') continue;
|
if (file.kind !== 'file') continue;
|
||||||
const found = findIndexedFile(cg, file.filePath.replace(/\\/g, '/'));
|
const found = findIndexedFile(cg, file.filePath.replace(/\\/g, '/'));
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import { nextRouteForFile } from '../../resolution/frameworks/nextjs';
|
|||||||
import { splitRouteName } from './routes';
|
import { splitRouteName } from './routes';
|
||||||
import { HUB_THRESHOLD, UNCERTAIN_BELOW, toNodeRef, type WireNodeRef } from './wire';
|
import { HUB_THRESHOLD, UNCERTAIN_BELOW, toNodeRef, type WireNodeRef } from './wire';
|
||||||
import { isTestPath } from '../../search/query-utils';
|
import { isTestPath } from '../../search/query-utils';
|
||||||
|
import { escapeRegExp } from '../../utils';
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Wire shapes
|
// 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;
|
if ((n.kind !== 'field' && n.kind !== 'property' && n.kind !== 'variable' && n.kind !== 'parameter') || !n.signature) continue;
|
||||||
const sig = n.signature.replace(/\s+/g, ' ').trim();
|
const sig = n.signature.replace(/\s+/g, ' ').trim();
|
||||||
// `OwnerRepository owners`, `private final OwnerRepository owners`, `owners: OwnerRepository`, `val owners: OwnerRepository`.
|
// `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());
|
if (typed && !types.has(n.name)) types.set(n.name, typed[1]!.trim());
|
||||||
}
|
}
|
||||||
fileTypes.set(caller.filePath, types);
|
fileTypes.set(caller.filePath, types);
|
||||||
|
|||||||
@@ -231,6 +231,17 @@ export function normalizePath(filePath: string): string {
|
|||||||
return filePath.replace(/\\/g, '/');
|
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.
|
* Cross-process file lock using a lock file with PID tracking.
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user