diff --git a/CHANGELOG.md b/CHANGELOG.md index 04363a2aa..f3c16db7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,6 +95,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes +- C++ anonymous pointer aggregates and partially qualified owners inside macro-opened namespaces retain their method calls. Pointer-annotation normalization leaves multiplication operands intact. Named-only React components do not invent default exports, and spaced type arguments do not create rendering edges. + - Cross-file route-table reconciliation commits nodes and references together, allowing retry after a failed reference write. - C++ receiver scans ignore multiline raw-string contents before reading declarations. @@ -479,11 +481,49 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - React Router routes kept in a table of their own are now indexed: the ASP.NET Core React template's `AppRoutes` array that `App.js` maps into ``, a `routes` array imported into `useRoutes(routes)` or `createBrowserRouter(routes)` or returned by a function as in `useRoutes(routes(isLoggedIn))`, route objects kept one per file and listed in `createBrowserRouter([MainRoutes, LoginRoutes])`, and routes written in place in `useRoutes([…])`. Before, route objects were read only from a file that itself creates a data router, so these apps had no routes, and their `navigate('/login')` calls and `` links led nowhere. Each route links to the page its `element` renders, past a guard like `` or ``, and through `lazy(() => import(…))` to the page it loads rather than a same-named page elsewhere in the repository. An `index: true` route is the page at its parent's address, and a parent route around others counts as their layout. A `{ path, element }` list that nothing hands to the router, such as a menu, still makes no routes. Re-index React projects after upgrading. -- In Go, a call or type written through an import whose path ends in a version, or in something other than the package's name, is now known to go through that import: `yaml.Unmarshal(…)` after `import "go.yaml.in/yaml/v3"` or `"gopkg.in/yaml.v3"`, `sqlite3.Error` after `import "github.com/mattn/go-sqlite3"`, `klog.V(2)` after `import "k8s.io/klog/v2"`. Before, only the last part of the path named such an import (`v3`, `yaml.v3`, `go-sqlite3`), so the name was matched on its own and could link to any project function, method or type that shared it: kubernetes' `klog.V(…)` calls were linked to a logging wrapper's `V` method, and etcd's `semver.Version` parameters to an unrelated `Version` function. A name from another module now links to nothing, and one through a package of your own project, like `kit.New()` after `import "example.com/kit/v2"`, links to that package's symbol. A comment in an import block is also no longer taken for the name of the import after it. Re-index Go projects after upgrading. - - React Router routes written as JSX now read `` the way route objects do: inside `}>`, `} />` is the page at `/`, and `Layout` counts as the layout of every route nested in it, as does a guard with no path of its own like `}>`. Before, the index route was never read, so `/` led to the layout itself and a `` holding only an index route led to nothing. A route's `element` written on lines of its own, the way Prettier formats a long one, or wrapped in `}>`, now links to the page it shows rather than to nothing or to the fallback, and a path written in braces, like `path={"agents"}`, is read like any other. Re-index React projects after upgrading. - In JavaScript and TypeScript, an import written `import { default as AppRoot } from './routes/app/root'`, as in bulletproof-react's router, now counts as the module's default import, the same as `import AppRoot from './routes/app/root'`. Before, it was read as an import of an export named `default`, which no module has, so a call, a route or a JSX attribute that used `AppRoot` was matched by its name alone: it could link to another file's `AppRoot`, or to nothing when the default export has a name of its own. Svelte, Vue and Astro script blocks are read the same way. + + +- In Go, a call made through a type assertion, like `srv.(KVServer).Range(ctx, in)` in generated gRPC code, `c.Reader.(*pipe).Close()` or `v.(storage.Store).Fetch(key)`, now links to that method of the asserted type, including one the type gets from a struct or interface it embeds, and to nothing when the type comes from outside your project. Before, it linked to whichever method shared the name, so every gRPC handler pointed at the `Unimplemented…Server` stub instead of the server interface. Re-index Go projects after upgrading. + +- In Go, a type declared on its own, like `type Point struct{…}` under a `// Point is …` comment, now keeps that comment as its documentation, as a type inside a `type ( … )` group already did, so searching for what a type does can find it. A comment at the end of a line of code, like `const sides = 4 // sides of a square.`, is no longer taken as the documentation of the declaration under it. Re-index Go projects after upgrading. + + +- A file named the way Google names its tests, like `wire_format_unittest.cc` in protocolbuffers/protobuf, `logging_unittest.cc` in google/glog or a Python `foo_unittest.py`, now counts as a test, as `wire_format_test.cc` already did. Before, it was taken for production code: search and `codegraph_explore` ranked it alongside the code it tests, `codegraph affected` never listed it, and calls in production code could link to a class or type a unittest declares for itself, as google/breakpad's `stack_frame_entries_.size()` did to a unittest's `StackHelper`. Re-index projects that have such files after upgrading. + +- `codegraph sync` and the file watcher now link a file to a module it imports under a name of its own when the module is added after the file was indexed, comes back after being deleted, or gets the export in a later edit: a default import like `import tagsController from './tag/tag.controller'`, a namespace import like `import * as api from './api'`, or an aliased one like `import { Component as GroupsPageWrapper } from './groups_page'`, in JavaScript, TypeScript, Vue, Svelte and Astro. Before, the import and every use of the name, such as a call, `new`, a base class, a JSX or template tag or an Express `app.use` mount, stayed unlinked until the importing file changed or the project was re-indexed, so the module looked unused and `codegraph affected` missed the files that use it. Re-index after upgrading to pick up links an earlier version missed. + +- 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. + +- 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 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. + +- In Go, a struct that embeds another is no longer treated as overriding the methods it redeclares. gin's `Engine` embeds `RouterGroup` and has a `Use` of its own, but calling `Use` on a `RouterGroup` only ever runs `RouterGroup.Use`; before, `codegraph_explore`'s call path and impact went on from it into `Engine.Use` and the code only the engine runs. The same held for every struct or defined type embedded in another that redeclares one of its methods, like etcd's `Mutex` inside `lockerMutex`. A call through an interface still reaches each type that implements it, the embedding struct included. Re-index Go projects after upgrading. + +- In Go, a type defined from other types, like etcd's `type WatchChan <-chan WatchResponse` or gin's `type HandlerFunc func(*Context)` and `type HandlersChain []HandlerFunc`, now links to the types it is built from, so impact on `WatchResponse` or gin's `Context` reaches these types and the code that uses them. Before, such a type linked to nothing. One written through another package, like etcd's `type PutResponse pb.PutResponse`, links to that package's type, and a recursive one like `type stateFn func(*Lexer) stateFn` doesn't link to itself. Re-index Go projects after upgrading. + +- In Go, a struct or defined type now counts as implementing an interface only when its methods take as many parameters and return as many results as the interface's. Before, matching method names was enough, so etcd's watch cache, whose `Get` takes a context, a key and options, was listed as a `peerGetter`, whose `Get` takes a peer ID, and a call through an interface could be shown reaching methods it can never reach. Look-alikes could also crowd out the real implementations, so `codegraph_explore` stopped at grpc-go's xDS `TransportBuilder` instead of reaching the transport it builds. A server still counts for the gRPC client interface of the service it serves, like etcd's `kvServer` for `KVClient`, so a client's call keeps leading to the handler that serves it. Re-index Go projects after upgrading. + +- In Go, a call or type written through an import whose path ends in a version, or in something other than the package's name, is now known to go through that import: `yaml.Unmarshal(…)` after `import "go.yaml.in/yaml/v3"` or `"gopkg.in/yaml.v3"`, `sqlite3.Error` after `import "github.com/mattn/go-sqlite3"`, `klog.V(2)` after `import "k8s.io/klog/v2"`. Before, only the last part of the path named such an import (`v3`, `yaml.v3`, `go-sqlite3`), so the name was matched on its own and could link to any project function, method or type that shared it: kubernetes' `klog.V(…)` calls were linked to a logging wrapper's `V` method, and etcd's `semver.Version` parameters to an unrelated `Version` function. A name from another module now links to nothing, and one through a package of your own project, like `kit.New()` after `import "example.com/kit/v2"`, links to that package's symbol. A comment in an import block is also no longer taken for the name of the import after it. Re-index Go projects after upgrading. Thanks @danusha2345 for the comment fix. (#2374) + +- In Go, a call is now matched to the package or variable it is actually made on. Before, a call through a package, like `klog.Infof("Log using Infof")` or `digest.Digest(dig)` inside `&distribution.Descriptor{Digest: …}`, could link to a project method of the same name when the name appeared again on the line, and in `klog.Error(err.Error())` the `err.Error()` call was taken for a call into `klog`. A parameter, receiver or local variable named like an import, like etcd's `jwt, err := newTokenProviderJWT(…)` or the `suite` receiver in a testify test suite, now counts as that variable, so calls on it link to the methods of the type it is declared as, and to nothing when that type comes from outside your project. Re-index Go projects after upgrading. + +- In JavaScript and TypeScript, a module loaded with `require('./x').default` or `const { default: X } = require('./x')` now links to its default export when it is written as an ES module: bitwarden's desktop app loads `export default class OsBiometricsServiceMac` this way before calling `new OsBiometricsServiceMac(…)`, and React Native's own libraries load many of their modules like this. Before, `.default` was looked up only as an export named `default`, which `export default` is not, so the `new` and the calls made through it linked to nothing, to the local variable holding the module, or to a method of the same name somewhere else. A Svelte component loaded this way and created with `new App({ target })` now links to the component. A CommonJS module that sets `exports.default` itself, or both `module.exports` and `module.exports.default` as fastify does, still links to what it sets, and one that sets no `default` still links to nothing. Re-index JavaScript and TypeScript projects after upgrading. + +- A React Router route that lazily loads a folder, like `lazy: () => import('@/pages/invocation_page')`, now links to the page the folder's `index.ts` passes on, whether it writes `export * from './invocation_page'`, `export { default } from './page'` or `export { Page as Component } from './page'`, even through another `index.ts`. A route module that names its page with `export { Page as Component }` links to it as well, and so does a page loaded with `lazy(() => import(…))` through such an `index.ts`. Before, these routes linked to nothing, so their pages had no route leading to them. Re-index React projects after upgrading. + +- In React, a component's JSX no longer links it to a class or component that only shares a name with one of its type arguments or with a variable of its own. A type in angle brackets, like `Document` in ` items={…} />`, `User` in `useState()` or `Entry` in a generic `(…) =>` component, is no longer read as a tag. A tag naming a variable or parameter the component sets itself, like `` after `const Content = isDropdown ? DropdownMenu.SubContent : ContextMenu.SubContent` or `` in `widgets.map((Widget) => )`, now links to nothing, unless the component declares a component of that name inside itself. Before, these linked to an unrelated class or component elsewhere in the repository: outline's document lists showed up among the callers of its `Document` model class, and its menus among the callers of the command bar's `Content`, so `codegraph_explore`, callers and impact followed renders that never happen. Re-index React projects after upgrading. + +- In JavaScript and TypeScript, a name imported from a package your `package.json` lists no longer links to a project symbol that only shares its name when `tsconfig.json` or `jsconfig.json` has a catch-all path alias, like `"*": ["./typings/*"]` or `"*": ["src/*", "node_modules/*"]`, or an alias that points the package at a file in `node_modules`, like `"lit/decorators": ["./node_modules/lit/decorators.js"]`. Such an alias made every package look like part of the project, so `import { Typography } from '@mui/material'` was linked to the project's own `Typography` and every lit `@property()` decorator to an unrelated class's `property` field, and `codegraph callers`, impact and `codegraph affected` listed code that never used them. An import the alias does map to a file of your project, like a `.d.ts` you keep for an untyped package or `components/Button` through `"*": ["src/*"]`, links as before. Re-index affected projects after upgrading. + +- `codegraph_explore`'s Flow now goes through the implementation your query names when an interface method has several that lead to the same place. Before, it took whichever one came first in the index: asking about prometheus's `Engine.execEvalStmt Queryable.Querier fanout.Querier NewMergeQuerier` traced the call through the TSDB's `DB.Querier` and left the `fanout.Querier` you named off the Flow. Any two routes of the same length are now settled this way, in favor of the one that passes through more of the symbols you named. + +- In C and C++, a struct, class, union or enum defined in a variable declaration, like `struct Foo { … } foo;`, is now indexed with its methods and enum values, next to the variable. Before, at most the variable was, so the type was missing from search and `codegraph_explore`, a class deriving from it had no base, and calls to its methods linked to nothing: in redis, the Sentinel's `sentinelState` and `redis-cli`'s `config` struct were missing. A struct, union or enum with no name of its own now takes the name of the variable it declares, so redis's `static struct { … } SPT;` is found as `SPT`, and one declared inside a function is named after its variable instead of ``. The comment above the declaration becomes the type's description. Re-index C and C++ projects after upgrading. + +- In TypeScript React code, a styled-components or emotion component declared with a type argument, like `const Wrapper = styled.div` followed by its CSS, or `styled(Base)`, is now indexed as a component, as one written without a type argument already was. Before, it was indexed as a plain constant, so `codegraph callers` and impact found none of the code that renders it, and its tags could link to another file's component of the same name instead: in outline/outline, `NudeButton` and `HStack` had no callers, and `` tags linked to a different `Text` than the one each file imports. Re-index TypeScript React projects after upgrading. #### MCP / indexing - Full indexes of large projects no longer sometimes run two to three times slower than usual. A timing clash between two background database maintenance steps could switch reference resolution from several threads to one partway through. diff --git a/README.md b/README.md index f550373c3..c21bdd0c2 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Follow [@getcodegraph](https://x.com/getcodegraph) on X for updates. ## About this fork -This is **bompus/codegraph**, a fork of [colbymchenry/codegraph](https://github.com/colbymchenry/codegraph). Its default branch, `fork/consolidated`, contains upstream `main` through [`ed199e60`](https://github.com/colbymchenry/codegraph/commit/ed199e60d8f1aed5baf73836366ef0a6f7838d11) (after v1.6.2) plus the fork's own work, and it takes upstream changes as they land. Changes that suit upstream are also offered there as pull requests. +This is **bompus/codegraph**, a fork of [colbymchenry/codegraph](https://github.com/colbymchenry/codegraph). Its default branch, `fork/consolidated`, contains upstream `main` through [`b635dd46`](https://github.com/colbymchenry/codegraph/commit/b635dd467f0578926a9c01a37b9d28d2b26689f1) (after v1.6.2) plus the fork's own work, and it takes upstream changes as they land. Changes that suit upstream are also offered there as pull requests. The fork publishes no releases. The install scripts, npm package, badges and `codegraph upgrade` further down this page install **upstream's** releases. To run the fork, build it from source (below). @@ -76,7 +76,7 @@ Then run `codegraph init` in each project, as in [Get Started](#get-started). In ### What the fork adds -Compared with upstream `main` at `ed199e60` (after v1.6.2). Each item here and in the dispatch and framework lists below was checked against upstream's tree at that commit. +Compared with upstream `main` at `b635dd46` (after v1.6.2). Each item here and in the dispatch and framework lists below was checked against upstream's tree at that commit. | Feature | Upstream | Fork | What it does | |---|:-:|:-:|---| @@ -113,6 +113,8 @@ C# property accessors and expression-bodied properties contribute calls and refe C# field and property initializers retain their calls and references under the member that owns them. Target-typed `new()` resolves relative declared types through enclosing namespaces and honors `global::` qualification without requiring a redundant `using`. VB.NET resolves typed receivers, enclosing and inherited members, and field or property reads through values and Shared types without choosing unrelated project declarations. Names beginning with keywords, such as `SharedCache`, `Dimension` and `NewItem`, retain their declarations. +Go declaration comments attach to the declared type, and defined types retain references to their named component types. Assertion calls follow the asserted project type and each declared return in a method chain. Local bindings shadow package imports. C/C++ types defined beside variables retain their members and comments; nested template arguments preserve receiver ownership, and leading `::` names require a visible global declaration. C++ declaration attributes and macros preserve source positions. Multiplication operands remain intact, and anonymous pointer declarations share their first declared type name. Macro-opened namespaces combine with ordinary nested scopes when resolving included owners. Renamed and default imports are retried when their module appears during sync. Named-only components are not default exports; explicit forwarded defaults retain their target. Whitespace before a type argument does not create a JSX rendering edge. React JSX respects component imports and local shadowing; lazy routes follow component exports through local barrels. Named flow paths prefer an equally long path containing more requested symbols. + The native resolver includes upstream's Go import-name assumptions, type-position checks, alias method forwarding and embedded interface method sets. Chained Go calls follow declared return types at each fluent step; an unproven receiver stays unresolved instead of borrowing an unrelated method by name. C++ bases use enclosing-scope directives, parent-scope lookup and translation-unit visibility; included test-named headers remain eligible, while declared external library receivers avoid unrelated project methods. Incremental sync retries newly available Liquid targets, lazy route modules and navigation destinations. Navigation in an imported route-table file also survives removing and restoring its destination. Route-table nodes and their references commit together, so a failed reference write can be retried without leaving an incomplete route. React layout links exclude Vue and Angular routes. C++ receiver declaration scans exclude multiline raw-string contents. Go interfaces link named embedded interfaces and aliases that resolve to interfaces. Named scalar or struct terms, unions, underlying-type constraints and basic types are not supertypes. Package-qualified embeddings stay in their imported package. Go imports follow the nearest indexed module and the longest matching module path, including module changes during incremental sync. Unexported receivers and embedded methods stay in their declaring package. Dart imports, exports and part directives follow their library URIs and visibility rules. Library directive reads stay inside the indexed project, including resolved symlink targets. Calls through import prefixes, annotations and member chains follow the visible declaration and written receiver types, including explicit generic lookup types. Parameters and locals shadow bare calls. Top-level and field initializers contribute calls; const constructors, redirecting factories and annotated members retain their declarations and dartdoc. Getter reads become calls only when the receiver type reaches that getter and no nearer field in the visible class hierarchy overrides it; enum extensions and type-position references participate in resolution. Rust enum-variant values retain their enum references. diff --git a/__tests__/c-cpp-declaration-types.test.ts b/__tests__/c-cpp-declaration-types.test.ts new file mode 100644 index 000000000..c37666551 --- /dev/null +++ b/__tests__/c-cpp-declaration-types.test.ts @@ -0,0 +1,240 @@ +/** + * A class, struct, union or enum DEFINED in the type of a C or C++ variable + * declaration was never indexed: + * + * namespace n { struct Foo { int a; void f() {} } foo; } // only n::foo + * static struct { int argc; char **argv; } SPT; // nothing at all + * enum { MODE_IDLE, MODE_BUSY } current_mode; // no enumerators + * + * Both extractors handed such a declaration to the variable extractor and + * skipped its children, so the type, its methods and its enumerators never + * became nodes, a base class defined this way had nothing to resolve to, and + * calls on its methods linked to nothing. + * + * The type is now walked like one written on its own, beside the variables + * (which keep their nodes), and the comment above the declaration documents + * it. An unnamed one takes the name of the first variable its declaration + * declares, as `typedef struct { … } Name;` takes the typedef name: no code + * can name the type of `SPT`, only `SPT`. In a C++ file whose tree has errors + * the type is walked in the scopes the file's braces open (#2426), and not at + * all when the braces don't balance. Runs against the required native kernel. + */ +import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import CodeGraph from '../src/index'; +import { tryKernelExtract, resetKernelForTests } from '../src/extraction/kernel'; +import type { ExtractionResult, Language } from '../src/types'; + + +const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_CCPP_ERROR_EXTRACT'] as const; + +/** ` ` for every symbol, file and imports aside. */ +function symbols(result: ExtractionResult): string[] { + return result.nodes + .filter((n) => n.kind !== 'file' && n.kind !== 'import') + .map((n) => `${n.kind} ${n.qualifiedName}`); +} + +/** ` > ` for every containment edge below the file. */ +function containment(result: ExtractionResult): string[] { + const byId = new Map(result.nodes.map((n) => [n.id, n])); + return result.edges + .filter((e) => e.kind === 'contains' && byId.get(e.source)?.kind !== 'file') + .map((e) => `${byId.get(e.source)?.qualifiedName} > ${byId.get(e.target)?.qualifiedName}`); +} + +describe('a type defined in a C/C++ declaration', () => { + let savedEnv: Record = {}; + + + beforeEach(() => { + savedEnv = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]])); + resetKernelForTests(); + }); + + afterEach(() => { + for (const k of ENV_KEYS) { + if (savedEnv[k] === undefined) delete process.env[k]; + else process.env[k] = savedEnv[k]; + } + resetKernelForTests(); + }); + + function extract(_backend: 'kernel', file: string, source: string, language: Language): ExtractionResult { + delete process.env.CODEGRAPH_KERNEL; + const result = tryKernelExtract(file, source, language); + expect(result, `kernel extraction of ${file}`).not.toBeNull(); + return result!; + } + + const backends = ['kernel'] as const; + + for (const crlf of [false, true]) { + const eol = (s: string) => (crlf ? s.replace(/\n/g, '\r\n') : s); + const label = crlf ? ' (CRLF)' : ''; + + it.each(backends)(`C++ classes and structs beside their variables: %s${label}`, (backend) => { + const source = [ + 'namespace n {', + 'struct Foo { int a; void f() {} } foo;', + 'class Bar { void g() {} } bar, *pbar;', + '}', + '', + ].join('\n'); + const result = extract(backend, 'src/decl.cpp', eol(source), 'cpp'); + expect(symbols(result)).toEqual([ + 'struct n::Foo', + 'method n::Foo::f', + 'variable n::foo', + 'class n::Bar', + 'method n::Bar::g', + 'variable n::bar', + ]); + expect(containment(result)).toEqual(['n::Foo > n::Foo::f', 'n::Bar > n::Bar::g']); + }); + + it.each(backends)(`C structs, unions and enums; an unnamed one takes its variable's name: %s${label}`, (backend) => { + const source = [ + 'struct Foo { int a; } foo;', + 'static struct { int argc; char **argv; } SPT;', + 'static const struct { const char *name; int code; } errors[] = { { "none", 0 } };', + 'struct { int x; } *first_point, second_point;', + 'union { int i; float f; } scratch = { 0 };', + 'enum { MODE_IDLE, MODE_BUSY } current_mode;', + 'typedef struct Bar { int c; } Bar;', + '', + ].join('\n'); + const result = extract(backend, 'src/decl.c', eol(source), 'c'); + expect(symbols(result)).toEqual([ + 'struct Foo', + 'struct SPT', + 'struct errors', + 'constant errors', + 'struct first_point', + 'variable first_point', + 'union scratch', + 'variable scratch', + 'enum current_mode', + 'enum_member current_mode::MODE_IDLE', + 'enum_member current_mode::MODE_BUSY', + 'struct Bar', + ]); + }); + + it.each(backends)(`a type whose declaration declares no variable stays unnamed: %s${label}`, (backend) => { + // A function's return type: naming it after the function would be wrong. + const result = extract(backend, 'src/proto.c', eol('struct { int unused; } make_unnamed(void);\n'), 'c'); + expect(symbols(result)).toEqual(['struct ']); + }); + + it.each(backends)(`the comment above the declaration documents the type: %s${label}`, (backend) => { + const source = [ + 'namespace app {', + '/** The handler registry. */', + 'struct Registry { int size() const { return 2; } } registry;', + '}', + '', + ].join('\n'); + const result = extract(backend, 'src/registry.cc', eol(source), 'cpp'); + const docs = result.nodes + .filter((n) => n.name === 'Registry' || n.name === 'registry') + .map((n) => `${n.kind} ${n.docstring}`); + expect(docs).toEqual(['struct The handler registry.', 'variable The handler registry.']); + }); + + it.each(backends)(`an unnamed type in a function body takes its variable's name too: %s${label}`, (backend) => { + const source = 'int hits(void) {\n static struct { int count; } local_stats;\n return ++local_stats.count;\n}\n'; + const result = extract(backend, 'src/stats.c', eol(source), 'c'); + expect(symbols(result)).toEqual(['function hits', 'struct hits::local_stats']); + }); + + it.each(backends)(`a file the parser misreads: C keeps the type, C++ needs balanced braces: %s${label}`, (backend) => { + // Native extraction recovers balanced C++ scopes in error trees. + // error-extract hatch walks one anyway, and must agree. + process.env.CODEGRAPH_KERNEL_CCPP_ERROR_EXTRACT = '1'; + const c = extract(backend, 'src/spt.c', eol('static struct { int argc; } SPT;\nint broken( { return 1; }\n'), 'c'); + expect(symbols(c)).toContain('struct SPT'); + // A C++ file with errors is walked in the scopes its braces open, so the + // class lands in its namespace. When the braces don't balance there are + // no scopes to trust, and error recovery can close a namespace or a + // class at the wrong `}`: the variable is kept and the class not walked. + const config = 'namespace n {\nstruct Config { int retries; void apply() {} } config;\n}\n'; + const balanced = extract(backend, 'src/config.cc', eol(`${config}int broken( { return 1; }\n`), 'cpp'); + expect(symbols(balanced)).toEqual(['struct n::Config', 'method n::Config::apply', 'variable n::config']); + const unbalanced = extract(backend, 'src/config2.cc', eol(`${config}int broken() { return 1;\n`), 'cpp'); + expect(symbols(unbalanced)).toEqual(['variable n::config', 'function broken']); + }); + + it.each(backends)(`a function value in the type's body is captured once, by the type: %s${label}`, (backend) => { + const source = [ + 'void on_open() {}', + 'struct Handlers { void (*all[1])() = {&on_open}; } handlers;', + '', + ].join('\n'); + const result = extract(backend, 'src/handlers.cc', eol(source), 'cpp'); + const byId = new Map(result.nodes.map((n) => [n.id, n])); + const refs = result.unresolvedReferences + .filter((r) => r.referenceKind === 'function_ref') + .map((r) => `${byId.get(r.fromNodeId)?.name} ${r.referenceName}`); + expect(refs).toEqual(['Handlers on_open']); + }); + } +}); + +describe('an indexed C/C++ project with types defined in declarations', () => { + let root = ''; + let cg: CodeGraph | undefined; + + afterEach(() => { + cg?.destroy(); + cg = undefined; + if (root) fs.rmSync(root, { recursive: true, force: true }); + root = ''; + }); + + it('links base classes and calls to them, and finds an unnamed struct by its variable', async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-declaration-types-')); + const files: Record = { + 'src/registry.cc': [ + 'namespace app {', + 'struct Registry { int size() const { return 2; } } registry;', + 'class Base { public: virtual int run() { return 1; } } base_instance;', + 'class Worker : public Base { public: int run() override { return registry.size(); } };', + 'int tick() { Worker w; return w.run() + registry.size(); }', + '}', + '', + ].join('\n'), + 'src/setproctitle.c': [ + '/* Saved argv. */', + 'static struct { int argc; char **argv; } SPT;', + 'int spt_init(int argc) { SPT.argc = argc; return SPT.argc; }', + '', + ].join('\n'), + }; + 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); + } + cg = await CodeGraph.init(root, { index: true }); + const graph = cg; + const one = (qualifiedName: string) => { + const found = graph.getNodesByName(qualifiedName.split('::').pop()!).filter((n) => n.qualifiedName === qualifiedName); + expect(found, qualifiedName).toHaveLength(1); + return found[0]!; + }; + const edges = (id: string, kind: 'calls' | 'extends') => + graph + .getIncomingEdgesTo([id], [kind]) + .map((e) => graph.getNode(e.source)?.qualifiedName) + .sort(); + + expect(edges(one('app::Registry::size').id, 'calls')).toEqual(['app::Worker::run', 'app::tick']); + expect(edges(one('app::Base').id, 'extends')).toEqual(['app::Worker']); + + const spt = graph.searchNodes('SPT', { limit: 5 }).map((r) => `${r.node.kind} ${r.node.name}`); + expect(spt).toContain('struct SPT'); + expect(one('SPT').docstring).toBe('Saved argv.'); + }, 60_000); +}); diff --git a/__tests__/catch-all-path-alias.test.ts b/__tests__/catch-all-path-alias.test.ts new file mode 100644 index 000000000..7f4163151 --- /dev/null +++ b/__tests__/catch-all-path-alias.test.ts @@ -0,0 +1,184 @@ +/** + * A tsconfig `paths` entry keyed `"*"` matches every specifier, so treating + * "an alias pattern matches" as "the import is the project's" made every + * package import in such a project look local. The out-of-repo guard never + * fired there: on cord-field (`"baseUrl": "src"`, `"*": ["./typings/*"]`), + * 169 imports of `Typography` from `@mui/material` landed on a project + * `Typography`, and `Form` from `react-final-form` on the project's form. + * + * An alias counts only when it maps the specifier to a project file. A + * package the importing file's package.json declares, which no alias maps to + * a file, is outside the repository, catch-all or not. A catch-all that does + * find a file (a local `.d.ts` for an untyped package, or `"*": ["src/*"]` + * for the project's own folders) still makes that import the project's. + */ +import { describe, it, expect, afterAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; + +const roots: string[] = []; +afterAll(() => { + for (const r of roots.splice(0)) fs.rmSync(r, { recursive: true, force: true }); +}); + +async function indexProject(files: Record): Promise { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-catch-all-alias-')); + 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 }); +} + +/** + * ` :` for every edge leaving + * `file`'s nodes. JSX tags are left out: a separate pass links `; } +`, + 'src/components/Card.tsx': `export function Card(props: { children?: unknown }) { return
{String(props.children)}
; } +`, + 'src/components/form/Form.tsx': `export function Form() { return
; } +export function useForm() { return { valid: true }; } +`, + 'src/common/DateTime.ts': `export class DateTime { + static now(): DateTime { return new DateTime(); } +} +`, + // The catch-all does answer this package: its types live in the project. + 'src/typings/legacy-widgets.d.ts': `export interface WidgetOptions { name: string } +export declare class Widget { + constructor(options: WidgetOptions); + render(): void; +} +`, + 'src/scenes/Page.tsx': `import { Button, type ButtonProps } from '@mui/material'; +import { Form, useForm } from 'react-final-form'; +import { DateTime } from 'luxon'; +import { Widget, type WidgetOptions } from 'legacy-widgets'; +import { Card } from '~/components/Card'; + +const options: WidgetOptions = { name: 'page' }; + +export function Page(props: ButtonProps) { + const form = useForm(); + const when = DateTime.now(); + new Widget(options).render(); + return ; +} +`, + }); + try { + const fromPage = edgesFrom(cg, 'src/scenes/Page.tsx'); + // No name the page imports from a package lands on a project namesake. + expect(fromPage.filter((e) => /src\/components\/(?:Button|form\/Form)\.tsx|src\/common\/DateTime\.ts/.test(e))).toEqual([]); + // `~/…` still reaches the project, and so does a package whose types the + // catch-all finds in the project. + expect(fromPage).toContain('imports src/components/Card.tsx:Card'); + expect(fromPage).toContain('imports src/typings/legacy-widgets.d.ts:Widget'); + expect(fromPage).toContain('imports src/typings/legacy-widgets.d.ts:WidgetOptions'); + } finally { + cg.close(); + } + }); + + it('still makes an import the project’s when it maps the import to a project file', async () => { + const cg = await indexProject({ + 'package.json': JSON.stringify({ name: 'shop', dependencies: { '@mui/material': '^5.15.0', react: '^18.2.0' } }), + 'tsconfig.json': JSON.stringify({ compilerOptions: { baseUrl: '.', jsx: 'react-jsx', paths: { '*': ['src/*'] } } }), + 'src/components/Button.tsx': `export function Button() { return