From 4f3eaf33ae903f906bddeb65505d19d256068884 Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Mon, 12 Oct 2026 19:23:41 +0000 Subject: [PATCH 1/5] fix(resolution): component and scoped-alias imports link the module file (#2528) An import statement's own module reference linked nothing in two cases, even when its bindings resolved through the file: a Vue, Svelte or Astro component's import (`isJsPathImportRef` covered only the script languages), and a bare scoped package that a `tsconfig` path or a workspace package maps to a project file (`isJsPathSpecifier` excluded `@scope/pkg`). So a barrel or a side-effect module showed no dependency from those files, and sync's importer discovery, which follows the module edge into a changed file, never found them when a barrel they import through was deleted or added. Both now resolve through `resolveImportPath`, as a script's path import does; an outside scoped package still names no file. Component imports written with an extension, which a basename match linked before, now link through the resolver too. When a sync adds a file, it also retries the failed imports that now resolve to it but wait under no key its path gives: a scoped package parked under its last segment (`common` for `@excalidraw/common`, mapped to `packages/common/src/index.ts`, whose keys are `src` and `index`), under the module ceiling, and an import of the importer's own folder or the one above (`.`, `..`), parked under no tail. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/sfc-alias-module-imports.test.ts | 78 ++++++++++++++++++++++ __tests__/sync-module-importers.test.ts | 10 ++- src/db/queries.ts | 77 ++++++++++++++++++--- src/index.ts | 29 +++++++- src/resolution/import-resolver.ts | 19 +++--- src/resolution/index.ts | 5 ++ 7 files changed, 196 insertions(+), 23 deletions(-) create mode 100644 __tests__/sfc-alias-module-imports.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d8cf93e910..fe4ce4961d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - `codegraph sync` and the file watcher now update the files that import through a module that re-exports what it imports, like a package's `__init__.py` or an `index.ts` barrel, when that module is deleted or added, and in Python also the files that reach it through another package re-exporting it. Before, deleting `shop/__init__.py` left every `from shop import Order` linked to the class it re-exported, and deleting `components/index.ts` left every `import { Button } from './components'` linked, until each importing file changed or the project was re-indexed. When such a module came back, the imports stayed linked the way they were while it was missing, and in a large Python project an import of a module whose name is as common as `models` or `test`, like `from django.test import TestCase`, stayed unlinked. Python, JavaScript, TypeScript, Vue, Svelte and Astro. - In JavaScript and TypeScript, a file that re-exports a module, like an `index.ts` with `export * from './button'` or `export { Button } from './button'`, now counts as importing that module, the way an `import` statement does. `codegraph affected` and a module's dependents now include the barrels that re-export it; before, a barrel made only of `export *` lines showed no dependency at all. Re-index JavaScript and TypeScript projects after upgrading. - `codegraph sync` and the file watcher now also update the files that import through an `index.ts` re-exporting another barrel, like `export * from './dropdown-menu'`, when that inner barrel is deleted or added. Before, their imports stayed linked through the deleted barrel, or stayed linked the way they were while it was missing, until each importing file changed or the project was re-indexed. +- An import in a Vue, Svelte or Astro component now links the component to the file it imports, as an import in a script does, and so does an import of a scoped package that a `tsconfig` path or a workspace package maps to a project file, like `import { debounce } from '@excalidraw/common'`. Before, only the names such an import binds were linked, so a barrel or a module imported for its side effects showed no dependency from those files in `codegraph affected`, and `codegraph sync` could not update them when a barrel they import through was deleted or added. `codegraph sync` and the file watcher also link such an import again, and one written as `.` or `..`, when the file it names comes back. Re-index JavaScript, TypeScript, Vue, Svelte and Astro projects after upgrading. - In C, C++ and Objective-C, a struct or class member declared as a pointer, reference, array or function, like `SharedState* shared;`, `jv elements[];` or `virtual Status Put(…) = 0;`, no longer makes the struct look like it inherits from the member's type. Before, `codegraph_explore` and impact listed those types as base classes, every struct holding a pointer to a type as one of its subclasses, and a method of that struct as an override of the type's method with the same name. Real base classes like `class Derived : public Base`, Objective-C superclasses and Go's embedded fields are unchanged. Re-index C, C++ and Objective-C projects after upgrading. - In C++, a call on a variable, parameter or member declared through a `typedef` or `using` alias now links to the method of the type the alias names, with the alias looked up the way C++ does it: in the calling function, its class and the classes that class inherits from, then the namespaces around it. In google/leveldb, `table_.Insert(buf)` on a `Table table_;` declared next to `typedef SkipList Table;` now reaches `SkipList::Insert` instead of the unrelated `HandleTable::Insert`, and `Table::Iterator iter(&table_);` reaches `SkipList::Iterator`'s constructor. A class that only shares the alias's name is no longer taken for it, and when the aliased type has no such method, as with an alias of `std::vector` or of a template parameter, no method is guessed from the receiver's name. Re-index C++ projects after upgrading. - In C++, a call on a variable, parameter or member declared with template arguments nested inside its template arguments, like `autovector>` or `Striped>`, now links to the method of the class it is declared as. Before, the nested arguments hid that class: in RocksDB, `files_marked_for_compaction_.clear()` on such an `autovector` linked to an unrelated `CompactionInputFiles::clear` that only shares words with the variable's name, and `mutex_.Get(key)` on a `Striped` linked to nothing. Re-index C++ projects after upgrading. diff --git a/__tests__/sfc-alias-module-imports.test.ts b/__tests__/sfc-alias-module-imports.test.ts new file mode 100644 index 0000000000..600c0252e6 --- /dev/null +++ b/__tests__/sfc-alias-module-imports.test.ts @@ -0,0 +1,78 @@ +/** + * An import statement's module reference links the importing file to the file + * it names: from a Vue, Svelte or Astro component as from a script, and for a + * scoped package a `tsconfig` path maps to a project file. Before, a + * component's `import { x } from '../lib/util'` and a script's `import { x } + * from '@acme/common'` bound `x` through that file while the statement itself + * linked nothing, so the module never counted the importer as a dependent. + */ +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'; + +let roots: string[] = []; +let graphs: CodeGraph[] = []; + +afterEach(() => { + for (const graph of graphs) graph.close(); + graphs = []; + for (const root of roots) fs.rmSync(root, { recursive: true, force: true }); + roots = []; +}); + +/** The files a file's import statements link it to (its module edges, not its calls). */ +function moduleImports(graph: CodeGraph, file: string): string[] { + const ids = graph.getNodesInFile(file).map((n) => n.id); + return [...new Set(graph.getOutgoingEdgesFrom(ids) + .filter((e) => e.kind === 'imports' && e.target.startsWith('file:')) + .map((e) => e.target.slice('file:'.length)))].sort(); +} + +async function index(files: Record): Promise { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-sfc-alias-')); + roots.push(root); + for (const [file, text] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); + fs.writeFileSync(path.join(root, file), text); + } + const graph = await CodeGraph.init(root, { index: true }); + graphs.push(graph); + return graph; +} + +describe('an import links the importing file to the module file', () => { + it('from a Vue, Svelte or Astro component, by relative path', async () => { + const graph = await index({ + 'package.json': '{"name":"app"}', + 'src/lib/util.ts': 'export function format(s: string) { return s; }\n', + 'src/components/Card.vue': + '\n\n', + 'src/components/Badge.svelte': + '\n{label}\n', + 'src/pages/index.astro': + '---\nimport { format } from \'../lib/util\';\nconst heading = format(\'z\');\n---\n

{heading}

\n', + }); + for (const component of ['src/components/Card.vue', 'src/components/Badge.svelte', 'src/pages/index.astro']) { + expect(moduleImports(graph, component), component).toEqual(['src/lib/util.ts']); + } + }, 120_000); + + it('for a scoped package a tsconfig path maps to a project file', async () => { + const graph = await index({ + 'package.json': '{"name":"app"}', + 'tsconfig.json': JSON.stringify({ + compilerOptions: { baseUrl: '.', paths: { '@acme/common': ['./packages/common/src/index.ts'] } }, + }), + 'packages/common/src/index.ts': "export * from './debounce';\n", + 'packages/common/src/debounce.ts': 'export function debounce(fn: () => void) { return fn; }\n', + // `useTesting` keeps the outside package's import in the file, unused. + 'apps/web/src/search.ts': + "import { debounce } from '@acme/common';\nimport { render } from '@testing-library/react';\nexport const useTesting = render;\nexport const run = debounce(() => {});\n", + }); + // The outside package names no project file: the file links to nothing for it. + expect(moduleImports(graph, 'apps/web/src/search.ts')).toEqual(['packages/common/src/index.ts']); + expect(graph.getFileDependents('packages/common/src/index.ts')).toEqual(['apps/web/src/search.ts']); + }, 120_000); +}); diff --git a/__tests__/sync-module-importers.test.ts b/__tests__/sync-module-importers.test.ts index 68268fdb92..57f6f311c7 100644 --- a/__tests__/sync-module-importers.test.ts +++ b/__tests__/sync-module-importers.test.ts @@ -57,10 +57,18 @@ const PYTHON_BARRELS = ['app/shop/__init__.py', 'lib/api.py', 'app/orm/__init__. * ES modules: an `index.ts` re-exporting under the same name and under * another, and modules re-exporting it in turn — an outer `index.ts`, all of * it and one name renamed, and a module passing on one name, read through a - * namespace import — imported in its stead. + * namespace import — imported in its stead. It is imported from Vue + * components too (one beside it, as `.`), and through a scoped package a + * `tsconfig` path maps to it. */ const TS: Files = { 'package.json': '{"name":"app"}', + 'tsconfig.json': JSON.stringify({ compilerOptions: { baseUrl: '.', paths: { '@app/widgets': ['./src/components/index.ts'] } } }), + 'src/View.vue': + "\n\n", + 'src/aliased.ts': "import { Button } from '@app/widgets';\nexport function aliased() { return Button(); }\n", + 'src/components/Panel.vue': + "\n\n", 'src/components/Button.ts': 'export function Button() { return 1; }\n', 'src/components/Icon.ts': 'export function BaseIcon() { return 2; }\n', 'src/components/index.ts': "export { Button } from './Button';\nexport { BaseIcon as Icon } from './Icon';\n", diff --git a/src/db/queries.ts b/src/db/queries.ts index 9700e3d63f..010d32fd36 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -4042,26 +4042,40 @@ export class QueryBuilder { /** * Failed `imports` refs a sync should retry once files appear. An import * names a file, a folder or a namespace, which the symbol lookup above - * cannot match, so it is found two ways: + * cannot match, so it is found three ways: * - by `pathKeys` (`importPathKeys` of each added file), as its tail or its * whole name: `package:app/b.dart` waits for a file named `b.dart`, a * bare `#include "b.h"` or `require 'db.php'` is the file's whole name; * - by `names` (the changed files' namespaces and modules), as its whole * name when its tail is something else: a C# `using Foo.Bar` waits for * the namespace node `Foo.Bar`. An import whose tail is the name is the - * symbol lookup's, under that lookup's ceiling. - * Same per-name ceiling as {@link getRetryableFailedReferences}, counted - * per key. + * symbol lookup's, under that lookup's ceiling; + * - by `resolvedModules`, module specifiers that now resolve to an added + * file, as its whole name: `@excalidraw/common`, which a `tsconfig` path + * maps to `packages/common/src/index.ts`, waits under the tail `common`, + * which the file's own keys (`src`, `index`) never give. + * The other lookups share the per-name ceiling of + * {@link getRetryableFailedReferences}, counted per key; this one has the + * module ceiling, since a specifier that resolves to the file is no noise. */ - getRetryableFailedImports(pathKeys: string[], names: string[] = [], perNameCeiling: number = 500): UnresolvedReference[] { + getRetryableFailedImports( + pathKeys: string[], + names: string[] = [], + perNameCeiling: number = 500, + resolvedModules: string[] = [], + perModuleCeiling: number = 5000, + ): UnresolvedReference[] { const failedImports = "status = 'failed' AND reference_kind = 'imports'"; const lookups = [ - { keys: pathKeys, column: 'name_tail', where: failedImports }, - { keys: pathKeys, column: 'reference_name', where: failedImports }, - { keys: names, column: 'reference_name', where: `${failedImports} AND name_tail != reference_name` }, + { keys: pathKeys, column: 'name_tail', where: failedImports, ceiling: perNameCeiling }, + { keys: pathKeys, column: 'reference_name', where: failedImports, ceiling: perNameCeiling }, + { keys: names, column: 'reference_name', where: `${failedImports} AND name_tail != reference_name`, ceiling: perNameCeiling }, + // A module specifier that now resolves to an added file: no noise, so + // the module ceiling, as for a module key. + { keys: resolvedModules, column: 'reference_name', where: failedImports, ceiling: perModuleCeiling }, ]; const rows = new Map(); - for (const { keys, column, where } of lookups) { + for (const { keys, column, where, ceiling } of lookups) { const unique = [...new Set(keys)].filter((key) => key.length > 0); // Pass 1: per-key counts, chunked under the SQLite parameter limit. const retryKeys: string[] = []; @@ -4072,7 +4086,7 @@ export class QueryBuilder { .prepare(`SELECT ${column} AS key, COUNT(*) AS count FROM unresolved_refs WHERE ${where} AND ${column} IN (${placeholders}) GROUP BY ${column}`) .all(...chunk) as Array<{ key: string; count: number }>; for (const row of counts) { - if (row.count <= perNameCeiling) retryKeys.push(row.key); + if (row.count <= ceiling) retryKeys.push(row.key); } } // Pass 2: load the surviving rows; a row two lookups find is kept once. @@ -4099,6 +4113,49 @@ export class QueryBuilder { })); } + /** + * The scoped package specifiers (`@excalidraw/common`) of failed imports, + * each with one file that wrote it — what a `tsconfig` path or a workspace + * package can map to a project file that a sync adds. + */ + getFailedScopedImports(): Array<{ name: string; filePath: string; language: Language }> { + return (this.db + .prepare( + `SELECT reference_name AS name, MIN(file_path) AS file_path, language + FROM unresolved_refs + WHERE status = 'failed' AND reference_kind = 'imports' + AND reference_name GLOB '@?*/?*' AND reference_name NOT GLOB '@*/*/*' + GROUP BY reference_name` + ) + .all() as Array<{ name: string; file_path: string; language: Language }>) + .map((row) => ({ name: row.name, filePath: row.file_path, language: row.language })); + } + + /** + * Failed imports of the folder the importing file is in or under (`.`, + * `..`). Their tail is empty, since they name a folder only through where + * they are written, so no added file's keys find them. + */ + getFailedFolderImports(): UnresolvedReference[] { + return (this.db + .prepare( + `SELECT * FROM unresolved_refs + WHERE status = 'failed' AND reference_kind = 'imports' AND reference_name IN ('.', './', '..', '../')` + ) + .all() as UnresolvedRefRow[]) + .map((row) => ({ + fromNodeId: row.from_node_id, + referenceName: row.reference_name, + referenceKind: row.reference_kind as EdgeKind, + line: row.line, + column: row.col, + candidates: row.candidates ? safeJsonParse(row.candidates, undefined) : undefined, + filePath: row.file_path, + language: row.language as Language, + rowId: row.id, + })); + } + /** * Failed `calls` refs whose name tail is one of `tails` — the navigation * calls (`history.push`, `navigate`) a sync looks through after a route diff --git a/src/index.ts b/src/index.ts index 2e777dbf5a..d98fac584e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,7 @@ import { FindRelevantContextOptions, UnresolvedReference, IndexHealth, + Language, } from './types'; import { DatabaseConnection, getDatabasePath, removeDatabaseFiles } from './db'; import { WalCheckpointValve, resolveWalValveMb } from './db/wal-valve'; @@ -1056,13 +1057,35 @@ export class CodeGraph { // Look those up by what the ADDED files can be imported as (a // modified file was already there for any import of it), and by // the namespaces and modules the changed files declare. + // A scoped package that a `tsconfig` path or a workspace package maps + // to an added file (`@excalidraw/common` to `packages/common/src/ + // index.ts`) waits under its last segment, which that file's keys + // never give: look its failed imports up by the specifier itself. + // An import of the importer's own folder or the one above (`.`, + // `..`) waits under no key at all, so each is tried where written. + const addedFiles = new Set(result.addedFilePaths ?? []); + const resolvesToAdded = (specifier: string, fromFile: string, language: Language): boolean => { + const file = this.resolver.resolveModuleFile(specifier, fromFile, language); + return file !== null && addedFiles.has(file); + }; + const resolvedModules = addedFiles.size === 0 ? [] : this.queries.getFailedScopedImports() + .filter((s) => resolvesToAdded(s.name, s.filePath, s.language)) + .map((s) => s.name); + const folderImports = addedFiles.size === 0 ? [] : this.queries.getFailedFolderImports() + .filter((ref) => ref.filePath !== undefined && ref.language !== undefined && + resolvesToAdded(ref.referenceName, ref.filePath, ref.language)); const retryRows = new Set(retryable.map((ref) => ref.rowId)); const importRetry = this.queries.getRetryableFailedImports( (result.addedFilePaths ?? []).flatMap(importPathKeys), - this.queries.getNodeNamesByFiles(result.changedFilePaths, ['namespace', 'module']) + this.queries.getNodeNamesByFiles(result.changedFilePaths, ['namespace', 'module']), + undefined, + resolvedModules ); - for (const ref of importRetry) { - if (!retryRows.has(ref.rowId)) retryable.push(ref); + for (const ref of [...importRetry, ...folderImports]) { + if (!retryRows.has(ref.rowId)) { + retryable.push(ref); + retryRows.add(ref.rowId); + } } // In row order, as a full index resolves them: when two refs make // the same edge, such as a route's lazily loaded class that its diff --git a/src/resolution/import-resolver.ts b/src/resolution/import-resolver.ts index c0a64b5540..9423eaebff 100644 --- a/src/resolution/import-resolver.ts +++ b/src/resolution/import-resolver.ts @@ -1717,25 +1717,26 @@ export function resolvePhpImportedStaticCall( return { original: ref, targetNodeId: methods[0]!.id, confidence: 0.95, resolvedBy: 'import' }; } -const JS_MODULE_LANGUAGES: ReadonlySet = new Set(['javascript', 'jsx', 'typescript', 'tsx', 'arkts']); - /** * A module specifier written as a path — relative (`./x`, `../x`), rooted, or - * an alias with a folder in it (`@/lib/x`, `~/x`, `$lib/x`) — rather than an - * imported binding's name or a bare package (`react`, `lodash`). + * an alias with a folder in it (`@/lib/x`, `~/x`, `$lib/x`) — or a scoped + * package (`@excalidraw/common`), which no binding can be named and which a + * `tsconfig` path or a workspace member can map to a project file; rather + * than an imported binding's name or a bare package (`react`, `lodash`). */ function isJsPathSpecifier(name: string): boolean { return name.startsWith('./') || name.startsWith('../') || name === '.' || name === '..' || - (name.includes('/') && !/\s/.test(name) && !/^@[\w.-]+\/[\w.-]+$/.test(name)); + (name.includes('/') && !/\s/.test(name)); } /** - * A JS/TS `imports` reference that names a module by path. It names a FILE, not - * a symbol, so it skips the resolver's name-exists pre-filter — a CommonJS - * `require('./x')` has no import node of that name to pass it. + * A JS/TS `imports` reference that names a module by path — from a script, or + * from a Vue, Svelte or Astro component's. It names a FILE, not a symbol, so + * it skips the resolver's name-exists pre-filter — a CommonJS `require('./x')` + * has no import node of that name to pass it. */ export function isJsPathImportRef(ref: UnresolvedRef): boolean { - return ref.referenceKind === 'imports' && JS_MODULE_LANGUAGES.has(ref.language) && isJsPathSpecifier(ref.referenceName); + return ref.referenceKind === 'imports' && ESM_IMPORT_LANGUAGES.has(ref.language) && isJsPathSpecifier(ref.referenceName); } /** PHP reference kinds whose name is a class name, written as in the source. */ diff --git a/src/resolution/index.ts b/src/resolution/index.ts index 24a5ad5ecf..cac05d6ce9 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -2641,6 +2641,11 @@ export class ReferenceResolver { return this.frameworks.map((f) => f.name); } + /** The project file a module specifier written in `fromFile` names, as its import resolves it; null when none. */ + resolveModuleFile(specifier: string, fromFile: string, language: Language): string | null { + return resolveImportPath(specifier, fromFile, language, this.context); + } + /** True when a detected router binds navigation calls to its routes (see {@link reopenNavigationsFor}). */ hasNavigationRouters(): boolean { return this.frameworks.some((f) => f.navigation !== undefined); From 80e4b459ff477d5a0bbdacf38a59939d2bffa6f3 Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Mon, 12 Oct 2026 20:05:25 +0000 Subject: [PATCH 2/5] fix(sync): an import moves to an added file that outranks the one it found (#2529) An import written without an extension that resolved to one file, `./button` to `button.vue` while `button.ts` was missing, kept that edge and what it binds when `button.ts` was added beside it, though the resolver now finds `button.ts` first: sync only retried imports that had failed. A fresh index links the added file. When a sync adds a file, the module edges into the files an import of the same name finds instead (`dir/name.*` under another extension, the folder's own `dir/name/index.*` or `dir/name/__init__.py`) whose specifier now resolves to the added file go back to the sweep, and their importers join the added module's importers with that statement, so the names it binds are re-resolved too. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/sync-module-importers.test.ts | 20 ++++++ src/db/queries.ts | 42 ++++++++++++ src/index.ts | 5 +- src/resolution/module-importers.ts | 85 +++++++++++++++++++++++-- 5 files changed, 147 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe4ce4961d..b74c5d37ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In JavaScript and TypeScript, a file that re-exports a module, like an `index.ts` with `export * from './button'` or `export { Button } from './button'`, now counts as importing that module, the way an `import` statement does. `codegraph affected` and a module's dependents now include the barrels that re-export it; before, a barrel made only of `export *` lines showed no dependency at all. Re-index JavaScript and TypeScript projects after upgrading. - `codegraph sync` and the file watcher now also update the files that import through an `index.ts` re-exporting another barrel, like `export * from './dropdown-menu'`, when that inner barrel is deleted or added. Before, their imports stayed linked through the deleted barrel, or stayed linked the way they were while it was missing, until each importing file changed or the project was re-indexed. - An import in a Vue, Svelte or Astro component now links the component to the file it imports, as an import in a script does, and so does an import of a scoped package that a `tsconfig` path or a workspace package maps to a project file, like `import { debounce } from '@excalidraw/common'`. Before, only the names such an import binds were linked, so a barrel or a module imported for its side effects showed no dependency from those files in `codegraph affected`, and `codegraph sync` could not update them when a barrel they import through was deleted or added. `codegraph sync` and the file watcher also link such an import again, and one written as `.` or `..`, when the file it names comes back. Re-index JavaScript, TypeScript, Vue, Svelte and Astro projects after upgrading. +- `codegraph sync` and the file watcher now move an import written without an extension, like `import { sizeOf } from './button'`, to a file added beside the one it found before when the new file comes first, such as `button.ts` next to `button.vue`. Before, the import and the names it binds stayed linked to `button.vue` until the importing file changed or the project was re-indexed. JavaScript, TypeScript, Vue, Svelte and Astro. - In C, C++ and Objective-C, a struct or class member declared as a pointer, reference, array or function, like `SharedState* shared;`, `jv elements[];` or `virtual Status Put(…) = 0;`, no longer makes the struct look like it inherits from the member's type. Before, `codegraph_explore` and impact listed those types as base classes, every struct holding a pointer to a type as one of its subclasses, and a method of that struct as an override of the type's method with the same name. Real base classes like `class Derived : public Base`, Objective-C superclasses and Go's embedded fields are unchanged. Re-index C, C++ and Objective-C projects after upgrading. - In C++, a call on a variable, parameter or member declared through a `typedef` or `using` alias now links to the method of the type the alias names, with the alias looked up the way C++ does it: in the calling function, its class and the classes that class inherits from, then the namespaces around it. In google/leveldb, `table_.Insert(buf)` on a `Table table_;` declared next to `typedef SkipList Table;` now reaches `SkipList::Insert` instead of the unrelated `HandleTable::Insert`, and `Table::Iterator iter(&table_);` reaches `SkipList::Iterator`'s constructor. A class that only shares the alias's name is no longer taken for it, and when the aliased type has no such method, as with an alias of `std::vector` or of a template parameter, no method is guessed from the receiver's name. Re-index C++ projects after upgrading. - In C++, a call on a variable, parameter or member declared with template arguments nested inside its template arguments, like `autovector>` or `Striped>`, now links to the method of the class it is declared as. Before, the nested arguments hid that class: in RocksDB, `files_marked_for_compaction_.clear()` on such an `autovector` linked to an unrelated `CompactionInputFiles::clear` that only shares words with the variable's name, and `mutex_.Get(key)` on a `Striped` linked to nothing. Re-index C++ projects after upgrading. diff --git a/__tests__/sync-module-importers.test.ts b/__tests__/sync-module-importers.test.ts index 57f6f311c7..2a8a524b17 100644 --- a/__tests__/sync-module-importers.test.ts +++ b/__tests__/sync-module-importers.test.ts @@ -133,9 +133,29 @@ const without = (files: Files, gone: string[]): Files => const only = (files: Files, kept: string[]): Files => Object.fromEntries(Object.entries(files).filter(([file]) => kept.includes(file))); +/** + * A file beside the one an extensionless import resolved to, which outranks + * it: a component's `./button` names `button.ts` when it is there, and + * `button.vue` when it is not — `sizeOf`, which both export, from the one or + * the other, and `onlyVue`, which only `button.vue` exports, through it or by + * its name. + */ +const OUTRANKING: Files = { + 'package.json': '{"name":"app"}', + 'src/widgets/button.vue': + '\n\n', + 'src/widgets/button.ts': + "export default function button() { return 'b'; }\nexport function buttonClass() { return 'c'; }\nexport function sizeOf() { return 2; }\n", + 'src/widgets/group.vue': + "\n\n", + 'src/widgets/panel.ts': "import button from './button';\nexport function panel() { return button(); }\n", +}; +const OUTRANKING_FILES = ['src/widgets/button.ts']; + describe.each([ ['Python package and module', PYTHON, PYTHON_BARRELS], ['ES module barrel', TS, TS_BARRELS], + ['file outranking the one an import resolved to', OUTRANKING, OUTRANKING_FILES], ])('sync re-resolves the imports through a re-exporting file: %s', (_label, files, barrels) => { it('when it is deleted', async () => { const root = tempRoot(); diff --git a/src/db/queries.ts b/src/db/queries.ts index 010d32fd36..f6b33befdd 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -3189,6 +3189,48 @@ export class QueryBuilder { })); } + /** + * The module edges (`imports` edges into a file node) into the files an + * import of `dir/name` can find instead of a file added there: `dir/name.*` + * under another extension, and the folder's own entry, `dir/name/index.*` + * or `dir/name/__init__.py`. With the source file and language a + * resurrection needs. + */ + getModuleImportsNamed( + dir: string, + name: string + ): Array { + const base = dir ? `${dir}/${name}` : name; + // `x.` up to `x/` and `x/index.` up to `x/index/` ('/' is the byte after '.'). + const rows = this.db + .prepare( + `SELECT e.*, tgt.file_path AS target_file_path, src.file_path AS source_file_path, src.language AS source_language + FROM nodes tgt + JOIN edges e ON e.target = tgt.id AND e.kind = 'imports' + JOIN nodes src ON src.id = e.source + WHERE tgt.kind = 'file' + AND ((tgt.file_path >= ? AND tgt.file_path < ?) OR (tgt.file_path >= ? AND tgt.file_path < ?) OR tgt.file_path = ?)` + ) + .all(`${base}.`, `${base}/`, `${base}/index.`, `${base}/index/`, `${base}/__init__.py`) as Array< + EdgeRow & { target_file_path: string; source_file_path: string; source_language: Language } + >; + return rows + // Not a deeper path that only shares the prefix (`dir/name.d/x.ts`). + .filter((row) => { + const file = row.target_file_path; + if (file === `${base}/__init__.py`) return true; + const index = `${base}/index.`; + return !(file.startsWith(index) ? file.slice(index.length) : file.slice(base.length + 1)).includes('/'); + }) + .map((row) => ({ + ...rowToEdge(row), + edgeId: row.id, + targetFilePath: row.target_file_path, + sourceFilePath: row.source_file_path, + sourceLanguage: row.source_language, + })); + } + /** The files with a resolution edge into one of `targetIds`. */ getFilesWithEdgesInto(targetIds: string[]): string[] { const files = new Set(); diff --git a/src/index.ts b/src/index.ts index d98fac584e..bd9e11a55a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1118,7 +1118,10 @@ export class CodeGraph { const file = this.queries.getFileByPath(filePath); return file ? [{ path: filePath, language: file.language }] : []; }); - const reopened = reopenReferencesThroughAddedFiles(this.queries, createImportReader(this.projectRoot), added, edgesBeforeResolution); + const reopened = reopenReferencesThroughAddedFiles( + this.queries, createImportReader(this.projectRoot), added, edgesBeforeResolution, + (specifier, fromFile, language) => this.resolver.resolveModuleFile(specifier, fromFile, language) + ); if (process.env.CODEGRAPH_SYNTH_TIMINGS) console.error(`[phase-timing] sync-added-module-importers: ${Date.now() - tAdded}ms (${reopened} refs re-opened)`); } } else { diff --git a/src/resolution/module-importers.ts b/src/resolution/module-importers.ts index 705b62c85a..4dec64f3bb 100644 --- a/src/resolution/module-importers.ts +++ b/src/resolution/module-importers.ts @@ -20,7 +20,7 @@ import * as fs from 'fs'; import * as path from 'path'; import type { QueryBuilder } from '../db/queries'; -import type { Language, UnresolvedReference } from '../types'; +import type { Edge, Language, UnresolvedReference } from '../types'; import type { ImportMapping, ReExport } from './types'; import { extractImportMappings, extractReExports, pythonModulePath, REEXPORT_MAX_DEPTH } from './import-resolver'; import { resurrectRefFromDroppedEdge } from './resurrect-ref'; @@ -123,6 +123,9 @@ export function reopenReferencesThroughRemovedFiles( return reopenEdges(queries, bindings, (e) => !gone.has(e.targetFilePath)); } +/** Resolves a module specifier written in `fromFile` as its import does: the project file it names, or null. */ +export type ModuleResolver = (specifier: string, fromFile: string, language: Language) => string | null; + /** * The other half: after a sync's retries, re-open the references the files * importing any of `added` make through those imports — the edges a name @@ -130,28 +133,96 @@ export function reopenReferencesThroughRemovedFiles( * `resolvedUpToEdgeId`: later ones were just resolved with the modules there) * and the references that failed, the import of the module itself among * them. (The failed-import retry looks those up by the module's name, and - * skips a name as common as `models` or `test`.) Returns the number re-opened. + * skips a name as common as `models` or `test`.) With `resolveModule`, also an + * import that resolved to a file beside an added one that now outranks it + * (`./button` to `button.vue` while `button.ts` was missing), and what that + * statement binds. Returns the number re-opened. */ export function reopenReferencesThroughAddedFiles( queries: QueryBuilder, reader: ImportReader, added: ReadonlyArray<{ path: string; language: Language }>, resolvedUpToEdgeId: number, + resolveModule?: ModuleResolver, ): number { - const bindings = bindingsOf(importersOf(queries, added, reader), reader); + const outranked = resolveModule ? outrankedImports(queries, added, resolveModule) : []; + const bindings = bindingsOf(importersOf(queries, added, reader, outranked), reader); const failed = [...bindings].flatMap(([file, { modules, names }]) => queries.getFailedReferencesThrough(file, modules, names)); - return reopenEdges(queries, bindings, (e) => e.edgeId <= resolvedUpToEdgeId) + queries.reopenFailedReferences(failed); + return reopenModuleEdges(queries, outranked) + + reopenEdges(queries, bindings, (e) => e.edgeId <= resolvedUpToEdgeId) + queries.reopenFailedReferences(failed); +} + +/** An import's module edge into a file that an added file now outranks. */ +interface OutrankedImport { + edge: Edge & { edgeId: number; sourceFilePath: string; sourceLanguage: Language }; + specifier: string; + added: string; +} + +/** + * The module edges an import resolved into a file beside one of `added` — + * another extension of its name, or its folder's own entry — when the + * import's specifier now names the added file: `./button` resolved to + * `button.vue` while `button.ts` was missing, and `button.ts` comes first. + */ +function outrankedImports( + queries: QueryBuilder, + added: ReadonlyArray<{ path: string; language: Language }>, + resolveModule: ModuleResolver, +): OutrankedImport[] { + const out: OutrankedImport[] = []; + for (const file of added) { + if (!BINDING_LANGUAGES.has(file.language)) continue; + const { dir, name } = importedName(file.path); + for (const edge of queries.getModuleImportsNamed(dir, name)) { + const specifier = edge.metadata?.refName; + if (edge.targetFilePath === file.path || typeof specifier !== 'string') continue; + if (resolveModule(specifier, edge.sourceFilePath, edge.sourceLanguage) === file.path) { + out.push({ edge, specifier, added: file.path }); + } + } + } + return out; +} + +/** The folder and name an import finds a file by: `a/b.ts` → `a`, `b`; a folder's own entry `a/b/index.ts` → `a`, `b`. */ +function importedName(filePath: string): { dir: string; name: string } { + const slash = filePath.lastIndexOf('/'); + const base = filePath.slice(slash + 1); + const dir = slash < 0 ? '' : filePath.slice(0, slash); + if (base.startsWith('index.') || base === '__init__.py') { + const up = dir.lastIndexOf('/'); + return { dir: up < 0 ? '' : dir.slice(0, up), name: dir.slice(up + 1) }; + } + const dot = base.indexOf('.', 1); + return { dir, name: dot < 0 ? base : base.slice(0, dot) }; +} + +/** Swap the outranked module edges for their references, for the sweep. Returns how many. */ +function reopenModuleEdges(queries: QueryBuilder, outranked: readonly OutrankedImport[]): number { + const edgeIds: number[] = []; + const refs: UnresolvedReference[] = []; + for (const { edge } of outranked) { + const ref = resurrectRefFromDroppedEdge(edge); + if (!ref || edgeIds.includes(edge.edgeId)) continue; + edgeIds.push(edge.edgeId); + refs.push(ref); + } + if (refs.length > 0) queries.replaceResolutionEdgesWithUnresolvedRefs(edgeIds, refs); + return refs.length; } /** * The files with an import statement naming one of `files`, or one of the * modules re-exporting from them, other than `files` themselves (going away, - * or just resolved with the others in place). + * or just resolved with the others in place); `outranked` imports name the + * added file they now resolve to. */ function importersOf( queries: QueryBuilder, files: ReadonlyArray<{ path: string; language: Language }>, reader: ImportReader, + outranked: readonly OutrankedImport[] = [], ): Importers { const importers: Importers = new Map(); const changed = new Set(files.map((file) => file.path)); @@ -177,6 +248,10 @@ function importersOf( for (const i of queries.getImportsInto(`file:${module.path}`, module.path)) { if (i.refName !== null) add(i.filePath, i.language, i.refName, module); } + if (depth > 1) continue; + for (const o of outranked) { + if (o.added === module.path) add(o.edge.sourceFilePath, o.edge.sourceLanguage, o.specifier, module); + } } const python = level.filter((module) => module.language === 'python'); if (python.length > 0) addPythonImporters(queries, python, reader, add); From 85201e95b3538853ab1ffd921ca7060903d576ca Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Mon, 12 Oct 2026 22:16:09 +0000 Subject: [PATCH 3/5] fix(cpp): a smart pointer's held class lacking a method gets no edge only when its bases are known (#2530) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit matchCppHeldTypeCall (#2440) returned null (no edge) for a `->` call on a `std::unique_ptr`/`std::shared_ptr`/`std::optional` whose held class lacks the method, unless that class is a class template. #2493 found the gap in the same rule for raw pointers and values: a base the index can't link may declare the method. protobuf's `class FlatAllocator : public decltype(ApplyTypeList(…))` has no extends edge to `FlatAllocatorImpl`, and rocksdb's `WritePreparedTxnDB` derives from an alias each `#if` arm declares differently. Both paths now share one verdict, cppLacksMethod: no class template, and cppBasesKnown (every base on the way is linked to a class, or is a library type). Otherwise the call goes on as before. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- __tests__/cpp-smart-pointer-receiver.test.ts | 37 ++++++++++++++++++-- src/resolution/name-matcher.ts | 26 +++++++++----- 3 files changed, 53 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b74c5d37ef..f3605fc29d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,7 +94,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In C++, a call on a variable, parameter or member declared as a standard-library or other outside type with a lowercase name, like `std::string`, `std::vector`, `std::set` or `absl::flat_hash_set`, no longer links to a project method that only shares the method's name. In google/leveldb, `saved_key_.clear()` on a `std::string saved_key_;` reached `Slice::clear`, and in protocolbuffers/protobuf, `proto.append(…)` on a `std::string` reached an unrelated `LeftoverBuffer::append`. The same goes for `->` through a raw pointer like `std::string* out`, while `->` through a smart pointer, iterator or optional still links to the type it holds. Comments also no longer pass for a variable's declaration, so more calls now link to the method of the type the variable is really declared with. Re-index C++ projects after upgrading. - In C++, a class written with attribute macros the parser can't read now keeps its members. That covers several macros between `class` and the class name, like protocolbuffers/protobuf's generated `class PROTOBUF_EXPORT PROTOBUF_FUTURE_ADD_EARLY_WARN_UNUSED Any final`, a macro with arguments there, like RocksDB's `struct ALIGN_AS(64U) HandleImpl`, or one before a template specialization, and macros on a member: in front of it (`PROTOBUF_FUTURE_ADD_EARLY_NODISCARD`), between a pointer and its name (`PROTOBUF_NONNULL`), after its parameters or its name (`ABSL_ATTRIBUTE_LIFETIME_BOUND`, google/leveldb's `LOCKS_EXCLUDED(mu_)` and `GUARDED_BY(mu_)`), or alone on a line above a comment ({fmt}'s `FMT_BEGIN_EXPORT`). Before, protobuf's generated message classes were missing and their members, like `GetDescriptor`, `GetReflection` and `unknown_fields`, were indexed as functions outside any class, and leveldb's annotated methods were indexed under the annotation's name, so calls to them linked to nothing. Re-index C++ projects after upgrading. - In C++, a call on a range-based `for` loop's variable, like `suite->SetVerbose(verbose)` inside `for (ConformanceTestSuite *suite : suites)`, now links to the method of the type the loop declares. Before, the loop's declaration was never read, so the call was matched by the variable's name alone: in RocksDB, `log->file()` on a `log::Writer` went to `BlobLogWriter::file`, and a loop variable named like a member of its class was taken for that member. The variable counts only inside its loop. Re-index C++ projects after upgrading. -- In C++, a call made with `->` on a variable, parameter or member declared as a `std::unique_ptr`, `std::shared_ptr` or `std::optional`, like `iter->Valid()` on a `std::unique_ptr iter`, now links to that method of the type it holds, or of a class that type inherits from, with the type looked up the way C++ does it, so a class of the same name nested elsewhere is not mistaken for it. Before, the call went to whichever project method shared its name: in facebook/rocksdb, `iter->Valid()` reached `ArenaWrappedDBIter::Valid` and `fs->GetFileSize(…)` on a `std::shared_ptr` reached a test's stand-in file system, and calls like `it->Next()` were often not linked at all. When what it holds is a template parameter or a standard-library type, like `std::unique_ptr>`, the call no longer links to a project method. Re-index C++ projects after upgrading. +- In C++, a call made with `->` on a variable, parameter or member declared as a `std::unique_ptr`, `std::shared_ptr` or `std::optional`, like `iter->Valid()` on a `std::unique_ptr iter`, now links to that method of the type it holds, or of a class that type inherits from, with the type looked up the way C++ does it, so a class of the same name nested elsewhere is not mistaken for it. Before, the call went to whichever project method shared its name: in facebook/rocksdb, `iter->Valid()` reached `ArenaWrappedDBIter::Valid` and `fs->GetFileSize(…)` on a `std::shared_ptr` reached a test's stand-in file system, and calls like `it->Next()` were often not linked at all. When what it holds is a template parameter or a standard-library type, like `std::unique_ptr>`, or a class that has no method of that name and neither do the classes it derives from, the call no longer links to a project method; a class derived from something that can't be followed, like a `decltype(…)`, may still have it, so that call is matched as before. Re-index C++ projects after upgrading. - In C++, a call a method makes with no receiver, or through `this->`, now links to the method C++ itself would call: one of the method's own class, else one the class inherits from a base class, else one of a class it is nested in. The same goes for a call in a lambda inside the method, and of a class's overloads the one the call's arguments fit is taken. Before, the call was matched by its name alone, so another class's method of that name could win because it was declared nearer or found first: in protocolbuffers/protobuf, every generated message's `Swap` and move assignment calling `InternalSwap(…)` reached `Any::InternalSwap`, and the `GetArena()` messages inherit from `MessageLite` reached an arena helper's `GetArena`, while googletest's `~linked_ptr() { depart(); }`, vendored in facebook/rocksdb, reached `linked_ptr_internal::depart`. Calls C++ doesn't look up in the class are matched as before: a free function's, a call to a parameter or local of that name, and a template's bare call into a base class that depends on its template parameters. Re-index C++ projects after upgrading. Thanks @danusha2345 for reporting the C++ case in #1952. - In C++, a call on the variable of a range-based `for` loop over `auto` elements, like `cfd->Ref()` inside facebook/rocksdb's `for (auto cfd : cfds)`, now links to the method of the type the loop goes over. That type is read from what the loop iterates: a `std::vector`, `std::set`, `absl::Span` or similar container, protocolbuffers/protobuf's `RepeatedPtrField` or RocksDB's `autovector`, whether a variable, parameter or member holds it, an `auto` variable is set to it or a function or method returns it, and a project class you loop over through its `begin()`, like RocksDB's `ColumnFamilySet`. Before, the call was matched by the variable's name, or by an earlier variable of that name: in RocksDB, `blob_file.GetBlobFileNumber()` over a list of `BlobFileGarbage` reached `BlobFileAddition::GetBlobFileNumber`, and in protobuf's Objective-C generator, calls over a list of `std::unique_ptr` reached other generators' methods. A call on an element of a standard-library type, like `file.find(…)` over a `std::vector`, no longer links to a project method, and `->` on an element held in a `std::unique_ptr` or `std::shared_ptr` reaches the type it holds. Re-index C++ projects after upgrading. - In C++, a call on a variable, parameter or member declared as one of your classes, like `iter->Valid()` on an `Iterator* iter` or `it.Valid()` on a `const Iterator& it`, now links to that method of the class C++ itself would find for the type where the call is written, or of a class it inherits from. Before, the method was picked by the type's last name, so a class of the same name nested elsewhere could win: in facebook/rocksdb, calls on an `Iterator` reached `MemTableRep::Iterator`, which declares `Valid` and `Next` itself while the `Iterator` the code means inherits them, google/leveldb's reached `SkipList::Iterator`, and in protocolbuffers/protobuf the Java generator's `context->options()` reached the hpb generator's `Context` and `text_parser.Parse(…)` on a `TextFormat::Parser` reached the `.proto` parser. When that class and the classes it derives from have no method of that name, the call no longer links to an unrelated class's method that shares it, as protobuf's `field->full_name()` on a `FieldDescriptor*` did. A type is also found through a `using` declaration the file writes, like protobuf's `using google::protobuf::Descriptor;`, before a class of the same name declared at global scope elsewhere in the repository; this also applies to base classes and to the type a smart pointer holds. Re-index C++ projects after upgrading. diff --git a/__tests__/cpp-smart-pointer-receiver.test.ts b/__tests__/cpp-smart-pointer-receiver.test.ts index 842e9755e5..5a71abca28 100644 --- a/__tests__/cpp-smart-pointer-receiver.test.ts +++ b/__tests__/cpp-smart-pointer-receiver.test.ts @@ -17,9 +17,9 @@ * `Iterator` the call means inherits `Valid` from `IteratorBase`. A template * parameter, a library type and an alias of a type outside the project hold * no project class, so those calls get no edge; neither does a call the held - * class has no method for (unless a class template's specialization may - * declare it). A held type the project doesn't declare — a generated message - * — leaves the call as it was. + * class has no method for (unless a class template's specialization, or a + * base the index doesn't link, may declare it). A held type the project + * doesn't declare — a generated message — leaves the call as it was. */ import { describe, it, expect, afterEach } from 'vitest'; import * as fs from 'fs'; @@ -447,4 +447,35 @@ describe('what keeps the call as it was', () => { cg.close(); } }); + + it('a held class with a base the index does not link, which may declare the method', async () => { + // protobuf's FlatAllocator derives from a `decltype(…)` that names FlatAllocatorImpl. + const cg = await indexed({ + 'src/google/protobuf/descriptor.cc': lines( + '#include ', + 'namespace google {', + 'namespace protobuf {', + 'namespace internal {', + 'template ', + 'class FlatAllocatorImpl {', + ' public:', + ' void PlanArray(int array_size) {}', + '};', + 'template ', + 'FlatAllocatorImpl ApplyTypeList(int);', + 'class FlatAllocator : public decltype(ApplyTypeList(0)) {};', + '} // namespace internal', + 'void PlanAllocationSize(std::unique_ptr& alloc) {', + ' alloc->PlanArray(1);', + '}', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + expect(calls(cg, 'google::protobuf::PlanAllocationSize')).toEqual(['google::protobuf::internal::FlatAllocatorImpl::PlanArray']); + } finally { + cg.close(); + } + }); }); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 6df55a82ef..0c87abaa6a 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -9576,8 +9576,9 @@ function cppStdHeldType(raw: string): string | null { * - The member of the class it names, or of a class that class derives from. * - null when no project method is called: the type is a template * parameter's, a library type (`std::unique_ptr>`) or an - * alias of one, or a class that lacks the method (but a class template's - * specialization may declare it, so not there). + * alias of one, or a class that surely lacks the method (cppLacksMethod: + * not when a class template's specialization or a base the index doesn't + * link may declare it). * - undefined when `raw` declares no such holder, or the type it holds is * none the project declares (a generated message, a class the index * doesn't name): the call goes on as before. @@ -9604,7 +9605,7 @@ function matchCppHeldTypeCall(raw: string, methodName: string, ref: UnresolvedRe } const method = cppMethodOf(found.node, methodName, ref, context); if (method) return { original: ref, targetNodeId: method.id, confidence: found.exact ? 0.9 : 0.85, resolvedBy: 'instance-method' }; - return cppTemplateParameters(found.node, context).size > 0 ? undefined : null; + return cppLacksMethod(found.node, context) ? null : undefined; } const CPP_CLASS_KINDS: ReadonlySet = new Set(['class', 'struct', 'union']); @@ -9630,10 +9631,9 @@ function cppCallSite(caller: Node, ref: UnresolvedRef, context: ResolutionContex * `MemTableRep::Iterator::Valid`, one of 22 classes named `Iterator`, while the * `Iterator` the code means inherits `Valid` from `IteratorBase`. * - The member of that class, or of a class it derives from. - * - null when that class has no such method: protobuf's descriptor accessors - * aren't indexed, and `field->full_name()` reached unrelated classes' by - * the method's name. (A class template's specialization may declare it, so - * not there.) + * - null when that class surely has no such method (cppLacksMethod): + * protobuf's descriptor accessors aren't indexed, and `field->full_name()` + * reached unrelated classes' by the method's name. * - undefined when the declaration may be another variable's (read from * elsewhere, past one of the receiver the scan couldn't read, or not taking * that operator), or the lookup doesn't place the type: a library type, a @@ -9664,7 +9664,17 @@ function matchCppDeclaredTypeCall( if (!found?.exact || !CPP_CLASS_KINDS.has(found.node.kind)) return undefined; const method = cppMethodOf(found.node, methodName, ref, context); if (method) return { original: ref, targetNodeId: method.id, confidence: 0.9, resolvedBy: 'instance-method' }; - return cppTemplateParameters(found.node, context).size === 0 && cppBasesKnown(found.node, context) ? null : undefined; + return cppLacksMethod(found.node, context) ? null : undefined; +} + +/** + * Whether a C++ class with no method of a name, its own or one it inherits + * through its base edges, surely has none: it is no class template, whose + * specializations may declare members the template doesn't, and every base on + * the way is one the walk follows (cppBasesKnown). + */ +function cppLacksMethod(cls: Node, context: ResolutionContext): boolean { + return cppTemplateParameters(cls, context).size === 0 && cppBasesKnown(cls, context); } /** From c19f0284f80561d60bc68d77b48b6f9367a93f23 Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Mon, 12 Oct 2026 22:17:33 +0000 Subject: [PATCH 4/5] fix(cpp): a declaration inside a string literal is not a receiver's declaration (#2531) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C++ receiver inference reads back from a call for a line that declares the receiver, blanking comments (cppCodeOf) but not string literals. Text inside one was taken for the declaration: - protobuf's upb generator emits C code from raw strings: `GenerateRepeatedSetters(upb::FieldDefPtr field, …)` writes `upb_MiniTableField field = $field_init$;` inside an `R"cc( … )cc"`, then calls `field.ctype()` on its own parameter. The scan found the template's line first, and the call got no edge (a C struct has no methods). - rocksdb's log messages read as declarations: `"… in WriteBatch handler; …"` made `handler` a `WriteBatch`, and `"… remote compaction (…"` gave `compaction` the type `remote`, so calls fell to guesses by the method's name. New cpp-raw-strings.ts reads a file once, when it holds `R"`, as the compiler does (comments, string and character literals, raw strings with their delimiters), and keeps the column ranges each raw string takes per line. The receiver scan, in the file and its header, blanks those and the contents of the line's ordinary string and character literals before cppCodeOf. A character literal holding a quote (`'"'`), a digit separator (`1'000`) and an `R"(` inside a comment don't hide the code after them. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + .../cpp-string-literal-declarations.test.ts | 182 ++++++++++++++++++ src/resolution/cpp-raw-strings.ts | 135 +++++++++++++ src/resolution/name-matcher.ts | 11 +- 4 files changed, 327 insertions(+), 2 deletions(-) create mode 100644 __tests__/cpp-string-literal-declarations.test.ts create mode 100644 src/resolution/cpp-raw-strings.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f3605fc29d..729069f0ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In C++, a call a method makes with no receiver, or through `this->`, now links to the method C++ itself would call: one of the method's own class, else one the class inherits from a base class, else one of a class it is nested in. The same goes for a call in a lambda inside the method, and of a class's overloads the one the call's arguments fit is taken. Before, the call was matched by its name alone, so another class's method of that name could win because it was declared nearer or found first: in protocolbuffers/protobuf, every generated message's `Swap` and move assignment calling `InternalSwap(…)` reached `Any::InternalSwap`, and the `GetArena()` messages inherit from `MessageLite` reached an arena helper's `GetArena`, while googletest's `~linked_ptr() { depart(); }`, vendored in facebook/rocksdb, reached `linked_ptr_internal::depart`. Calls C++ doesn't look up in the class are matched as before: a free function's, a call to a parameter or local of that name, and a template's bare call into a base class that depends on its template parameters. Re-index C++ projects after upgrading. Thanks @danusha2345 for reporting the C++ case in #1952. - In C++, a call on the variable of a range-based `for` loop over `auto` elements, like `cfd->Ref()` inside facebook/rocksdb's `for (auto cfd : cfds)`, now links to the method of the type the loop goes over. That type is read from what the loop iterates: a `std::vector`, `std::set`, `absl::Span` or similar container, protocolbuffers/protobuf's `RepeatedPtrField` or RocksDB's `autovector`, whether a variable, parameter or member holds it, an `auto` variable is set to it or a function or method returns it, and a project class you loop over through its `begin()`, like RocksDB's `ColumnFamilySet`. Before, the call was matched by the variable's name, or by an earlier variable of that name: in RocksDB, `blob_file.GetBlobFileNumber()` over a list of `BlobFileGarbage` reached `BlobFileAddition::GetBlobFileNumber`, and in protobuf's Objective-C generator, calls over a list of `std::unique_ptr` reached other generators' methods. A call on an element of a standard-library type, like `file.find(…)` over a `std::vector`, no longer links to a project method, and `->` on an element held in a `std::unique_ptr` or `std::shared_ptr` reaches the type it holds. Re-index C++ projects after upgrading. - In C++, a call on a variable, parameter or member declared as one of your classes, like `iter->Valid()` on an `Iterator* iter` or `it.Valid()` on a `const Iterator& it`, now links to that method of the class C++ itself would find for the type where the call is written, or of a class it inherits from. Before, the method was picked by the type's last name, so a class of the same name nested elsewhere could win: in facebook/rocksdb, calls on an `Iterator` reached `MemTableRep::Iterator`, which declares `Valid` and `Next` itself while the `Iterator` the code means inherits them, google/leveldb's reached `SkipList::Iterator`, and in protocolbuffers/protobuf the Java generator's `context->options()` reached the hpb generator's `Context` and `text_parser.Parse(…)` on a `TextFormat::Parser` reached the `.proto` parser. When that class and the classes it derives from have no method of that name, the call no longer links to an unrelated class's method that shares it, as protobuf's `field->full_name()` on a `FieldDescriptor*` did. A type is also found through a `using` declaration the file writes, like protobuf's `using google::protobuf::Descriptor;`, before a class of the same name declared at global scope elsewhere in the repository; this also applies to base classes and to the type a smart pointer holds. Re-index C++ projects after upgrading. +- In C++, text inside a string literal is no longer taken for a variable's declaration. That covers the C code protocolbuffers/protobuf's upb generator writes in a raw string (`R"cc( … upb_MiniTableField field = $field_init$; … )cc"`) and log messages like facebook/rocksdb's `"… in WriteBatch handler; …"`. Before, a later call like `field.ctype()` on the function's own `upb::FieldDefPtr field` parameter, or `handler->PutCF(…)` on a `WriteBatch::Handler* handler`, was matched against the type that text seemed to name, so it linked to nothing or to another class's method. Re-index C++ projects after upgrading. - Framework support now switches on when an app's `package.json` sits three or more folders deep, as with an ASP.NET solution's Angular or React app in `src/Web/ClientApp/`, or a React app in a workspace folder like `web/ui/mantine-ui/`. Before, only the project root and the first two folder levels were read, so frameworks such as Angular, React Router, Vue Router, SvelteKit and TanStack Router could go undetected there: an Angular app's routes, its `routerLink` and `router.navigate(…)` navigation and its template's event bindings were missing, so a handler used only from a `(click)` binding had no callers, and a React app's `navigate('/login')` calls didn't reach their route. Re-index projects laid out this way after upgrading. - In a repository that mixes languages, a web framework's naming conventions now link only code written in that framework's own languages. Before, Express's also linked Go and Python code: in etcd a Go method's `*zap.Logger` result type linked to the method itself, and in Harbor a Python `self.jaeger.validate()` call linked to another class's `validate`. Links that cross languages on purpose, like React Native and Expo native modules, Swift and Objective-C, Drupal's `routing.yml`, SvelteKit's `$lib/…` imports in `.ts` files and a Razor page's `@model`, are unchanged. - In C and C++, a call written from the global scope, like `::leveldb::RepairDB(…)` or `::hpb::CreateMessage(…)`, now links to the function of exactly that name, and one through a namespace alias, like protobuf's `::_pbi::…` calls, to the function in the aliased namespace. Before, these calls were dropped unless the repository also held Swift and Objective-C files, and even then a name spelled out in full, like `::google::protobuf::internal::TSanWrite(…)`, linked to nothing, while `::operator delete(…)` linked to one class's own `operator delete`. A global call never links to a function or method of the same name inside a namespace or class, such as a wrapper `open()` that calls the C library's `::open(…)`, and `::std::…` and `::testing::…` calls stay unlinked unless that library's source is part of your project. Re-index C and C++ projects after upgrading. diff --git a/__tests__/cpp-string-literal-declarations.test.ts b/__tests__/cpp-string-literal-declarations.test.ts new file mode 100644 index 0000000000..c0076eb547 --- /dev/null +++ b/__tests__/cpp-string-literal-declarations.test.ts @@ -0,0 +1,182 @@ +/** + * A declaration written inside a C++ string literal is text, not code, so + * receiver inference doesn't take it for the receiver's declaration. + * + * protocolbuffers/protobuf's upb generator emits C code from raw string + * templates: `GenerateRepeatedSetters(upb::FieldDefPtr field, …)` writes + * `upb_MiniTableField field = $field_init$;` inside an `R"cc( … )cc"`, then + * calls `field.ctype()` on its own parameter. Reading back from the call + * found the template's line first, took `field` for a `upb_MiniTableField` — + * a C struct with no methods — and the call got no edge (before #2493, a + * guess by the method's name). + * + * A raw string can span many lines and hold quotes and comment markers, so + * its lines are found by reading the file once, as the compiler does. An + * ordinary string's contents are blanked on the line being read. + */ +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'; +import type { Node } from '../src/types'; + +const roots: string[] = []; +afterEach(() => { + for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true }); +}); + +async function indexed(files: Record): Promise { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-strdecl-')); + roots.push(root); + for (const [rel, content] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + return CodeGraph.init(root, { index: true }); +} + +function callable(cg: CodeGraph, qualifiedName: string): Node { + const node = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName); + if (!node) throw new Error(`no function or method ${qualifiedName}`); + return node; +} + +/** `calls` callees of a function or method, as `qualifiedName`. */ +function calls(cg: CodeGraph, caller: string): string[] { + return cg + .getCallees(callable(cg, caller).id) + .filter((r) => r.edge.kind === 'calls') + .map((r) => r.node.qualifiedName) + .sort(); +} + +const lines = (...l: string[]): string => [...l, ''].join('\n'); + +/** upb's field wrapper, and the C struct a generated template declares. */ +const UPB = { + 'upb/reflection/def.hpp': lines( + 'namespace upb {', + 'class FieldDefPtr {', + ' public:', + ' int ctype() const { return 0; }', + ' const char* name() const { return ""; }', + '};', + '} // namespace upb', + ), + 'upb/mini_table/field.h': lines( + 'typedef struct upb_MiniTableField {', + ' int number;', + '} upb_MiniTableField;', + ), +}; + +describe('a declaration inside a string literal is not the receiver\'s', () => { + it('reproduction: upb\'s generator declares `field` in the C code a raw string emits', async () => { + const cg = await indexed({ + ...UPB, + 'upb_generator/c/generator.cc': lines( + '#include "upb/reflection/def.hpp"', + '#include "upb/mini_table/field.h"', + 'namespace upb {', + 'namespace generator {', + 'void Emit(const char* text) {}', + 'void GenerateRepeatedSetters(upb::FieldDefPtr field) {', + ' Emit(R"cc(', + ' UPB_INLINE void $0_resize(void* msg, size_t size) {', + ' upb_MiniTableField field = $1;', + ' // "a quote" and ) in the template', + ' upb_Message_Resize(msg, &field, size);', + ' }', + ' )cc");', + ' if (field.ctype() == 1) Emit(field.name());', + '}', + '} // namespace generator', + '} // namespace upb', + ), + }); + try { + expect([...new Set(calls(cg, 'upb::generator::GenerateRepeatedSetters'))]).toEqual([ + 'upb::FieldDefPtr::ctype', + 'upb::FieldDefPtr::name', + 'upb::generator::Emit', + ]); + } finally { + cg.close(); + } + }); + + it('an ordinary string that reads like a declaration', async () => { + const cg = await indexed({ + 'src/runner.h': lines( + 'namespace demo {', + 'class Runner {', + ' public:', + ' void Run() {}', + '};', + 'class Stub {', + ' public:', + ' void Run() {}', + '};', + 'class Printer {', + ' public:', + ' void Print(const char* text) {}', + '};', + '} // namespace demo', + ), + 'src/generator.cc': lines( + '#include "src/runner.h"', + 'namespace demo {', + 'void Generate(Printer* printer, Runner* runner) {', + ' printer->Print("Stub* runner = nullptr;");', + ' runner->Run();', + '}', + '} // namespace demo', + ), + }); + try { + expect(calls(cg, 'demo::Generate')).toEqual(['demo::Printer::Print', 'demo::Runner::Run']); + } finally { + cg.close(); + } + }); +}); + +describe('what a literal on the line does not hide', () => { + it('a declaration after a character literal holding a quote, or after a comment holding `R"(`', async () => { + const cg = await indexed({ + 'src/runner.h': lines( + 'namespace demo {', + 'class Runner {', + ' public:', + ' void Run() {}', + '};', + 'class Stub {', + ' public:', + ' void Run() {}', + '};', + 'inline Runner* MakeRunner() { return nullptr; }', + '} // namespace demo', + ), + 'src/quotes.cc': lines( + '#include "src/runner.h"', + 'namespace demo {', + 'void Quote(Stub* runner) {', + ' char q = \'"\'; int n = 1\'000; Runner* runner = MakeRunner();', + ' runner->Run();', + '}', + 'void Comment(Stub* runner) {', + ' /* R"( not a raw string */ Runner* runner = MakeRunner();', + ' runner->Run();', + '}', + '} // namespace demo', + ), + }); + try { + expect(calls(cg, 'demo::Quote')).toEqual(['demo::MakeRunner', 'demo::Runner::Run']); + expect(calls(cg, 'demo::Comment')).toEqual(['demo::MakeRunner', 'demo::Runner::Run']); + } finally { + cg.close(); + } + }); +}); diff --git a/src/resolution/cpp-raw-strings.ts b/src/resolution/cpp-raw-strings.ts new file mode 100644 index 0000000000..3eaf1b4998 --- /dev/null +++ b/src/resolution/cpp-raw-strings.ts @@ -0,0 +1,135 @@ +/** + * C and C++ string literals as text, not code, for the scans that read + * declarations off source lines (receiver inference in name-matcher.ts). + * + * A raw string literal — `R"cc( … )cc"`, which protobuf's generators use for + * the code they emit — can span many lines and hold anything, declarations + * included: upb's generator writes `upb_MiniTableField field = $field_init$;` + * inside one, and reading back from a later `field.ctype()` on the function's + * own `upb::FieldDefPtr field` parameter found that line first. Its lines are + * found by reading the file once, as the compiler does: comments, ordinary + * string and character literals, and raw strings with their delimiters. + * An ordinary string can't span lines, so its contents are blanked on the + * line being read. + */ +import type { ResolutionContext } from './types'; + +/** Column ranges, `[start, end)`, a raw string literal takes on a line (its quotes and delimiters included). */ +type Ranges = Array<[number, number]>; + +const RAW_STRING_LINES = new WeakMap>>(); + +/** Drop the per-file raw-string memo (see ReferenceResolver.clearCaches). */ +export function clearCppRawStringMemos(context: ResolutionContext): void { + RAW_STRING_LINES.delete(context); +} + +const isWordChar = (c: string | undefined): boolean => c !== undefined && /[A-Za-z0-9_$]/.test(c); + +/** + * The 1-based lines of a C or C++ file that raw string literals cover, each + * with the columns they take. Empty for a file with none. + */ +export function cppRawStringLines(file: string, context: ResolutionContext): Map { + let memo = RAW_STRING_LINES.get(context); + if (!memo) RAW_STRING_LINES.set(context, (memo = new Map())); + const hit = memo.get(file); + if (hit) return hit; + const lines = new Map(); + const text = context.readFile(file); + if (text && text.includes('R"')) { + let line = 1; + let column = 0; + const n = text.length; + let i = 0; + // Mark [from, to) of the text, which may span lines, as raw string contents. + const mark = (from: number, fromLine: number, fromColumn: number, to: number): void => { + let l = fromLine; + let start = fromColumn; + let c = fromColumn; + for (let k = from; k < to; k++) { + if (text[k] === '\n') { + if (c > start) (lines.get(l) ?? lines.set(l, []).get(l)!).push([start, c]); + l++; + start = 0; + c = 0; + } else if (text[k] !== '\r') c++; + } + if (c > start) (lines.get(l) ?? lines.set(l, []).get(l)!).push([start, c]); + }; + // Advance over [i, to), keeping line and column. + const advance = (to: number): void => { + for (; i < to; i++) { + if (text[i] === '\n') { + line++; + column = 0; + } else if (text[i] !== '\r') column++; + } + }; + while (i < n) { + const c = text[i]!; + const next = text[i + 1]; + if (c === '/' && next === '/') { + const end = text.indexOf('\n', i); + advance(end < 0 ? n : end); + } else if (c === '/' && next === '*') { + const end = text.indexOf('*/', i + 2); + advance(end < 0 ? n : end + 2); + } else if (c === '"') { + // `R"`, `u8R"`, `uR"`, `UR"`, `LR"`, not part of a longer name. + const prefix = /(?:u8|[uUL])?R$/.exec(text.slice(Math.max(0, i - 3), i)); + const open = prefix && !isWordChar(text[i - prefix[0].length - 1]) ? text.indexOf('(', i + 1) : -1; + const delimiter = open > i ? text.slice(i + 1, open) : null; + if (delimiter !== null && delimiter.length <= 16 && /^[^\s()\\]*$/.test(delimiter)) { + // From the opening quote through the closing one: `"cc( … )cc"`. + const close = text.indexOf(`)${delimiter}"`, open + 1); + const end = close < 0 ? n : close + delimiter.length + 2; + mark(i, line, column, end); + advance(end); + } else { + // An ordinary string: to its closing quote, or the end of the line. + let k = i + 1; + while (k < n && text[k] !== '"' && text[k] !== '\n') k += text[k] === '\\' ? 2 : 1; + advance(Math.min(n, text[k] === '"' ? k + 1 : k)); + } + } else if (c === '\'' && !isWordChar(text[i - 1])) { + // A character literal (`'"'`); after a digit it is a digit separator (`1'000`). + let k = i + 1; + while (k < n && text[k] !== '\'' && text[k] !== '\n') k += text[k] === '\\' ? 2 : 1; + advance(Math.min(n, text[k] === '\'' ? k + 1 : k)); + } else advance(i + 1); + } + } + memo.set(file, lines); + return lines; +} + +/** + * Line `line` (1-based) of a C or C++ file with the contents of its string + * and character literals blanked, raw strings included, columns kept. + */ +export function cppLiteralsBlanked(text: string, line: number, rawLines: Map): string { + const raw = rawLines.get(line); + const out = text.split(''); + if (raw) for (const [start, end] of raw) for (let k = start; k < end && k < out.length; k++) out[k] = ' '; + if (!text.includes('"') && !text.includes('\'')) return out.join(''); + const inRaw = (k: number): boolean => !!raw?.some(([start, end]) => k >= start && k < end); + for (let k = 0; k < text.length; k++) { + const c = text[k]!; + if (inRaw(k)) continue; + if (c === '/' && text[k + 1] === '/') break; + if (c === '/' && text[k + 1] === '*') { + // A comment's quotes are no string's (cppCodeOf blanks the comment). + const close = text.indexOf('*/', k + 2); + if (close < 0) break; + k = close + 1; + continue; + } + if (c !== '"' && !(c === '\'' && !isWordChar(text[k - 1]))) continue; + let end = k + 1; + while (end < text.length && text[end] !== c) end += text[end] === '\\' ? 2 : 1; + for (let b = k + 1; b < Math.min(end, text.length); b++) out[b] = ' '; + k = end; + } + return out.join(''); +} diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 0c87abaa6a..83cae1579c 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -16,6 +16,7 @@ import { isDartLocallyBound } from './dart-local-scope'; import { breakVbTie, isVbMemberInScope, isVbNestedTypeInScope, isVbTypeQualifiedBy, matchVbTypedCall, preferVbProject, sameVbProject } from './vbnet-receivers'; import { cppAliasedClass, cppAliasedTypeName, cppAliasedTypeWritten, cppParentScope, cppScopesWithin, cppTemplateArguments, cppTemplateParameters, cppTypeSegments, isCppPointerType, resolveCppAliasedType, stripCppTemplateArguments, type CppAliasedType } from './cpp-type-aliases'; import { clearCppNamespaceMemos, cppMacroNamespaceFrames, cppNamespaceAliases } from './cpp-namespaces'; +import { clearCppRawStringMemos, cppLiteralsBlanked, cppRawStringLines } from './cpp-raw-strings'; import { cppClassWritten, type CppNameSite } from './cpp-supertypes'; import { cppIncludedFile, cppIncluders } from './cpp-includers'; import { isTestPath } from '../search/query-utils'; @@ -10132,11 +10133,15 @@ function inferCppReceiverType( const escapedReceiver = receiverName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const receiverPattern = new RegExp(`\\b${escapedReceiver}\\b`); const declaratorRegex = buildDeclaratorRegex(escapedReceiver); + // A declaration in a string literal is text: protobuf's upb generator + // writes `upb_MiniTableField field = $field_init$;` in the raw string it + // emits, above calls on its own `upb::FieldDefPtr field` parameter. + const rawLines = cppRawStringLines(ref.filePath, context); for (let i = callLineIndex; i >= 0; i--) { const text = lines[i]; if (!text || !receiverPattern.test(text)) continue; - const line = cppCodeOf(text); + const line = cppCodeOf(cppLiteralsBlanked(text, i + 1, rawLines)); if (!line || !receiverPattern.test(line)) continue; const declaratorMatch = line.match(declaratorRegex); @@ -10198,11 +10203,12 @@ function inferCppReceiverType( ? context.getFileLines(headerPath) : (context.readFile(headerPath)?.split(/\r?\n/) ?? null); if (!headerLines) continue; + const headerRawLines = cppRawStringLines(headerPath, context); for (let i = 0; i < headerLines.length; i++) { const text = headerLines[i]!; if (!receiverPattern.test(text)) continue; - const line = cppCodeOf(text); + const line = cppCodeOf(cppLiteralsBlanked(text, i + 1, headerRawLines)); if (!line) continue; const declaratorMatch = line.match(declaratorRegex); if (!declaratorMatch) continue; @@ -10723,6 +10729,7 @@ export function clearNameMatcherMemos(context: ResolutionContext): void { KOTLIN_HIERARCHIES.delete(context); KOTLIN_FRAMES.delete(context); clearCppNamespaceMemos(context); + clearCppRawStringMemos(context); CPP_CLASSES_IN_FILE.delete(context); CPP_BASE_SPECS.delete(context); CPP_MEMBER_MACRO_ARGS.delete(context); From 9c7db7dc4e9eab9f9e31a96e11aec83f1327cb96 Mon Sep 17 00:00:00 2001 From: Aaron Queen Date: Mon, 12 Oct 2026 18:29:53 -0600 Subject: [PATCH 5/5] docs(changelog): state the held-class result as unlinked, drop the string-literal entry the fork already satisfied --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4679b1555a..dd5a43e916 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -469,7 +469,6 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In C++, a call a method makes with no receiver, or through `this->`, now links to the method C++ itself would call: one of the method's own class, else one the class inherits from a base class, else a static member of a class it is nested in, which a nested class can name without an object. The same goes for a call in a lambda inside the method, and of a class's overloads the one the call's arguments fit is taken. Before, the call was matched by its name alone, so another class's method of that name could win because it was declared nearer or found first: in protocolbuffers/protobuf, every generated message's `Swap` and move assignment calling `InternalSwap(…)` reached `Any::InternalSwap`, and the `GetArena()` messages inherit from `MessageLite` reached an arena helper's `GetArena`, while googletest's `~linked_ptr() { depart(); }`, vendored in facebook/rocksdb, reached `linked_ptr_internal::depart`. Calls C++ doesn't look up in the class are matched as before: a free function's, a call to a parameter or local of that name, and a template's bare call into a base class that depends on its template parameters. Re-index C++ projects after upgrading. Thanks @danusha2345 for reporting the C++ case in #1952. - In C++, a call on the variable of a range-based `for` loop over `auto` elements, like `cfd->Ref()` inside facebook/rocksdb's `for (auto cfd : cfds)`, now links to the method of the type the loop goes over. That type is read from what the loop iterates: a `std::vector`, `std::set`, `absl::Span` or similar container, protocolbuffers/protobuf's `RepeatedPtrField` or RocksDB's `autovector`, whether a variable, parameter or member holds it, an `auto` variable is set to it or a function or method returns it, and a project class you loop over through its `begin()`, like RocksDB's `ColumnFamilySet`. Before, the call was matched by the variable's name, or by an earlier variable of that name: in RocksDB, `blob_file.GetBlobFileNumber()` over a list of `BlobFileGarbage` reached `BlobFileAddition::GetBlobFileNumber`, and in protobuf's Objective-C generator, calls over a list of `std::unique_ptr` reached other generators' methods. A call on an element of a standard-library type, like `file.find(…)` over a `std::vector`, no longer links to a project method, and `->` on an element held in a `std::unique_ptr` or `std::shared_ptr` reaches the type it holds. Re-index C++ projects after upgrading. - In C++, a call on a variable, parameter or member declared as one of your classes, like `iter->Valid()` on an `Iterator* iter` or `it.Valid()` on a `const Iterator& it`, now links to that method of the class C++ itself would find for the type where the call is written, or of a class it inherits from. Before, the method was picked by the type's last name, so a class of the same name nested elsewhere could win: in facebook/rocksdb, calls on an `Iterator` reached `MemTableRep::Iterator`, which declares `Valid` and `Next` itself while the `Iterator` the code means inherits them, google/leveldb's reached `SkipList::Iterator`, and in protocolbuffers/protobuf the Java generator's `context->options()` reached the hpb generator's `Context` and `text_parser.Parse(…)` on a `TextFormat::Parser` reached the `.proto` parser. When that class and the classes it derives from have no method of that name, the call no longer links to an unrelated class's method that shares it, as protobuf's `field->full_name()` on a `FieldDescriptor*` did. A type is also found through a `using` declaration the file writes, like protobuf's `using google::protobuf::Descriptor;`, before a class of the same name declared at global scope elsewhere in the repository; this also applies to base classes and to the type a smart pointer holds. Re-index C++ projects after upgrading. -- In C++, text inside a string literal is no longer taken for a variable's declaration. That covers the C code protocolbuffers/protobuf's upb generator writes in a raw string (`R"cc( … upb_MiniTableField field = $field_init$; … )cc"`) and log messages like facebook/rocksdb's `"… in WriteBatch handler; …"`. Before, a later call like `field.ctype()` on the function's own `upb::FieldDefPtr field` parameter, or `handler->PutCF(…)` on a `WriteBatch::Handler* handler`, was matched against the type that text seemed to name, so it linked to nothing or to another class's method. Re-index C++ projects after upgrading. - In this fork, the two C++ changes above run in the native resolver. A call on a variable whose type is a template parameter, like `T* iter`, stays unlinked rather than reaching a class that happens to share the parameter's name. A call stays linked as before when the class has no method of that name but one of its base classes comes from an outside library other than the standard library, such as googletest's `testing::Test`. - Framework support now switches on when an app's `package.json` sits three or more folders deep, as with an ASP.NET solution's Angular or React app in `src/Web/ClientApp/`, or a React app in a workspace folder like `web/ui/mantine-ui/`. Before, only the project root and the first two folder levels were read, so frameworks such as Angular, React Router, Vue Router, SvelteKit and TanStack Router could go undetected there: an Angular app's routes, its `routerLink` and `router.navigate(…)` navigation and its template's event bindings were missing, so a handler used only from a `(click)` binding had no callers, and a React app's `navigate('/login')` calls didn't reach their route. Re-index projects laid out this way after upgrading. @@ -565,7 +564,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In C++, a call on a range-based `for` loop's variable, like `suite->SetVerbose(verbose)` inside `for (ConformanceTestSuite *suite : suites)`, now links to the method of the type the loop declares. Before, the loop's declaration was never read, so the call was matched by the variable's name alone: in RocksDB, `log->file()` on a `log::Writer` went to `BlobLogWriter::file`, and a loop variable named like a member of its class was taken for that member. The variable counts only inside its loop. Re-index C++ projects after upgrading. -- In C++, a call made with `->` on a variable, parameter or member declared as a `std::unique_ptr`, `std::shared_ptr` or `std::optional`, like `iter->Valid()` on a `std::unique_ptr iter`, now links to that method of the type it holds, or of a class that type inherits from, with the type looked up the way C++ does it, so a class of the same name nested elsewhere is not mistaken for it. Before, the call went to whichever project method shared its name: in facebook/rocksdb, `iter->Valid()` reached `ArenaWrappedDBIter::Valid` and `fs->GetFileSize(…)` on a `std::shared_ptr` reached a test's stand-in file system, and calls like `it->Next()` were often not linked at all. When what it holds is a template parameter or a standard-library type, like `std::unique_ptr>`, or a class that has no method of that name and neither do the classes it derives from, the call no longer links to a project method; a class derived from something that can't be followed, like a `decltype(…)`, may still have it, so that call is matched as before. Re-index C++ projects after upgrading. +- In C++, a call made with `->` on a variable, parameter or member declared as a `std::unique_ptr`, `std::shared_ptr` or `std::optional`, like `iter->Valid()` on a `std::unique_ptr iter`, now links to that method of the type it holds, or of a class that type inherits from, with the type looked up the way C++ does it, so a class of the same name nested elsewhere is not mistaken for it. Before, the call went to whichever project method shared its name: in facebook/rocksdb, `iter->Valid()` reached `ArenaWrappedDBIter::Valid` and `fs->GetFileSize(…)` on a `std::shared_ptr` reached a test's stand-in file system, and calls like `it->Next()` were often not linked at all. When what it holds is a template parameter or a standard-library type, like `std::unique_ptr>`, or a class that has no method of that name and neither do the classes it derives from, the call no longer links to a project method; a class derived from something that can't be followed, like a `decltype(…)`, may still have it, so that call is left unlinked. Re-index C++ projects after upgrading. - `codegraph sync` and the file watcher now move a React Router route to its page when the router file loads that page lazily into a value of its own, like `const Docs = lazy(() => import('./pages/Docs'))` rendered by `} />`, and the page's file is added after the router was indexed, comes back after being deleted, or gets or changes its default export in a later edit. Before, the route kept pointing at the `Docs` declaration, or at the page's old component, until the router file itself changed or the project was re-indexed, unless the page's component happened to share the declaration's name. The same now holds for a `lazy: () => import('./pages/Docs')` route whose page switches its default export to another component. Re-index React projects after upgrading to correct routes an earlier version left behind.