diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e880cc69b..04363a2aa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,6 +95,18 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes +- 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. +- Go conventional import names are available before reading chained calls and keep source order; imported types also resolve from a module root. +- C++ receiver lookup reuses comment-stripped source and refuses outer receiver types hidden by unproven function declarations. +- React layout links stay with React routes, and navigation in an imported route-table file survives removing and restoring its destination. + +- Go chained calls with an unproven receiver no longer link an unrelated method by its name. Longer fluent chains follow each declared return type; typed receivers and package factory chains retain their links. +- Native Go import-name collection avoids recursively scanning function bodies, so deep expressions retain the existing per-file stack protection. +- C++ base lookup ignores using-directives in closed sibling scopes. React imported tables bind the exported module declaration rather than an unused local namesake. +- SolidStart TSX/JSX route targets remain eligible for framework resolution. Angular navigation from shared libraries is reconsidered when another app changes the sole-app fallback. + - Shopify templates and Liquid links now stay current when a theme marker is added or removed, including committed changes after reopening and ignored marker files in otherwise included themes. - Go interface type-set terms no longer appear as base interfaces; named aliases still link when their declarations reach an interface. @@ -432,6 +444,46 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In Go, an interface that embeds another, like gin's `IRouter` embedding `IRoutes`, now links to it, so `codegraph_explore` and impact show the embedded interface as its base. Before, every embedded interface was dropped. An embedded field written through another package of your project, like `*refresh.Discovery` or `storage.Appender`, or with type arguments, like `List[int]`, now links too. One from the standard library or another module, like `io.Closer` or `sync.Mutex`, links to nothing rather than to a project type that shares its name, and a type-set constraint like `~int | ~float64` is not an embedding. An embedded type written without a package now always links to its own package's type: before, etcd's `Client`, which embeds its own `Lease` interface, was linked to a `Lease` struct in another package. Re-index Go projects after upgrading. - In Angular templates, a property binding, an interpolation, a structural directive or a control-flow block that calls one of the component's own members, like `[name]="icon()"`, `{{ label() }}`, `*ngIf="isOpen()"` or `@if (loading()) {`, now links the component to that member, and so does reading a getter, like `[disabled]="!canSave"`, or handing a method to a child component, like `[displayWith]="displayFn"` or `trackBy: trackById`. Before, only event bindings such as `(click)="save()"` were read, so a signal, a `computed` value, a getter or a method used only from a template had no callers and looked unused. A pipe, a template variable, a call on another object such as `form.reset()`, and a plain field read like `[value]="title"` still link nothing. Re-index Angular projects after upgrading. + +- In Go, an exported type written without a package, like `Node` in `func Walk(v Visitor, node Node)`, now links to the type of that name in its own package, whether it is a struct, an interface or another kind of type. Before, it could link to a struct of the same name in an unrelated package, so prometheus's PromQL parser functions pointed at the Kubernetes discovery `Node` struct instead of the parser's own `Node` interface, or, for a result type like `Appender` in `func (f *fanout) Appender(…) Appender`, to the method itself. A type from a package outside your project, like `apiv1.Node` or `http.Handler`, no longer links to a struct of the same name in the file that uses it; a variadic parameter like `...storage.Filter` now links to that package's type; a route handler written as a method value, like `h.Follow` in an Echo or Gin app, links to that method rather than a model struct named `Follow`; and a method called on another call's result or on an indexed value, like `err[i].Error()` or a chained `.String()`, is no longer recorded as creating a struct of the same name. Re-index Go projects after upgrading. + +- In Go, a function or method passed as a value, like `sync.Pool{New: wm.new}`, now shows the function that passes it among its callers and impact even when it is named `new`, `nil`, `None`, `self` or a handful of similar words, or when a line break or comment sits beside the dot. Before, such a value was skipped, so a method used only that way looked unused. Re-index Go projects after upgrading. + +- In Go, a type in a parameter, a result or a composite literal now always links to a type, never to a method or function that shares its name. Before, an unexported type like `keyIndex` in etcd's `func (ti *treeIndex) KeyIndex(keyi *keyIndex) *keyIndex` linked to the `keyIndex` method declared beside it, prometheus's `samples{…}` literals to a `samples` method, and a type from a package outside your project, like `config.URL` in `&config.URL{…}`, to whatever project method had that name. A type written without a package, including an unexported one or one named like `AppenderV2` or `Histogram_CountInt`, now links to its own package's type when there is one, and a generic method's type parameter, like `T` in `func (p *Pool[T]) Get() T`, no longer links to an unrelated `T`. Re-index Go projects after upgrading. + +- Indexing a project that ships a large minified JavaScript file, such as the GraphiQL bundle inside go-ethereum, no longer stalls at "Resolving refs" when CodeGraph runs on Node 22, and linking such a file is faster on any Node version. The graph it builds is unchanged. + +- `codegraph sync` and the file watcher now link a navigation call to its page when the page or route is added after the call was indexed, or comes back after being removed: `history.push('/login')` and `navigate('/login')` with React Router, `router.push('/login')` and `redirect('/login')` in Next.js, Expo Router, Vue Router, TanStack Router and SvelteKit (`goto('/login')`), and `this.router.navigate(['/login'])` and `routerLink="/login"` in Angular. Before, the call stayed unlinked, or linked to a catch-all or `:id` page that answered it in the meantime, until its own file changed or the project was re-indexed, so the step from that call to the page it opens was missing or wrong. A route whose path changes without its own file changing, like one built from a path constant kept in another file, now also lets go of the calls that named its old path. Re-index after upgrading. + +- `codegraph sync` and the file watcher now link a Shopify section or snippet to the files that name it when it is added after them, or comes back after being deleted, whether they name it in a `{% render %}`, `{% include %}` or `{% section %}` tag or as a section `type` in a JSON template. Before, the link appeared only once a file naming it changed or the project was re-indexed, even in a repository with a single theme, so the new section or snippet looked unused and `codegraph affected` missed the files that use it. Re-index Shopify themes after upgrading to pick up links an earlier version missed. + +- `codegraph sync` and the file watcher now link a route to the page or layout it loads lazily when that file is added after the router was indexed, comes back after being deleted, or gets the component the route renders in a later edit, whether or not the import names the file's extension: React Router's `lazy: () => import('./pages/Team')`, Vue Router's `component: () => import('@/views/Login')` and Angular's `loadComponent: () => import('./home/home.component')`. Before, the route stayed unlinked until the router file changed or the project was re-indexed, so the page looked unused and `codegraph affected` missed the tests that reach it through the router. Re-index after upgrading to pick up links an earlier version missed. + +- In C++, a base class like `Message` in `class DynamicMessage final : public Message` now links to the class C++ itself would pick: the one declared in the namespaces around the class, or brought in by the file's `using` declarations and namespace aliases, and the class a `typedef` or `using` alias names. Before, a base class was matched by its name alone, so it could land on a class of the same name in another namespace or another language: in protocolbuffers/protobuf, `DynamicMessage` derived from the JSON parser's internal `Message` class, and in google/leveldb and RocksDB, iterators derived from an unrelated nested `Iterator` class such as the skip list's. The wrong base showed up in `codegraph_explore`'s type hierarchy and in impact, and tied the base's methods to the wrong overrides. A template parameter used as a base no longer links to a class that shares its name, and a base written from the global scope, like `public ::testing::Test`, now links to its class. Re-index C++ projects after upgrading. + +- In C and C++, a header or source file named like a test, such as protocolbuffers/protobuf's `conformance_test.h` and `test_runner.h`, now counts as part of the code that `#include`s it, directly or through other headers. Before, calls from that code into it were dropped, so protobuf's conformance suites' `suite_.ReportFailure(…)` and `RunValidInputTest(…)` calls, and its unit tests' calls to the shared `TestUtil` helpers, linked to nothing. A test file that only tests include is still kept out of reach of the rest of your code. Re-index C and C++ projects after upgrading. + +- In C++, a class, struct or method now keeps the namespaces and the class it is declared in when the parser misreads something earlier in the file, such as an unknown macro in front of a member or in a class header. Before, everything after the misread code could lose them, or land inside a class it isn't in: in protocolbuffers/protobuf, `FieldDescriptor` was indexed without `google::protobuf::`, and in RocksDB, the `Opts` struct declared inside a cache table class was indexed outside it, so finding a class's base classes and the methods called on its objects fell back to guessing by name. Methods that had come loose from their class are its members again, and classes the misread code had hidden, like leveldb's POSIX file and environment classes, are now indexed. Re-index C++ projects after upgrading. + +- 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 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 Go, working out which structs implement an interface now counts the methods of the interfaces it embeds, and the methods a struct gets from the types it embeds. Before, only each type's own methods counted: etcd's `AuthReadTx`, which embeds `UnsafeAuthReader` and adds `RLock` and `RUnlock`, was listed as implemented by every read-write lock in the project, an interface that only embeds others had no implementations at all, and a struct that gets its methods from an embedded base, like prometheus's service discoveries embedding `refresh.Discovery` or gin's `Engine` embedding `RouterGroup`, was missing. `codegraph_explore`, impact and the type hierarchy now list the right implementations, and a call through the interface reaches a method the embedding struct overrides, like gin's `Engine.Use`. Re-index Go projects after upgrading. + +- In Go, a call through an interface now reaches a method that the implementing struct gets from a type it embeds: prometheus's `targetScraper` gets `Report` from the `*Target` it embeds, so a call through the `scraper` interface now reaches `Target.Report`, and etcd's `Hasher.MemberID` reaches `EtcdServer.MemberID` through `hasherAdapter`. Before, the call reached only structs that declare the method themselves, often just a test double, so `codegraph_explore`'s call path stopped at the interface. The method is picked the way Go picks it, from the nearest embedded type, and none is linked when two embedded types at the same depth both have it or it comes from an embedded interface. Re-index Go projects after upgrading. + +- In Go, a type defined over a slice, map, function or basic type now counts as implementing the interfaces its methods satisfy, just as a struct does: prometheus's `type staticDiscoverer []*targetgroup.Group` is a `Discoverer`, gin's `type formSource map[string][]string` a `setter`, etcd's `type Int64Comparable int64` a `Comparable`, and an adapter like `type HandlerFunc func(…)` a `Handler`. Before, only structs were considered, so these types were missing from an interface's implementations in `codegraph_explore`, impact and the type hierarchy, and a call through the interface, like `Discoverer.Run` or `setter.TrySet`, stopped there instead of reaching their methods. Re-index Go projects after upgrading. + +- In Go, a type alias like `type Event = mvccpb.Event`, alone or inside a `type ( … )` group, is now indexed and links to the type it names, so impact on that type reaches the code that uses the alias. Before, aliases were missing: a use written through the alias's package, like `clientv3.Event` or prometheus's `storage.AOptions{…}`, linked to nothing, and one inside the package, like `&PrometheusVersion{}` in prometheus's `web` package, linked to whichever type of that name the project had, there a test helper's struct. A method called on a value of an alias's type now reaches the aliased type's method rather than a namesake's, and a generic alias like `type Items[T any] = List[T]` links to `List`, so `xs.Len()` on one reaches `List.Len` instead of nothing. Re-index Go projects after upgrading. + +- 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. #### 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 c80324c330..f550373c3b 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 [`02d22aeb`](https://github.com/colbymchenry/codegraph/commit/02d22aebedd7fa70ddfe8b368c43bb24a80bdf0a) (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 [`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. 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 `02d22aeb` (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 `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. | 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. +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. Named JavaScript and TypeScript object literals own their function members, including local objects and classic-script global assignments. Member calls follow the visible object; loop-local objects stay within their scope. Destructured member calls resolve across lines while preserving the source binding at the destructure declaration; unrelated bare names stay unresolved. Vue template expressions contribute calls to script bindings while preserving component ownership. Encoded attribute expressions retain their original source positions, and template-local bindings stay within their scope. diff --git a/__tests__/cpp-base-class-lookup.test.ts b/__tests__/cpp-base-class-lookup.test.ts new file mode 100644 index 0000000000..7b01b05a9b --- /dev/null +++ b/__tests__/cpp-base-class-lookup.test.ts @@ -0,0 +1,515 @@ +/** + * A C++ base class — `class DynamicMessage final : public Message` — is the + * class C++ name lookup finds from where the deriving class is declared. + * + * The base was matched by its name alone, so one named like a class of + * another namespace bound to whichever namesake ranked first. + * protocolbuffers/protobuf's `google::protobuf::DynamicMessage` derived from + * `json_internal::ResolverPool::Message` (and, before #2397, from the PHP + * extension's C struct `Message`); google/leveldb's iterators derived from + * `SkipList::Iterator`, and its test comparators from a struct + * `skiplist_test.cc` declares for itself. The wrong supertype then fed the + * type hierarchy, the cpp-override edges from a base method to its overrides, + * and the supertype walk that resolves a call to an inherited method. + * + * The lookup goes outwards from the scope the class is declared in: a class + * scope with its bases, a qualified name a segment at a time, an alias + * followed to the class it names, `::Base` from the global scope. Then + * through what the file writes before the class: a namespace alias, a + * using-declaration, a using-directive. A class a source file defines is that + * translation unit's own. A template parameter names no class. When none of + * that finds it, a class of that name the file can see is taken only when it + * is the only one. + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; +import { buildTypeHierarchy } from '../src/graph/type-hierarchy'; +import type { Node } from '../src/types'; + +const FILES: Record = { + // protocolbuffers/protobuf: the real `Message`, a nested namesake in + // json_internal, and the PHP extension's C struct. + 'src/google/protobuf/message_lite.h': [ + 'namespace google {', + 'namespace protobuf {', + 'namespace internal {', + 'class MessageGlobalsBase {', + ' public:', + ' int default_instance() const { return 0; }', + '};', + '} // namespace internal', + '} // namespace protobuf', + '} // namespace google', + '', + ].join('\n'), + 'src/google/protobuf/message.h': [ + '#include "google/protobuf/message_lite.h"', + 'namespace google {', + 'namespace protobuf {', + 'class Message {', + ' public:', + ' virtual ~Message();', + ' virtual int ByteSizeLong() const = 0;', + '};', + '} // namespace protobuf', + '} // namespace google', + '', + ].join('\n'), + 'src/google/protobuf/json/internal/untyped_message.h': [ + 'namespace google {', + 'namespace protobuf {', + 'namespace json_internal {', + 'class ResolverPool {', + ' public:', + ' class Message {', + ' public:', + ' int ByteSizeLong() const { return 0; }', + ' };', + '};', + '} // namespace json_internal', + '} // namespace protobuf', + '} // namespace google', + '', + ].join('\n'), + 'php/ext/google/protobuf/message.c': [ + 'typedef struct {', + ' int std;', + '} Message;', + '', + ].join('\n'), + 'src/google/protobuf/dynamic_message.cc': [ + '#include "google/protobuf/message.h"', + 'namespace google {', + 'namespace protobuf {', + 'class DynamicMessage final : public Message {', + ' public:', + ' int ByteSizeLong() const override { return 1; }', + '};', + '} // namespace protobuf', + '} // namespace google', + '', + ].join('\n'), + 'src/google/protobuf/generated_message_bases.h': [ + '#include "google/protobuf/message.h"', + 'namespace google {', + 'namespace protobuf {', + 'namespace internal {', + 'class ZeroFieldsBase : public Message {', + ' public:', + ' int ByteSizeLong() const override { return 0; }', + '};', + '} // namespace internal', + '} // namespace protobuf', + '} // namespace google', + '', + ].join('\n'), + // Generated code names its bases through a namespace alias. + 'src/google/protobuf/any.pb.cc': [ + '#include "google/protobuf/message_lite.h"', + 'namespace _pbi = ::google::protobuf::internal;', + 'namespace google {', + 'namespace protobuf {', + 'struct AnyGlobalsTypeInternal : ::_pbi::MessageGlobalsBase {', + ' int x;', + '};', + '} // namespace protobuf', + '} // namespace google', + '', + ].join('\n'), + + // google/leveldb: the public iterator, the skip list's nested one, a test's + // own `Comparator`, and the C API's using-declarations. + 'include/leveldb/iterator.h': [ + 'namespace leveldb {', + 'class Iterator {', + ' public:', + ' virtual ~Iterator();', + ' virtual bool Valid() const = 0;', + ' virtual void Next() = 0;', + '};', + '} // namespace leveldb', + '', + ].join('\n'), + 'db/skiplist.h': [ + 'namespace leveldb {', + 'template ', + 'class SkipList {', + ' public:', + ' class Iterator {', + ' public:', + ' bool Valid() const { return node_ != nullptr; }', + ' void Next() { node_ = nullptr; }', + ' void SeekToFirst() { node_ = nullptr; }', + ' private:', + ' const char* node_;', + ' };', + '};', + '} // namespace leveldb', + '', + ].join('\n'), + 'db/db_iter.cc': [ + '#include "include/leveldb/iterator.h"', + 'namespace leveldb {', + 'class DBIter : public Iterator {', + ' public:', + ' bool Valid() const override { return valid_; }', + ' void Next() override { valid_ = false; }', + ' private:', + ' bool valid_;', + '};', + '} // namespace leveldb', + '', + ].join('\n'), + 'include/leveldb/comparator.h': [ + 'namespace leveldb {', + 'class Comparator {', + ' public:', + ' virtual int Compare(const char* a, const char* b) const = 0;', + '};', + '} // namespace leveldb', + '', + ].join('\n'), + 'db/skiplist_test.cc': [ + 'namespace leveldb {', + 'struct Comparator {', + ' int operator()(const int& a, const int& b) const { return a - b; }', + '};', + '} // namespace leveldb', + '', + ].join('\n'), + 'db/db_test.cc': [ + '#include "include/leveldb/comparator.h"', + 'namespace leveldb {', + 'class NumberComparator : public Comparator {', + ' public:', + ' int Compare(const char* a, const char* b) const override { return 0; }', + '};', + '} // namespace leveldb', + '', + ].join('\n'), + 'include/leveldb/filter_policy.h': [ + 'namespace leveldb {', + 'class FilterPolicy {', + ' public:', + ' virtual const char* Name() const = 0;', + '};', + '} // namespace leveldb', + '', + ].join('\n'), + 'db/c.cc': [ + '#include "include/leveldb/filter_policy.h"', + 'using leveldb::FilterPolicy;', + 'struct leveldb_filterpolicy_t : public FilterPolicy {', + ' const char* Name() const override { return "c"; }', + '};', + '', + ].join('\n'), + + // facebook/rocksdb: a nested class's base is a member its outer class + // inherits; a base named through an alias; a using-directive. + 'cache/clock_cache.h': [ + 'namespace rocksdb {', + 'namespace clock_cache {', + 'class BaseClockTable {', + ' public:', + ' struct BaseOpts {', + ' int eviction_effort_cap;', + ' };', + '};', + 'class AutoHyperClockTable : public BaseClockTable {', + ' public:', + ' struct Opts : public BaseOpts {', + ' int min_avg_value_size;', + ' };', + '};', + '} // namespace clock_cache', + '} // namespace rocksdb', + '', + ].join('\n'), + 'cache/lru_cache.h': [ + 'namespace rocksdb {', + 'namespace lru_cache {', + 'struct BaseOpts {', + ' int capacity;', + '};', + '} // namespace lru_cache', + '} // namespace rocksdb', + '', + ].join('\n'), + 'table/internal_iterator.h': [ + 'namespace rocksdb {', + 'class Slice {};', + 'template ', + 'class InternalIteratorBase {', + ' public:', + ' virtual void Next() = 0;', + ' void SeekToFirst() {}', + '};', + 'using InternalIterator = InternalIteratorBase;', + 'template using bool_constant = std::integral_constant;', + '} // namespace rocksdb', + '', + ].join('\n'), + 'table/merging_iterator.cc': [ + '#include "table/internal_iterator.h"', + 'namespace rocksdb {', + 'class MergingIterator : public InternalIterator {', + ' public:', + ' void Next() override {}', + '};', + 'void Rewind(MergingIterator* iter) {', + ' iter->SeekToFirst();', + '}', + 'template ', + 'struct is_fast_float : bool_constant {};', + '} // namespace rocksdb', + '', + ].join('\n'), + 'include/rocksdb/listener.h': [ + 'namespace rocksdb {', + 'class EventListener {', + ' public:', + ' virtual void OnFlushCompleted() {}', + '};', + '} // namespace rocksdb', + '', + ].join('\n'), + 'examples/compact_files_example.cc': [ + '#include "include/rocksdb/listener.h"', + 'using namespace rocksdb;', + 'namespace decoy { class EventListener {}; }', + 'namespace unrelated { using namespace decoy; }', + 'class Compactor : public EventListener {', + ' public:', + ' void OnFlushCompleted() override {}', + '};', + '', + ].join('\n'), + + // googletest: a base named from the global scope. + 'test/gtest/gtest.h': [ + 'namespace testing {', + 'class Test {', + ' public:', + ' virtual void TestBody() = 0;', + '};', + 'namespace internal {', + 'class SuiteApiResolver {', + ' public:', + ' typedef int Test;', + '};', + '} // namespace internal', + '} // namespace testing', + '', + ].join('\n'), + 'test/gtest-extra-test.cc': [ + '#include "test/gtest/gtest.h"', + 'class single_evaluation_test : public ::testing::Test {', + ' protected:', + ' void TestBody() override {}', + '};', + '', + ].join('\n'), + + // A template parameter names no class, whatever the project declares. + 'util/mixin.h': [ + 'class Base {', + ' public:', + ' void Run() {}', + '};', + 'template ', + 'class Logged : public Base {', + ' public:', + ' void Log() {}', + '};', + '', + ].join('\n'), + + // A header's using-directive the file itself does not repeat. + 'util/testharness.h': [ + '#include "include/leveldb/env.h"', + 'using namespace leveldb;', + '', + ].join('\n'), + 'include/leveldb/env.h': [ + 'namespace leveldb {', + 'class EnvWrapper {', + ' public:', + ' virtual int NowMicros() { return 0; }', + '};', + '} // namespace leveldb', + '', + ].join('\n'), + 'util/env_test.cc': [ + '#include "util/testharness.h"', + 'class SleepingEnv : public EnvWrapper {', + ' public:', + ' int NowMicros() override { return 1; }', + '};', + '', + ].join('\n'), +}; + +let root = ''; +let cg: CodeGraph; + +beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-bases-')); + 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 }); +}, 60_000); + +afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); +}); + +function node(qualifiedName: string, file: string): Node { + const name = qualifiedName.split('::').pop()!; + const found = cg.getNodesByName(name).filter((n) => n.qualifiedName === qualifiedName && n.filePath === file && n.kind !== 'import'); + expect(found, `${qualifiedName} in ${file}`).toHaveLength(1); + return found[0]!; +} + +/** The supertypes of a class, as `kind qualifiedName (file) resolvedBy`. */ +function supertypes(qualifiedName: string, file: string): string[] { + return cg + .getOutgoingEdgesFrom([node(qualifiedName, file).id], ['extends', 'implements']) + .map((e) => { + const target = cg.getNode(e.target); + return `${e.kind} ${target?.qualifiedName} (${target?.filePath}) ${String(e.metadata?.resolvedBy)}`; + }) + .sort(); +} + +/** The base names a class's file names that stay unresolved. */ +function unresolved(qualifiedName: string, file: string): string[] { + return cg + .getUnresolvedReferencesFrom(node(qualifiedName, file).id) + .filter((r) => r.referenceKind === 'extends' || r.referenceKind === 'implements') + .map((r) => r.referenceName) + .sort(); +} + +describe('a C++ base class is the one name lookup finds from the deriving class', () => { + it('finds the base in the namespaces around the class, not a namesake elsewhere', () => { + expect(supertypes('google::protobuf::DynamicMessage', 'src/google/protobuf/dynamic_message.cc')).toEqual([ + 'extends google::protobuf::Message (src/google/protobuf/message.h) qualified-name', + ]); + expect(supertypes('google::protobuf::internal::ZeroFieldsBase', 'src/google/protobuf/generated_message_bases.h')).toEqual([ + 'extends google::protobuf::Message (src/google/protobuf/message.h) qualified-name', + ]); + expect(supertypes('leveldb::DBIter', 'db/db_iter.cc')).toEqual([ + 'extends leveldb::Iterator (include/leveldb/iterator.h) qualified-name', + ]); + }); + + it('never takes a class another source file defines for its own translation unit', () => { + expect(supertypes('leveldb::NumberComparator', 'db/db_test.cc')).toEqual([ + 'extends leveldb::Comparator (include/leveldb/comparator.h) qualified-name', + ]); + }); + + it('looks a nested class base up in its outer class and the classes that one inherits', () => { + expect(supertypes('rocksdb::clock_cache::AutoHyperClockTable::Opts', 'cache/clock_cache.h')).toEqual([ + 'extends rocksdb::clock_cache::BaseClockTable::BaseOpts (cache/clock_cache.h) exact-match', + ]); + }); + + it('reads the namespace aliases, using-declarations and using-directives the file writes', () => { + expect(supertypes('google::protobuf::AnyGlobalsTypeInternal', 'src/google/protobuf/any.pb.cc')).toEqual([ + 'extends google::protobuf::internal::MessageGlobalsBase (src/google/protobuf/message_lite.h) qualified-name', + ]); + expect(supertypes('leveldb_filterpolicy_t', 'db/c.cc')).toEqual([ + 'extends leveldb::FilterPolicy (include/leveldb/filter_policy.h) qualified-name', + ]); + expect(supertypes('Compactor', 'examples/compact_files_example.cc')).toEqual([ + 'extends rocksdb::EventListener (include/rocksdb/listener.h) qualified-name', + ]); + }); + + it('finds `::testing::Test` from the global scope, not a nested `Test` typedef', () => { + expect(supertypes('single_evaluation_test', 'test/gtest-extra-test.cc')).toEqual([ + 'extends testing::Test (test/gtest/gtest.h) qualified-name', + ]); + }); + + it('follows an alias to the class it names, and links an alias of an outside type itself', () => { + expect(supertypes('rocksdb::MergingIterator', 'table/merging_iterator.cc')).toEqual([ + 'extends rocksdb::InternalIteratorBase (table/internal_iterator.h) qualified-name', + ]); + // `bool_constant`: the comparison is no template bracket. + expect(supertypes('rocksdb::is_fast_float', 'table/merging_iterator.cc')).toEqual([ + 'extends rocksdb::bool_constant (table/internal_iterator.h) qualified-name', + ]); + }); + + it('leaves a template parameter unresolved', () => { + expect(supertypes('Logged', 'util/mixin.h')).toEqual([]); + expect(unresolved('Logged', 'util/mixin.h')).toEqual(['Base']); + }); + + it('takes the only class of that name the file can see when the lookup finds none', () => { + // `using namespace leveldb;` is in a header the file includes. + expect(supertypes('SleepingEnv', 'util/env_test.cc')).toEqual([ + 'extends leveldb::EnvWrapper (include/leveldb/env.h) exact-match', + ]); + }); + + it('never resolves a C or C++ base through a framework resolver or to another language', () => { + const bases = cg + .getNodesByKind('class') + .concat(cg.getNodesByKind('struct')) + .filter((n) => n.language === 'cpp' || n.language === 'c'); + const edges = cg.getOutgoingEdgesFrom(bases.map((n) => n.id), ['extends', 'implements']); + expect(edges.length).toBeGreaterThan(0); + for (const e of edges) { + const target = cg.getNode(e.target); + expect(e.metadata?.resolvedBy, `${cg.getNode(e.source)?.qualifiedName} → ${target?.qualifiedName}`).not.toBe('framework'); + expect(['c', 'cpp']).toContain(target?.language); + } + expect(cg.getIncomingEdgesTo([node('Message', 'php/ext/google/protobuf/message.c').id], ['extends'])).toEqual([]); + }); +}); + +describe('what the hierarchy feeds', () => { + it('links each base method to the overrides of the right subclasses', () => { + const overrides = (qualifiedName: string, file: string): string[] => + cg + .getOutgoingEdgesFrom([node(qualifiedName, file).id], ['calls']) + .filter((e) => e.metadata?.synthesizedBy === 'cpp-override') + .map((e) => cg.getNode(e.target)?.qualifiedName ?? '') + .sort(); + expect(overrides('leveldb::Iterator::Next', 'include/leveldb/iterator.h')).toEqual(['leveldb::DBIter::Next']); + expect(overrides('leveldb::SkipList::Iterator::Next', 'db/skiplist.h')).toEqual([]); + expect(overrides('google::protobuf::Message::ByteSizeLong', 'src/google/protobuf/message.h')).toEqual([ + 'google::protobuf::DynamicMessage::ByteSizeLong', + 'google::protobuf::internal::ZeroFieldsBase::ByteSizeLong', + ]); + expect(overrides('google::protobuf::json_internal::ResolverPool::Message::ByteSizeLong', 'src/google/protobuf/json/internal/untyped_message.h')).toEqual([]); + // Through the alias: InternalIterator is InternalIteratorBase. + expect(overrides('rocksdb::InternalIteratorBase::Next', 'table/internal_iterator.h')).toEqual(['rocksdb::MergingIterator::Next']); + }); + + it('resolves a call to an inherited method on the base an alias names', () => { + const callees = cg + .getOutgoingEdgesFrom([node('rocksdb::Rewind', 'table/merging_iterator.cc').id], ['calls']) + .map((e) => `${cg.getNode(e.target)?.qualifiedName} (${cg.getNode(e.target)?.filePath})`); + expect(callees).toEqual(['rocksdb::InternalIteratorBase::SeekToFirst (table/internal_iterator.h)']); + }); + + it('shows the real ancestors in the type hierarchy', () => { + const ancestors = (qualifiedName: string, file: string): string[] => + (buildTypeHierarchy(cg, node(qualifiedName, file))?.ancestors ?? []).map((a) => `${a.relation} ${a.node.qualifiedName} (${a.node.filePath})`); + expect(ancestors('google::protobuf::DynamicMessage', 'src/google/protobuf/dynamic_message.cc')).toEqual([ + 'extends google::protobuf::Message (src/google/protobuf/message.h)', + ]); + expect(ancestors('leveldb::DBIter', 'db/db_iter.cc')).toEqual(['extends leveldb::Iterator (include/leveldb/iterator.h)']); + }); +}); diff --git a/__tests__/cpp-brace-scopes.test.ts b/__tests__/cpp-brace-scopes.test.ts new file mode 100644 index 0000000000..a8b1acbb9a --- /dev/null +++ b/__tests__/cpp-brace-scopes.test.ts @@ -0,0 +1,352 @@ +/** + * A C++ class keeps the namespaces and enclosing classes it is declared in + * when tree-sitter's error recovery closes one of them at the wrong `}`. + * + * tree-sitter recovers from a construct it can't parse by inserting the token + * it expected, and inside a class that is often a `}`. An unknown macro in + * front of a member (protobuf's `PROTOBUF_FUTURE_ADD_EARLY_NODISCARD + * absl::string_view name() const`) or in a class head (rocksdb's `struct + * ALIGN_AS(64U) HandleImpl`) closed the class there, and every `}` after it + * then closed the scope one level out. The rest of the class parsed as + * declarations of its namespace, and the rest of each namespace as + * declarations of the one around it: + * - protobuf's `FieldDescriptor` was indexed as `FieldDescriptor`, not + * `google::protobuf::FieldDescriptor`; + * - rocksdb's `struct Opts` lost the class it is declared in, and + * `AutoHyperClockTable` lost `clock_cache::`; + * - a scope could also stay open past its own `}`, putting the next class + * inside the previous one. + * Lookups that start from a declaration's scope (base classes, alias + * receivers, constructors) then had only a unique-name guess left. + * + * For a file whose tree has errors, the walker now takes each declaration's + * namespaces and enclosing classes from the source's braces, which the + * parser rarely misreads; a class the tree glued into a declaration's type is + * still walked as a class. Files that parse cleanly keep the tree's scopes. + * The native kernel extracts both clean and erroring files. + */ +import { describe, it, expect, beforeAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import CodeGraph from '../src/index'; +import { extractFromSource } from '../src/extraction'; +import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars'; +import { cppExtractor } from '../src/extraction/languages/c-cpp'; +import { scanCppBraceScopes } from '../src/extraction/languages/cpp-brace-scopes'; +import { parseSourceTreeSync } from '../src/extraction/parse-tree'; +import type { ExtractionResult, Node } from '../src/types'; + +/** protocolbuffers/protobuf descriptor.h: an unknown attribute macro in front of a member. */ +const PROTOBUF_SHAPE = `namespace google { +namespace protobuf { + +class Names { + public: + PROTOBUF_FUTURE_ADD_EARLY_NODISCARD absl::string_view name() const { + return absl::string_view(payload_); + } + + private: + const char* payload_; +}; + +class FieldDescriptor : private internal::SymbolBase { + public: + int index() const; +}; + +} // namespace protobuf +} // namespace google +`; + +/** facebook/rocksdb cache/clock_cache.h: a macro call in a nested struct's head. */ +const ROCKSDB_SHAPE = `namespace rocksdb { +namespace clock_cache { + +class FixedTable : public BaseTable { + public: + struct ALIGN_AS(64U) HandleImpl : public ClockHandle { + RelaxedAtomic displacements{}; + bool standalone = false; + }; + + struct Opts : public BaseOpts { + explicit Opts(size_t size) : BaseOpts(0), size(size) {} + size_t size; + }; + + void Insert(const Opts& opts); +}; + +class AutoTable : public BaseTable { + public: + void Erase(); +}; + +} // namespace clock_cache +} // namespace rocksdb +`; + +/** A function head per #if branch: recovery keeps the class open past its own `}`. */ +const BRANCHES_SHAPE = `namespace net { + +class Socket { + public: +#ifdef _WIN32 + int Open(SOCKET handle) { +#else + int Open(int fd) { +#endif + return Bind(); + } + + int Bind(); +}; + +class Listener { + public: + void Accept() {} +}; + +} // namespace net +`; + +/** fmtlib/fmt core.h: \`unsigned(id) < unsigned(…)\` read as a template turns the namespaces into an ERROR. */ +const FMT_SHAPE = `namespace fmt { +namespace detail { + +template class basic_format_args { + public: + FMT_CONSTEXPR auto get(int id) const -> format_arg { + auto arg = format_arg(); + if (!is_packed()) { + if (unsigned(id) < unsigned(max_size())) arg = args_[id]; + return arg; + } + if (unsigned(id) >= detail::max_packed_args) return arg; + return arg; + } + + auto max_size() const -> int { return 0; } +}; + +class context { + public: + void advance_to(int it) {} +}; + +} // namespace detail +} // namespace fmt +`; + +const SHAPES: Record = { + 'descriptor.h': PROTOBUF_SHAPE, + 'clock_cache.h': ROCKSDB_SHAPE, + 'socket.h': BRANCHES_SHAPE, + 'core.h': FMT_SHAPE, +}; + +const extractFromSourceCpp = (file: string, source: string) => extractFromSource(file, source, 'cpp'); + +const named = (result: ExtractionResult, kind: string): string[] => + result.nodes.filter((n) => n.kind === kind).map((n) => n.qualifiedName); + +const nodeAt = (result: ExtractionResult, qualifiedName: string): Node | undefined => + result.nodes.find((n) => n.qualifiedName === qualifiedName); + +describe('C++ scopes from the source braces of a file whose tree has errors', () => { + beforeAll(async () => { + await initGrammars(); + await loadGrammarsForLanguages(['cpp']); + }); + + it('every fixture still misparses (else the cases below prove nothing)', () => { + for (const [file, source] of Object.entries(SHAPES)) { + const tree = parseSourceTreeSync(cppExtractor.preParse!(source, file), 'cpp')!; + expect(tree.rootNode.hasError, file).toBe(true); + tree.delete(); + } + }); + + it('a class after one closed early keeps its namespaces (protobuf FieldDescriptor)', () => { + const result = extractFromSourceCpp('src/google/protobuf/descriptor.h', PROTOBUF_SHAPE); + expect(named(result, 'class')).toEqual(['google::protobuf::Names', 'google::protobuf::FieldDescriptor']); + // `Names` is the one recovery glued to the tokens after it: it ends at its own `};`. + expect(nodeAt(result, 'google::protobuf::Names')!.endLine).toBe(12); + }); + + it('a nested struct keeps its class and the next class its namespaces (rocksdb clock_cache)', () => { + const result = extractFromSourceCpp('cache/clock_cache.h', ROCKSDB_SHAPE); + expect(named(result, 'class')).toEqual(['rocksdb::clock_cache::FixedTable', 'rocksdb::clock_cache::AutoTable']); + expect(named(result, 'struct')).toEqual(['rocksdb::clock_cache::FixedTable::Opts']); + expect(named(result, 'method')).toEqual(['rocksdb::clock_cache::FixedTable::Opts::Opts']); + // The class spans its whole body, so the struct it holds lies inside it. + const fixed = nodeAt(result, 'rocksdb::clock_cache::FixedTable')!; + expect([fixed.startLine, fixed.endLine]).toEqual([4, 17]); + const contains = result.edges.find( + (e) => e.kind === 'contains' && e.target === nodeAt(result, 'rocksdb::clock_cache::FixedTable::Opts')!.id + ); + expect(contains?.source).toBe(fixed.id); + }); + + it('a class the tree kept open past its `}` lets go of the next class', () => { + const result = extractFromSourceCpp('net/socket.h', BRANCHES_SHAPE); + expect(named(result, 'class')).toEqual(['net::Socket', 'net::Listener']); + expect(named(result, 'method')).toContain('net::Listener::Accept'); + expect(nodeAt(result, 'net::Socket')!.endLine).toBe(14); + }); + + it('namespaces the tree swallowed into an ERROR still prefix what they hold (fmt core.h)', () => { + const result = extractFromSourceCpp('include/fmt/core.h', FMT_SHAPE); + expect(named(result, 'class')).toEqual(['fmt::detail::context']); + expect(named(result, 'method')).toEqual(['fmt::detail::context::advance_to']); + }); + + it('a file that parses cleanly keeps the tree scopes', () => { + const clean = PROTOBUF_SHAPE.replace('PROTOBUF_FUTURE_ADD_EARLY_NODISCARD ', ''); + const tree = parseSourceTreeSync(cppExtractor.preParse!(clean, 'descriptor.h'), 'cpp')!; + expect(tree.rootNode.hasError).toBe(false); + tree.delete(); + const result = extractFromSourceCpp('src/google/protobuf/descriptor.h', clean); + expect(named(result, 'class')).toEqual(['google::protobuf::Names', 'google::protobuf::FieldDescriptor']); + expect(named(result, 'method')).toEqual(['google::protobuf::Names::name']); + }); + + it('the base class of a class that lost its namespace resolves by scope lookup', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-cpp-brace-scopes-')); + try { + fs.mkdirSync(path.join(dir, 'src'), { recursive: true }); + fs.writeFileSync(path.join(dir, 'src', 'descriptor.h'), PROTOBUF_SHAPE); + // The base, and a namesake in another namespace: with the namespace + // lost, neither a scope lookup nor the only-candidate guess finds it. + fs.writeFileSync( + path.join(dir, 'src', 'symbol_base.h'), + 'namespace google {\nnamespace protobuf {\nnamespace internal {\nclass SymbolBase {\n public:\n int kind() const;\n};\n} // namespace internal\n} // namespace protobuf\n} // namespace google\n' + ); + fs.writeFileSync( + path.join(dir, 'src', 'other.h'), + 'namespace other {\nnamespace internal {\nclass SymbolBase {\n public:\n int kind() const;\n};\n} // namespace internal\n} // namespace other\n' + ); + const cg = await CodeGraph.init(dir); + try { + await cg.indexAll(); + const derived = cg.searchNodes('FieldDescriptor').map((r) => r.node).find((n) => n.kind === 'class')!; + expect(derived.qualifiedName).toBe('google::protobuf::FieldDescriptor'); + const bases = cg + .getOutgoingEdges(derived.id) + .filter((e) => e.kind === 'extends') + .map((e) => cg.getNode(e.target)?.qualifiedName); + expect(bases).toEqual(['google::protobuf::internal::SymbolBase']); + } finally { + cg.close(); + } + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); +}); + +describe('scanCppBraceScopes', () => { + it('matches braces past comments, literals and preprocessor lines', () => { + const source = [ + 'namespace a {', + '// } a comment', + '/* { */ const char* s = "}{";', + "char c = '}'; int n = 1'000'000;", + 'auto r = R"x(})x";', + '#define OPEN {', + 'struct S { int x; };', + '}', + '', + ].join('\n'); + const scopes = scanCppBraceScopes(source)!; + expect(scopes).not.toBeNull(); + const open = source.indexOf('{'); + expect(scopes.closeOf(open)).toBe(source.lastIndexOf('}')); + const struct = source.indexOf('S {') + 2; + expect(source.slice(struct, scopes.closeOf(struct)! + 1)).toBe('{ int x; }'); + expect(scopes.namespacesAt(struct)).toEqual(['a']); + }); + + it('reads each #if branch from the braces open at the #if', () => { + const source = [ + 'namespace a {', + '#if defined(X)', + 'struct S : Base {', + '#else', + 'struct S {', + '#endif', + ' int x;', + '};', + 'struct T {};', + '}', + '', + ].join('\n'); + const scopes = scanCppBraceScopes(source)!; + expect(scopes).not.toBeNull(); + const first = source.indexOf('Base {') + 5; + expect(scopes.closeOf(first)).toBe(source.indexOf('};')); + expect(scopes.namespacesAt(source.indexOf('struct T'))).toEqual(['a']); + }); + + it('names namespaces as written and skips what declares none', () => { + const source = [ + 'namespace a::b {', + 'namespace [[deprecated]] c {', + 'inline namespace v1 {', + 'namespace {', + 'using namespace std;', + 'namespace fs = std::filesystem;', + 'int here;', + '}', + '}', + '}', + '}', + 'namespace std _GLIBCXX_VISIBILITY(default) {', + 'int there;', + '}', + '', + ].join('\n'); + const scopes = scanCppBraceScopes(source)!; + expect(scopes.namespacesAt(source.indexOf('int here'))).toEqual(['a::b', 'c', 'v1']); + expect(scopes.namespacesAt(source.indexOf('int there'))).toEqual(['std']); + }); + + it('gives up on braces that do not balance', () => { + expect(scanCppBraceScopes('namespace a {\nint x;\n')).toBeNull(); + expect(scanCppBraceScopes('int x;\n}\n')).toBeNull(); + }); + + it('places offsets in UTF-16 columns, as the parser does', () => { + const source = 'namespace n {\n/* é😀 */ class C { int x; };\n}\n'; + const scopes = scanCppBraceScopes(source)!; + const close = scopes.closeOf(source.indexOf('{ int'))!; + expect(scopes.positionOf(close + 1)).toEqual({ line: 2, column: 28 }); + }); +}); + +describe('C++ native brace recovery with CRLF', () => { + it('reports the recovered class end in UTF-16 columns after a non-ASCII prefix', () => { + const source = 'namespace n {\r\n/* é😀 */ class C { UNKNOWN_MACRO void f() {} };\r\n}\r\n'; + const tree = parseSourceTreeSync(source, 'cpp')!; + try { + expect(tree.rootNode.hasError).toBe(true); + } finally { + tree.delete(); + } + const node = nodeAt(extractFromSource('unicode.hpp', source, 'cpp'), 'n::C')!; + expect([node.endLine, node.endColumn]).toEqual([2, 47]); + }); + + const classes: Record = { + 'descriptor.h': ['google::protobuf::Names', 'google::protobuf::FieldDescriptor'], + 'clock_cache.h': ['rocksdb::clock_cache::FixedTable', 'rocksdb::clock_cache::AutoTable'], + 'socket.h': ['net::Socket', 'net::Listener'], + 'core.h': ['fmt::detail::context'], + }; + it.each(Object.keys(SHAPES))('%s keeps its declaration scopes', (file) => { + const result = extractFromSource(file, SHAPES[file]!.replace(/\n/g, '\r\n'), 'cpp'); + expect(named(result, 'class')).toEqual(classes[file]); + }); +}); diff --git a/__tests__/cpp-included-test-suite.test.ts b/__tests__/cpp-included-test-suite.test.ts new file mode 100644 index 0000000000..19efb98f0e --- /dev/null +++ b/__tests__/cpp-included-test-suite.test.ts @@ -0,0 +1,259 @@ +/** + * A C or C++ file is part of every translation unit that includes it, whatever + * its name says. protobuf's conformance framework lives in + * `conformance/conformance_test.h` / `.cc` (`ConformanceTestSuite`) and + * `conformance/test_runner.h`, named like test suites, and the suites and + * runners that use it include them: `suite_.ReportFailure(…)` in + * binary_json_conformance_suite.cc is `ConformanceTestSuite::ReportFailure`. + * The test-suite rule ("production code never names a test suite's symbol") + * dropped that edge. A test-named file no production file includes stays out + * of reach, and so does a test that only includes the header it tests. + */ +import { describe, it, expect, afterAll, beforeAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; + +let root = ''; +let cg: CodeGraph; + +beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-cpp-included-test-')); + const files: Record = { + 'conformance/test_runner.h': `#ifndef CONFORMANCE_TEST_RUNNER_H +#define CONFORMANCE_TEST_RUNNER_H + +#include + +namespace conformance { + +class ConformanceTestRunner { + public: + virtual ~ConformanceTestRunner() = default; + virtual void RunTest(const std::string& test_name, const std::string& input, std::string* output) = 0; +}; + +} // namespace conformance + +#endif +`, + 'conformance/conformance_test.h': `#ifndef CONFORMANCE_CONFORMANCE_TEST_H +#define CONFORMANCE_CONFORMANCE_TEST_H + +#include + +#include "conformance/test_runner.h" + +namespace conformance { + +class ConformanceTestSuite { + public: + virtual ~ConformanceTestSuite() = default; + bool RunSuite(ConformanceTestRunner* runner, std::string* output); + void ReportFailure(int test, int level); + + protected: + virtual void RunSuiteImpl() = 0; + int failures_ = 0; +}; + +int FailureCount(const ConformanceTestSuite& suite); + +} // namespace conformance + +#endif +`, + 'conformance/conformance_test.cc': `#include "conformance/conformance_test.h" + +namespace conformance { + +bool ConformanceTestSuite::RunSuite(ConformanceTestRunner* runner, std::string* output) { + RunSuiteImpl(); + return failures_ == 0; +} + +void ConformanceTestSuite::ReportFailure(int test, int level) { + failures_ += test + level; +} + +int FailureCount(const ConformanceTestSuite& suite) { + return 0; +} + +} // namespace conformance +`, + // Another class's ReportFailure: where the call went by guess before. + 'conformance/fork_pipe_runner.h': `#include + +#include "conformance/test_runner.h" + +namespace conformance { + +class ForkPipeRunner : public ConformanceTestRunner { + public: + void RunTest(const std::string& test_name, const std::string& input, std::string* output) override; + + private: + std::string ReportFailure(bool timed_out, const std::string& what_failed); +}; + +} // namespace conformance +`, + 'conformance/fork_pipe_runner.cc': `#include "conformance/fork_pipe_runner.h" + +namespace conformance { + +void ForkPipeRunner::RunTest(const std::string& test_name, const std::string& input, std::string* output) { + *output = ReportFailure(false, test_name); +} + +std::string ForkPipeRunner::ReportFailure(bool timed_out, const std::string& what_failed) { + return what_failed; +} + +} // namespace conformance +`, + 'conformance/binary_json_conformance_suite.h': `#include "conformance/conformance_test.h" + +namespace conformance { + +class BinaryAndJsonConformanceSuite : public ConformanceTestSuite { + private: + void RunSuiteImpl() override; +}; + +class BinaryAndJsonConformanceSuiteImpl { + public: + explicit BinaryAndJsonConformanceSuiteImpl(BinaryAndJsonConformanceSuite* suite); + void RunValidBinaryInputTest(int test); + + private: + BinaryAndJsonConformanceSuite& suite_; +}; + +} // namespace conformance +`, + 'conformance/binary_json_conformance_suite.cc': `#include "conformance/binary_json_conformance_suite.h" + +namespace conformance { + +void BinaryAndJsonConformanceSuite::RunSuiteImpl() { + BinaryAndJsonConformanceSuiteImpl impl(this); + impl.RunValidBinaryInputTest(1); +} + +void BinaryAndJsonConformanceSuiteImpl::RunValidBinaryInputTest(int test) { + suite_.ReportFailure(test, 0); +} + +} // namespace conformance +`, + 'conformance/conformance_test_main.cc': `#include "conformance/binary_json_conformance_suite.h" +#include "conformance/conformance_test.h" + +int RunConformance(const conformance::BinaryAndJsonConformanceSuite& suite) { + return FailureCount(suite); +} +`, + // Production code that does not include conformance_test.h. + 'tools/report.cc': `int Summarize(int suite) { + return FailureCount(suite); +} +`, + // A C test-named file a production file includes (redis' expr.c includes + // fastjson_test.c for its test build). + 'modules/fastjson_test.c': `int run_fastjson_test(void) { + return 0; +} +`, + 'modules/expr.c': `#include "fastjson_test.c" + +int expr_selftest(void) { + return run_fastjson_test(); +} +`, + // A real test suite nothing includes: its helper is not production's. + 'src/db.h': `int open_db(void); +`, + 'src/db.c': `#include "db.h" + +int open_db(void) { + return make_options(); +} +`, + 'src/db_test.c': `#include "db.h" + +int make_options(void) { + return 1; +} +`, + // A test that includes the header it tests, named like it, does not + // implement it: jemalloc's test/unit/hash.c and its hash.h. + 'include/hash.h': `unsigned hash(const char* key); +`, + 'src/hash.c': `#include "hash.h" + +unsigned hash(const char* key) { + return hash_selftest(); +} +`, + 'test/unit/hash.c': `#include "hash.h" + +unsigned hash_selftest(void) { + return hash("abc"); +} +`, + }; + 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 }); +}); + +afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); +}); + +/** `qualifiedName (file)` of every call the file's symbols make. */ +const callsFrom = (file: string): string[] => { + const ids = cg.getNodesInFile(file).map((n) => n.id); + return cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'calls').map((e) => { + const target = cg.getNode(e.target)!; + return `${target.qualifiedName} (${target.filePath})`; + }); +}; + +describe('a C/C++ file named like a test that the program includes', () => { + it('links a call through the base class its suite derives from', () => { + const calls = callsFrom('conformance/binary_json_conformance_suite.cc'); + expect(calls).toContain('conformance::ConformanceTestSuite::ReportFailure (conformance/conformance_test.cc)'); + expect(calls).not.toContain('conformance::ForkPipeRunner::ReportFailure (conformance/fork_pipe_runner.cc)'); + }); + + it('links a free function the included header declares', () => { + expect(callsFrom('conformance/conformance_test_main.cc')).toContain('conformance::FailureCount (conformance/conformance_test.cc)'); + }); + + it('links a test-named source file that a production file includes', () => { + expect(callsFrom('modules/expr.c')).toContain('run_fastjson_test (modules/fastjson_test.c)'); + }); + + it('keeps it out of reach of a file that does not include it', () => { + expect(callsFrom('tools/report.cc').some((c) => c.includes('conformance_test.cc'))).toBe(false); + }); +}); + +describe('a C/C++ test suite nothing in the program includes', () => { + it('is still out of reach of production code', () => { + expect(callsFrom('src/db.c').some((c) => c.includes('src/db_test.c'))).toBe(false); + }); + + it('does not implement the production header it includes to test', () => { + expect(callsFrom('src/hash.c').some((c) => c.includes('test/unit/hash.c'))).toBe(false); + // The test still reaches the code it tests. + expect(callsFrom('test/unit/hash.c')).toContain('hash (src/hash.c)'); + }); +}); diff --git a/__tests__/cpp-library-type-receiver.test.ts b/__tests__/cpp-library-type-receiver.test.ts new file mode 100644 index 0000000000..19d82f6341 --- /dev/null +++ b/__tests__/cpp-library-type-receiver.test.ts @@ -0,0 +1,569 @@ +/** + * A C++ receiver declared as a type from outside the project with a lowercase + * name — `std::string`, `std::vector<…>`, `absl::flat_hash_set<…>` — calls + * that type's own member, not a project method. + * + * google/leveldb's `DBIter` declares `std::string saved_key_;` and calls + * `saved_key_.clear()`. Receiver inference read the type as `string`, which + * names no project class, and the check for a type from outside the project + * only caught capitalized names (`List`, `String`), so the call fell through + * to a guess by the method's name: `Slice::clear`, the one project `clear`. + * protocolbuffers/protobuf had about 900 such edges (`std::string proto; + * proto.append(…)` reached `LeftoverBuffer::append`). + * + * When the calling function or its class declares the receiver that way, a + * `.` call — or a `->` through a raw pointer, `std::string* out` — gets no + * project edge. A `->` on a smart pointer, iterator or optional still goes to + * the element type, and a call that contradicts its declaration (`.` on a + * pointer) keeps its guess, since that declaration is another variable's. + * Comments no longer pass for declarations in the scan: `// … non-null imm_` + * read as `imm_`'s declaration, with the type `null`. + */ +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-library-')); + 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'); + +/** leveldb's Slice and its namesakes elsewhere: every method a library type's call could be mistaken for. */ +const PROJECT = { + 'include/leveldb/slice.h': lines( + 'namespace leveldb {', + 'class Slice {', + ' public:', + ' const char* data() const { return data_; }', + ' unsigned size() const { return size_; }', + ' void clear() { size_ = 0; }', + ' const char* begin() const { return data_; }', + ' void remove_prefix(unsigned n) { size_ -= n; }', + ' private:', + ' const char* data_;', + ' unsigned size_;', + '};', + '} // namespace leveldb', + ), + 'util/buffers.h': lines( + 'namespace leveldb {', + 'class LeftoverBuffer {', + ' public:', + ' void append(const char* s, unsigned n) {}', + '};', + 'class Arena {', + ' public:', + ' void insert(int block) {}', + '};', + 'class Cache {', + ' public:', + ' void reset() {}', + '};', + 'class Iterator {', + ' public:', + ' void SeekToFirst() {}', + '};', + '} // namespace leveldb', + ), +}; + +describe('a C++ receiver declared as a library type with a lowercase name', () => { + it('reproduction: `std::string saved_key_; saved_key_.clear()` is not Slice::clear', async () => { + const cg = await indexed({ + ...PROJECT, + 'db/db_iter.cc': lines( + '#include ', + '#include "include/leveldb/slice.h"', + 'namespace leveldb {', + 'class DBIter {', + ' public:', + ' void Next();', + ' private:', + ' std::string saved_key_; // == current key when direction_==kReverse', + '};', + 'void DBIter::Next() {', + ' saved_key_.clear();', + '}', + '} // namespace leveldb', + ), + }); + try { + expect(calls(cg, 'leveldb::DBIter::Next')).toEqual([]); + expect(cg.getCallers(callable(cg, 'leveldb::Slice::clear').id).filter((r) => r.edge.kind === 'calls')).toEqual([]); + } finally { + cg.close(); + } + }); + + it('a local, a parameter, a raw pointer, nested template arguments and an abseil container', async () => { + const cg = await indexed({ + ...PROJECT, + 'db/encode.cc': lines( + '#include ', + '#include ', + '#include "absl/container/flat_hash_set.h"', + '#include "include/leveldb/slice.h"', + '#include "util/buffers.h"', + 'namespace leveldb {', + 'void Encode(std::string* dst, const Slice& value) {', + ' dst->clear();', + ' dst->append(value.data(), value.size());', + '}', + 'std::string Render(const std::vector>& files) {', + ' std::string proto;', + ' proto.append("x", 1);', + ' absl::flat_hash_set seen;', + ' seen.insert(1);', + ' const char* first = proto.data();', + ' return files.begin() == files.end() ? proto : std::string(first);', + '}', + '} // namespace leveldb', + ), + }); + try { + // `value` is a Slice; `dst`, `proto`, `seen` and `files` are not. + expect(calls(cg, 'leveldb::Encode')).toEqual(['leveldb::Slice::data', 'leveldb::Slice::size']); + expect(calls(cg, 'leveldb::Render')).toEqual([]); + } finally { + cg.close(); + } + }); + + it('a `std::` name beside a project alias of the same name elsewhere', async () => { + // facebook/rocksdb vendors googletest, whose `typedef ::std::string + // string;` in `testing::internal` is a project type named `string`. + const cg = await indexed({ + ...PROJECT, + 'third_party/gtest.h': lines( + '#include ', + 'namespace testing {', + 'namespace internal {', + 'typedef ::std::string string;', + '} // namespace internal', + '} // namespace testing', + ), + 'tools/trim.cc': lines( + '#include ', + '#include "include/leveldb/slice.h"', + 'void Trim(std::string& text) {', + ' text.clear();', + '}', + ), + }); + try { + expect(calls(cg, 'Trim')).toEqual([]); + } finally { + cg.close(); + } + }); + + it('a smart pointer: `.` is its own member, `->` reaches the element type', async () => { + const cg = await indexed({ + ...PROJECT, + 'db/scan.cc': lines( + '#include ', + '#include "util/buffers.h"', + 'namespace leveldb {', + 'void Scan(Iterator* raw) {', + ' std::unique_ptr iter(raw);', + ' iter->SeekToFirst();', + ' iter.reset();', + '}', + '} // namespace leveldb', + ), + }); + try { + // `iter.reset()` is unique_ptr's, not Cache::reset. + expect(calls(cg, 'leveldb::Scan')).toEqual(['leveldb::Iterator::SeekToFirst']); + } finally { + cg.close(); + } + }); + + it('unproven std trait and member types do not guess a project owner', async () => { + // facebook/rocksdb's compression.cc: `std::conditional_t dict` is a Slice when the flag is off. + const cg = await indexed({ + ...PROJECT, + 'util/compression.cc': lines( + '#include ', + '#include ', + '#include "include/leveldb/slice.h"', + 'namespace leveldb {', + 'template ', + 'unsigned DictSize(std::conditional_t dict) {', + ' return dict.size();', + '}', + 'void Drop(const std::vector& slices) {', + ' std::vector::value_type first = slices[0];', + ' first.remove_prefix(1);', + '}', + '} // namespace leveldb', + ), + }); + try { + expect(calls(cg, 'leveldb::DictSize')).toEqual([]); + expect(calls(cg, 'leveldb::Drop')).toEqual([]); + } finally { + cg.close(); + } + }); + + it('a project type with a lowercase name is still the project\'s', async () => { + const cg = await indexed({ + ...PROJECT, + 'util/arena.h': lines( + 'namespace util {', + 'class arena {', + ' public:', + ' void* allocate(unsigned n) { return 0; }', + '};', + '} // namespace util', + ), + 'util/use.cc': lines( + '#include "util/arena.h"', + 'void Use(util::arena& a) {', + ' a.allocate(8);', + '}', + ), + }); + try { + expect(calls(cg, 'Use')).toEqual(['util::arena::allocate']); + } finally { + cg.close(); + } + }); +}); + +describe('a declaration that is not the receiver\'s does not rule a guess out', () => { + it('a range-for variable, or an `auto` one, hides an earlier library-typed one of the same name', async () => { + const cg = await indexed({ + ...PROJECT, + 'db/shadow.cc': lines( + '#include ', + '#include ', + '#include "include/leveldb/slice.h"', + '#include "util/buffers.h"', + 'namespace leveldb {', + 'void Trim(const std::vector& slices) {', + ' std::string key;', + ' for (const Slice& key : slices) {', + ' key.remove_prefix(1);', + ' }', + '}', + 'void Flush(Cache* caches) {', + ' { std::string buf; buf.clear(); }', + ' auto buf = caches[0];', + ' buf.reset();', + '}', + '} // namespace leveldb', + ), + }); + try { + expect(calls(cg, 'leveldb::Trim')).toEqual(['leveldb::Slice::remove_prefix']); + // The closed block cannot type the new binding; its auto initializer is unproven. + expect(calls(cg, 'leveldb::Flush')).toEqual([]); + } finally { + cg.close(); + } + }); + + it('a call that contradicts the declaration it found: `.` on what reads as a pointer', async () => { + // As in fmtlib/fmt's bigint, where `carry = (upper * bigits_[i] …` reads + // as a declaration `upper* bigits_[…]`: the product here reads as + // `absl::kWeightScale* key[…]`, but `key.size()` is a `.` call. + const cg = await indexed({ + ...PROJECT, + 'util/weight.cc': lines( + '#include "include/leveldb/slice.h"', + 'namespace leveldb {', + 'unsigned Weight(const Slice& key) {', + ' unsigned w = absl::kWeightScale * key[0];', + ' return w + key.size();', + '}', + '} // namespace leveldb', + ), + }); + try { + expect(calls(cg, 'leveldb::Weight')).toEqual(['leveldb::Slice::size']); + } finally { + cg.close(); + } + }); + + it('a constant of a project class read as a type, and a scalar', async () => { + const cg = await indexed({ + ...PROJECT, + 'util/mask.cc': lines( + '#include ', + '#include "include/leveldb/slice.h"', + '#define SLICE_AT(var, list, i) const Slice& var = list[i]', + 'namespace leveldb {', + 'unsigned Mask(const Slice& key) {', + // reads like `Slice::kMask& key[…]` + ' unsigned m = Slice::kMask & key[0];', + ' return m + key.size();', + '}', + 'void Visit(const Slice* slices) {', + ' std::size_t key = 0;', + ' {', + // the macro declares the `key` the call is made on + ' SLICE_AT(key, slices, 0);', + ' key.remove_prefix(1);', + ' }', + '}', + '} // namespace leveldb', + ), + }); + try { + expect(calls(cg, 'leveldb::Mask')).toEqual(['leveldb::Slice::size']); + // Macro expansion supplies no native proof of a Slice binding. + expect(calls(cg, 'leveldb::Visit')).toEqual([]); + } finally { + cg.close(); + } + }); + + it('a member of another class in the file', async () => { + // google/leveldb's db/log_test.cc: StringDest's `std::string contents_` + // sits above StringSource, whose own `Slice contents_` comes after its methods. + const cg = await indexed({ + ...PROJECT, + 'db/log_test.cc': lines( + '#include ', + '#include "include/leveldb/slice.h"', + 'namespace leveldb {', + 'class StringDest {', + ' public:', + ' std::string contents_;', + '};', + 'class StringSource {', + ' public:', + ' void Skip(unsigned n) { contents_.remove_prefix(n); }', + ' private:', + ' Slice contents_;', + '};', + '} // namespace leveldb', + ), + }); + try { + expect(calls(cg, 'leveldb::StringSource::Skip')).toEqual(['leveldb::Slice::remove_prefix']); + } finally { + cg.close(); + } + }); + + it('the last line of a declaration that starts on an earlier one', async () => { + // facebook/rocksdb's BlobFileReader: `autovector>>& blob_reqs` starts the next. + const cg = await indexed({ + 'util/autovector.h': lines( + 'namespace rocksdb {', + 'template ', + 'class autovector {', + ' public:', + ' unsigned size() const { return 0; }', + '};', + '} // namespace rocksdb', + ), + 'db/blob/blob_file_reader.cc': lines( + '#include ', + '#include "util/autovector.h"', + 'namespace rocksdb {', + 'class BlobFileReader {', + ' public:', + ' unsigned MultiGetBlob(autovector>>& blob_reqs);', + '};', + 'unsigned BlobFileReader::MultiGetBlob(autovector>>& blob_reqs) {', + ' return blob_reqs.size();', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::BlobFileReader::MultiGetBlob')).toEqual(['rocksdb::autovector::size']); + } finally { + cg.close(); + } + }); + + it('a member of a class nested in the caller\'s', async () => { + // facebook/rocksdb's MultiScan: its nested MultiScanIterator declares a + // `scan_opts_` of its own, above MultiScan's. + const cg = await indexed({ + 'include/rocksdb/multi_scan.h': lines( + '#include ', + 'namespace rocksdb {', + 'struct ScanOptions {};', + 'class MultiScanArgs {', + ' public:', + ' int GetScanRanges() const { return 0; }', + '};', + 'class MultiScan {', + ' public:', + ' class MultiScanIterator {', + ' private:', + ' const std::vector& scan_opts_;', + ' };', + ' int begin() { return scan_opts_.GetScanRanges(); }', + ' private:', + ' const MultiScanArgs scan_opts_;', + '};', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::MultiScan::begin')).toEqual(['rocksdb::MultiScanArgs::GetScanRanges']); + } finally { + cg.close(); + } + }); +}); + +describe('comments in the declaration scan', () => { + it('a comment that mentions the receiver is not its declaration', async () => { + // google/leveldb's db_impl.cc: `// … since there is a non-null imm_` + // above `imm_->…` read as `imm_`'s declaration, with the type `null`. + const cg = await indexed({ + 'db/memtable.h': lines( + 'namespace leveldb {', + 'class MemTable {', + ' public:', + ' unsigned ApproximateMemoryUsage() { return 0; }', + '};', + 'class Arena {', + ' public:', + ' unsigned ApproximateMemoryUsage() { return 0; }', + '};', + '} // namespace leveldb', + ), + 'db/db_impl.h': lines( + '#include "db/memtable.h"', + 'namespace leveldb {', + 'class DBImpl {', + ' public:', + ' unsigned MemoryUsage();', + ' private:', + ' MemTable* imm_;', + '};', + '} // namespace leveldb', + ), + 'db/db_impl.cc': lines( + '#include "db/db_impl.h"', + 'namespace leveldb {', + 'unsigned DBImpl::MemoryUsage() {', + ' // Count the memtable being compacted, since there is a non-null imm_', + ' return imm_->ApproximateMemoryUsage();', + '}', + '} // namespace leveldb', + ), + }); + try { + expect(calls(cg, 'leveldb::DBImpl::MemoryUsage')).toEqual(['leveldb::MemTable::ApproximateMemoryUsage']); + } finally { + cg.close(); + } + }); + + it('a parameter after a leading `/* static */` is the caller\'s own', async () => { + const cg = await indexed({ + ...PROJECT, + 'util/status.cc': lines( + '#include ', + '#include "include/leveldb/slice.h"', + 'namespace leveldb {', + 'class Status {', + ' public:', + ' static bool Check(const std::string& msg);', + '};', + '/* static */ bool Status::Check(const std::string& msg) {', + ' return msg.begin() != nullptr;', + '}', + '} // namespace leveldb', + ), + }); + try { + expect(calls(cg, 'leveldb::Status::Check')).toEqual([]); + } finally { + cg.close(); + } + }); +}); + +describe('proved receiver types retain their written pointer operation', () => { + it('links project values and raw pointers while refusing mismatched operators and shadow bindings', async () => { + const cg = await indexed({ + ...PROJECT, + 'db/operators.cc': lines( + '#include "include/leveldb/slice.h"', + 'namespace leveldb {', + 'void Valid(Slice value, Slice* pointer) { value.size(); pointer->data(); }', + 'void Invalid(Slice value, Slice* pointer, Slice** many) {', + ' value->size(); pointer.size(); many->clear();', + '}', + 'void Function() { std::unique_ptr factory(UnknownType); factory->size(); }', + 'void Shadow(Slice value) {', + ' auto& [value, count] = Unknown();', + ' value.size();', + '}', + '}', + ), + }); + try { + expect(calls(cg, 'leveldb::Valid')).toEqual(['leveldb::Slice::data', 'leveldb::Slice::size']); + expect(calls(cg, 'leveldb::Invalid')).toEqual([]); + expect(calls(cg, 'leveldb::Shadow')).toEqual([]); + expect(calls(cg, 'leveldb::Function')).toEqual([]); + } finally { + cg.close(); + } + }); +}); + +it('an unproven parenthesized declaration shadows the outer receiver', async () => { + const cg = await indexed({ ...PROJECT, 'shadow.cc': lines( + '#include "include/leveldb/slice.h"', + 'struct Foo {};', + 'void Bad() {', + ' leveldb::Slice key;', + ' { std::unique_ptr key(Unknown); key.size(); }', + '}', + 'void Good() { leveldb::Slice key; key.size(); }', + ) }); + try { + expect(calls(cg, 'Bad')).toEqual([]); + expect(calls(cg, 'Good')).toEqual(['leveldb::Slice::size']); + } finally { cg.close(); } +}); diff --git a/__tests__/dead-code.test.ts b/__tests__/dead-code.test.ts index c0fd98f817..b87ad8a466 100644 --- a/__tests__/dead-code.test.ts +++ b/__tests__/dead-code.test.ts @@ -532,3 +532,337 @@ function reallyUnused() {} expect(names(report)).toContain('neverCalledMember'); }); }); + +describe('a framework that calls members by name', () => { + let root: string; + let graph: CodeGraph; + + beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-deadcode-by-name-')); + // Three components that write the interface out: together they are the + // index's own record of what Angular's `OnInit` carries. The third inherits + // `ngOnInit` rather than declaring it, and lacks the `refresh` the other + // two happen to share. + write( + root, + 'src/app/first.component.ts', + `import { Component, OnInit } from '@angular/core'; + +@Component({ selector: 'app-first', template: '' }) +export class FirstComponent implements OnInit { + ngOnInit(): void {} + refresh(): void {} +} +` + ); + write( + root, + 'src/app/second.component.ts', + `import { AfterViewInit, Component, OnInit } from '@angular/core'; + +@Component({ selector: 'app-second', template: '' }) +export class SecondComponent implements OnInit, AfterViewInit { + ngOnInit(): void {} + ngAfterViewInit(): void {} + reload(): void {} + refresh(): void {} +} +` + ); + write( + root, + 'src/app/base-page.ts', + `import { Directive } from '@angular/core'; + +@Directive() +export abstract class BasePage { + ngOnInit(): void { + void 0; + } +} +` + ); + write( + root, + 'src/app/third.component.ts', + `import { Component, OnInit } from '@angular/core'; +import { BasePage } from './base-page'; + +@Component({ selector: 'app-third', template: '' }) +export class ThirdComponent extends BasePage implements OnInit {} +` + ); + // Writes no `implements` clause. Angular calls `ngOnInit` all the same. + write( + root, + 'src/app/demo.component.ts', + `import { Component, HostListener } from '@angular/core'; + +@Component({ selector: 'app-demo', template: '' }) +export class DemoComponent { + ngOnInit(): void { + void 0; + } + + @HostListener('window:keyup', ['$event']) + keyEvent(event: KeyboardEvent): void { + void event; + } + + reload(): void { + void 0; + } + + refresh(): void { + void 0; + } + + unusedHelper(): number { + return 1; + } +} +` + ); + // No decorator, so no framework registers it: the name alone is no evidence. + write(root, 'src/app/plain.ts', `export class Plain {\n ngOnInit(): void {\n void 0;\n }\n}\n`); + write( + root, + 'src/api/cron.service.ts', + `import { Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { Cron } from '@nestjs/schedule'; + +@Injectable() +export class CronService { + @Cron('0 * * * *') + runEveryHour(): void { + void 0; + } + + @OnEvent('portfolio.changed') + handlePortfolioChanged(): void { + void 0; + } + + hasCurrencyPair(): boolean { + return false; + } +} +` + ); + write( + root, + 'src/main.ts', + `import { FirstComponent } from './app/first.component'; +import { SecondComponent } from './app/second.component'; +import { ThirdComponent } from './app/third.component'; +import { DemoComponent } from './app/demo.component'; +import { Plain } from './app/plain'; +import { CronService } from './api/cron.service'; + +export const declarations = [FirstComponent, SecondComponent, ThirdComponent, DemoComponent, Plain, CronService]; +` + ); + // Java checks the interface itself: Spring calls `afterPropertiesSet` on a + // bean that implements InitializingBean, never on one that only has the name. + for (const bean of ['FirstBean', 'SecondBean']) { + write( + root, + `src/main/java/demo/${bean}.java`, + `package demo; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Component; + +@Component +public class ${bean} implements InitializingBean { + public void afterPropertiesSet() {} +} +` + ); + } + write( + root, + 'src/main/java/demo/LooseBean.java', + `package demo; + +import org.springframework.stereotype.Component; + +@Component +public class LooseBean { + public void afterPropertiesSet() {} +} +` + ); + write( + root, + 'src/main/java/demo/App.java', + `package demo; + +public class App { + public static void main(String[] args) { + new FirstBean(); + new SecondBean(); + new LooseBean(); + } +} +` + ); + graph = CodeGraph.initSync(root, { + config: { include: ['src/**/*.ts', 'src/**/*.java'], exclude: [] }, + }); + await graph.indexAll(); + }, 60_000); + + afterAll(() => { + graph?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + const listed = (report: ReturnType, qualifiedName: string): boolean => + report.entries.some((entry) => entry.node.qualifiedName === qualifiedName); + + it('does not list a method whose decorator resolves outside the index', () => { + for (const readSource of [undefined, null] as const) { + const report = buildDeadCodeReport(graph, readSource === null ? { readSource } : {}); + expect(listed(report, 'DemoComponent::keyEvent')).toBe(false); + expect(listed(report, 'CronService::runEveryHour')).toBe(false); + expect(listed(report, 'CronService::handlePortfolioChanged')).toBe(false); + expect(report.excluded.decorated).toBeGreaterThanOrEqual(3); + } + }); + + it('does not list a hook its class fills by name, without writing the interface', () => { + for (const readSource of [undefined, null] as const) { + const report = buildDeadCodeReport(graph, readSource === null ? { readSource } : {}); + expect(listed(report, 'DemoComponent::ngOnInit')).toBe(false); + // The base a component inherits the hook from is reached the same way. + expect(listed(report, 'BasePage::ngOnInit')).toBe(false); + expect(report.excluded.hooks).toBe(2); + } + }); + + it('still lists what nothing reaches in a class a framework registers', () => { + const report = buildDeadCodeReport(graph); + // A decorator on the class is not evidence for each of its members. + expect(listed(report, 'DemoComponent::unusedHelper')).toBe(true); + expect(listed(report, 'CronService::hasCurrencyPair')).toBe(true); + // One implementer is not a contract: `reload` is only SecondComponent's. + expect(listed(report, 'DemoComponent::reload')).toBe(true); + // Two implementers sharing a method is no contract while a third lacks it. + expect(listed(report, 'DemoComponent::refresh')).toBe(true); + // Nothing registers an undecorated class. + expect(listed(report, 'Plain::ngOnInit')).toBe(true); + }); + + it('infers a contract only where the interface has no runtime effect', () => { + // Java has no export marker in the index, so ask for the exported list, + // where every other rule still runs. + const report = buildDeadCodeReport(graph, { includeExported: true }); + expect(listed(report, 'demo::LooseBean::afterPropertiesSet')).toBe(true); + expect(listed(report, 'DemoComponent::ngOnInit')).toBe(false); + }); +}); + +describe('a declaration that merges into a type outside the index', () => { + let root: string; + let graph: CodeGraph; + + beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-deadcode-ambient-')); + // ghostfolio's chart.registry.ts: chart.js reads both interfaces through + // the types they extend, and nothing in the repository names either. + write( + root, + 'src/chart.registry.ts', + `import { Chart, Tooltip, type ChartType } from 'chart.js'; + +interface VerticalHoverLinePluginOptions { + color?: string; +} + +declare module 'chart.js' { + interface PluginOptionsByType { + verticalHoverLine: TType extends 'line' ? VerticalHoverLinePluginOptions : never; + } + interface TooltipPositionerMap { + top: (items: unknown[]) => { x: number; y: number }; + } +} + +export function registerChartConfiguration(): void { + Chart.register(Tooltip); +} + +function unusedChartHelper(): void {} +` + ); + // angular-realworld's app.config.ts, with a Node.js augmentation beside it. + write( + root, + 'src/app.config.ts', + `declare global { + interface Window { + __conduit_debug__?: { token(): string | null }; + } + namespace NodeJS { + interface ProcessEnv { + API_URL?: string; + } + } +} + +// In a module file this namespace is the file's own: it merges with nothing. +declare namespace Settings { + interface NotMergedAnywhere { + retries: number; + } +} + +export const appConfig = { providers: [] }; +` + ); + write( + root, + 'src/main.ts', + `import { registerChartConfiguration } from './chart.registry'; +import { appConfig } from './app.config'; + +registerChartConfiguration(); +export const config = appConfig; +` + ); + graph = CodeGraph.initSync(root, { config: { include: ['src/**/*.ts'], exclude: [] } }); + await graph.indexAll(); + }, 60_000); + + afterAll(() => { + graph?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + const merged = ['PluginOptionsByType', 'TooltipPositionerMap', 'Window', 'ProcessEnv']; + + it('does not list what a declare module or declare global block declares', () => { + for (const readSource of [undefined, null] as const) { + const report = buildDeadCodeReport(graph, readSource === null ? { readSource } : {}); + for (const name of merged) expect(names(report)).not.toContain(name); + // TypeScript exports them without the keyword, so the exported rule + // takes them, and counts them. + expect(report.excluded.exported).toBe(merged.length); + } + }); + + it('lists them with the outside-reach caveat when exported symbols are asked for', () => { + const report = buildDeadCodeReport(graph, { includeExported: true }); + for (const name of merged) { + expect(report.entries.find((entry) => entry.node.name === name)?.exported).toBe(true); + } + }); + + it('still lists what nothing reaches beside them', () => { + const report = buildDeadCodeReport(graph); + expect(names(report)).toContain('unusedChartHelper'); + expect(names(report)).toContain('NotMergedAnywhere'); + }); +}); diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index 909f857640..28f4ff150a 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -8083,10 +8083,10 @@ describe('Liquid Shopify JSON template section resolution', () => { expect(detectLanguage('shop/templates/product.json', undefined, { '.json': 'yaml' }, tempDir)).toBe('liquid'); }); - const writeFiles = (files: Record): void => { + const writeFiles = (files: Record, root = tempDir): void => { for (const [file, text] of Object.entries(files)) { - fs.mkdirSync(path.dirname(path.join(tempDir, file)), { recursive: true }); - fs.writeFileSync(path.join(tempDir, file), text); + fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); + fs.writeFileSync(path.join(root, file), text); } }; // A theme at the project root, a theme in a folder marked only by its settings @@ -8148,6 +8148,67 @@ describe('Liquid Shopify JSON template section resolution', () => { expect(cg.getFileDependencies('examples/tailwind/templates/404.json')).toEqual([]); }); + it('links a section or snippet that appears after the files naming it', async () => { + // Sync retries a reference it could not resolve once a file it may name + // appears. A section or snippet reference was parked under its extension, + // `liquid`, which no file is named, so it stayed unlinked until the file + // naming it changed or the project was indexed again. + const later: Record = { + 'sections/404.liquid': `

404

\n`, + 'snippets/price.liquid': severalThemes['snippets/price.liquid'], + // Only straps/dusk had a 404 section, which is no stand-in for this theme's own. + 'examples/tailwind/sections/404.liquid': `

404

\n`, + 'extensions/reviews/snippets/star.liquid': severalThemes['extensions/reviews/snippets/star.liquid'], + }; + const initial = Object.fromEntries(Object.entries(severalThemes).filter(([file]) => !(file in later))); + writeFiles({ ...initial, 'templates/404.json': jsonTemplate('404') }); + cg = CodeGraph.initSync(tempDir); + await cg.indexAll(); + const naming = ['templates/404.json', 'sections/header.liquid', 'examples/tailwind/templates/404.json', + 'examples/tailwind/sections/header.liquid', 'extensions/reviews/blocks/stars.liquid']; + const links = (graph = cg) => Object.fromEntries(naming.map((file) => [file, graph.getFileDependencies(file).sort()])); + expect(links()).toEqual({ + 'templates/404.json': [], + 'sections/header.liquid': [], + 'examples/tailwind/templates/404.json': [], + 'examples/tailwind/sections/header.liquid': ['examples/tailwind/snippets/icon.liquid'], + 'extensions/reviews/blocks/stars.liquid': [], + }); + + writeFiles(later); + expect((await cg.sync()).filesAdded).toBe(4); + const synced = links(); + expect(synced).toEqual({ + 'templates/404.json': ['sections/404.liquid'], + 'sections/header.liquid': ['snippets/price.liquid'], + 'examples/tailwind/templates/404.json': ['examples/tailwind/sections/404.liquid'], + // The root theme's new price snippet is not this theme's. + 'examples/tailwind/sections/header.liquid': ['examples/tailwind/snippets/icon.liquid'], + 'extensions/reviews/blocks/stars.liquid': ['extensions/reviews/snippets/star.liquid'], + }); + expect(cg.getPendingReferenceCount()).toBe(0); + // A fresh index of the same files links the same. (Indexing again over + // this index would skip the unchanged files that name the new ones.) + const freshDir = createTempDir(); + writeFiles({ ...severalThemes, 'templates/404.json': jsonTemplate('404'), ...later }, freshDir); + const fresh = CodeGraph.initSync(freshDir); + try { + await fresh.indexAll(); + expect(links(fresh)).toEqual(synced); + } finally { + fresh.close(); + fs.rmSync(freshDir, { recursive: true, force: true }); + } + + // A deleted snippet's references wait for it the same way. + fs.rmSync(path.join(tempDir, 'snippets/price.liquid')); + await cg.sync(); + expect(cg.getFileDependencies('sections/header.liquid')).toEqual([]); + writeFiles({ 'snippets/price.liquid': later['snippets/price.liquid']! }); + await cg.sync(); + expect(cg.getFileDependencies('sections/header.liquid')).toEqual(['snippets/price.liquid']); + }, 60_000); + it('finds the theme a file is in from the theme folder it sits in', () => { const markers = new Set(['layout/theme.liquid', 'examples/tailwind/config/settings_schema.json']); const exists = (relativePath: string): boolean => markers.has(relativePath); diff --git a/__tests__/fixtures/golden/markdown-docs.dump b/__tests__/fixtures/golden/markdown-docs.dump index a84c0bd944..e61ff42fb7 100644 --- a/__tests__/fixtures/golden/markdown-docs.dump +++ b/__tests__/fixtures/golden/markdown-docs.dump @@ -76,7 +76,7 @@ == refs (3) {"from_node_id":"file:src/widget.test.ts","reference_name":"test","reference_kind":"calls","line":2,"col":0,"candidates":null,"file_path":"src/widget.test.ts","language":"typescript","status":"failed","name_tail":"test"} {"from_node_id":"function:e78917dda989e6051383586a18681419","reference_name":"console.log","reference_kind":"calls","line":11,"col":2,"candidates":null,"file_path":"src/widget.ts","language":"typescript","status":"failed","name_tail":"log","failure_reason":"unknown-receiver"} -{"from_node_id":"module:9f3c3ccfb61608be0c4ec12471d12a83","reference_name":"src/widget.test.ts","reference_kind":"references","line":7,"col":50,"candidates":null,"file_path":"CONTRIBUTING.md","language":"markdown","status":"failed","name_tail":"ts"} +{"from_node_id":"module:9f3c3ccfb61608be0c4ec12471d12a83","reference_name":"src/widget.test.ts","reference_kind":"references","line":7,"col":50,"candidates":null,"file_path":"CONTRIBUTING.md","language":"markdown","status":"failed","name_tail":"widget.test.ts"} == bindings (11) {"file_path":"src/widget.test.ts","name":"createWidget","kind":"import","node_id":null,"target_spec":"./widget","target_name":"createWidget","exported_as":null,"export_form":null,"scope_start":1,"scope_end":3,"storage":null,"line":1} {"file_path":"src/widget.test.ts","name":"render","kind":"import","node_id":null,"target_spec":"./widget","target_name":"render","exported_as":null,"export_form":null,"scope_start":1,"scope_end":3,"storage":null,"line":1} diff --git a/__tests__/fixtures/golden/payroll-go.dump b/__tests__/fixtures/golden/payroll-go.dump index 488f10b2c5..523cea6294 100644 --- a/__tests__/fixtures/golden/payroll-go.dump +++ b/__tests__/fixtures/golden/payroll-go.dump @@ -937,7 +937,7 @@ {"source":"variable:02a9b4868be4f999189e4d930af29b95","target":"interface:9382b4b7c3b120f81bd0566fb2d74864","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayrollServiceServer\"}","line":97,"col":14,"provenance":null} {"source":"variable:9c3a0c9681a69504be5d3af6ddb5378c","target":"struct:3d4fd5cb833f90205ff1d82443698813","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayrollCycle\",\"refKind\":\"calls\"}","line":136,"col":1,"provenance":null} {"source":"variable:9c3a0c9681a69504be5d3af6ddb5378c","target":"struct:4325013490f7f7a77604146f5e38c292","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"RunPayrollCycleRequest\",\"refKind\":\"calls\"}","line":133,"col":1,"provenance":null} -{"source":"variable:9c3a0c9681a69504be5d3af6ddb5378c","target":"struct:d541b867b240edf388feab5ff2002afd","kind":"instantiates","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Payslip\",\"refKind\":\"calls\"}","line":135,"col":1,"provenance":null} +{"source":"variable:9c3a0c9681a69504be5d3af6ddb5378c","target":"struct:d541b867b240edf388feab5ff2002afd","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Payslip\",\"refKind\":\"calls\"}","line":135,"col":1,"provenance":null} {"source":"variable:9c3a0c9681a69504be5d3af6ddb5378c","target":"struct:e66f442bcf28c3bd0a3f42fbacb69b2a","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"RunPayrollCycleResponse\",\"refKind\":\"calls\"}","line":134,"col":1,"provenance":null} == refs (531) {"from_node_id":"file:cmd/payrolld/main.go","reference_name":"github.com/example/payroll-svc/internal/platform/clock","reference_kind":"imports","line":9,"col":1,"candidates":null,"file_path":"cmd/payrolld/main.go","language":"go","status":"failed","name_tail":"clock"} @@ -1459,15 +1459,15 @@ {"from_node_id":"method:fd95c33dd58bbddc5221ee4ced0bbcd3","reference_name":"len","reference_kind":"calls","line":69,"col":4,"candidates":null,"file_path":"internal/usecase/payroll/cycle.go","language":"go","status":"failed","name_tail":"len"} {"from_node_id":"method:ff7a7eea8969534c02484b666f0ef39b","reference_name":"protoimpl.X.MessageStringOf","reference_kind":"calls","line":125,"col":48,"candidates":null,"file_path":"internal/gen/payrollpb/payroll.pb.go","language":"go","status":"failed","name_tail":"MessageStringOf","failure_reason":"unknown-receiver"} {"from_node_id":"module:39730d6601ee76ddcd5e78d50abfe5a2","reference_name":".pb.go","reference_kind":"references","line":41,"col":15,"candidates":null,"file_path":"README.md","language":"markdown","status":"failed","name_tail":"go"} -{"from_node_id":"module:39730d6601ee76ddcd5e78d50abfe5a2","reference_name":"__tests__/explore-allocation-1500.test.ts","reference_kind":"references","line":52,"col":15,"candidates":null,"file_path":"README.md","language":"markdown","status":"failed","name_tail":"ts"} +{"from_node_id":"module:39730d6601ee76ddcd5e78d50abfe5a2","reference_name":"__tests__/explore-allocation-1500.test.ts","reference_kind":"references","line":52,"col":15,"candidates":null,"file_path":"README.md","language":"markdown","status":"failed","name_tail":"explore-allocation-1500.test.ts"} {"from_node_id":"module:39730d6601ee76ddcd5e78d50abfe5a2","reference_name":"cycle.go","reference_kind":"references","line":49,"col":50,"candidates":null,"file_path":"README.md","language":"markdown","status":"failed","name_tail":"go"} {"from_node_id":"module:39730d6601ee76ddcd5e78d50abfe5a2","reference_name":"payslip.go","reference_kind":"references","line":38,"col":31,"candidates":null,"file_path":"README.md","language":"markdown","status":"failed","name_tail":"go"} {"from_node_id":"module:39730d6601ee76ddcd5e78d50abfe5a2","reference_name":"store.go","reference_kind":"references","line":38,"col":45,"candidates":null,"file_path":"README.md","language":"markdown","status":"failed","name_tail":"go"} {"from_node_id":"module:900ad55709598429050c36805696291a","reference_name":"cycle.go","reference_kind":"references","line":66,"col":1,"candidates":null,"file_path":"README.md","language":"markdown","status":"failed","name_tail":"go"} {"from_node_id":"module:900ad55709598429050c36805696291a","reference_name":"payslip_builder.go","reference_kind":"references","line":67,"col":54,"candidates":null,"file_path":"README.md","language":"markdown","status":"failed","name_tail":"go"} -{"from_node_id":"module:a883d05f3bb8edb262fd18c8c2849417","reference_name":"docs/design/explore-budget-allocation.md","reference_kind":"references","line":82,"col":1,"candidates":null,"file_path":"README.md","language":"markdown","status":"failed","name_tail":"md"} -{"from_node_id":"module:a883d05f3bb8edb262fd18c8c2849417","reference_name":"scripts/agent-eval/allocation-fixtures.js","reference_kind":"references","line":79,"col":49,"candidates":null,"file_path":"README.md","language":"markdown","status":"failed","name_tail":"js"} -{"from_node_id":"module:f8d63ed69e573ade27e2d1d4fc0ae8b4","reference_name":"internal/gen/fkit/payroll/store.go","reference_kind":"references","line":87,"col":56,"candidates":null,"file_path":"README.md","language":"markdown","status":"failed","name_tail":"go"} +{"from_node_id":"module:a883d05f3bb8edb262fd18c8c2849417","reference_name":"docs/design/explore-budget-allocation.md","reference_kind":"references","line":82,"col":1,"candidates":null,"file_path":"README.md","language":"markdown","status":"failed","name_tail":"explore-budget-allocation.md"} +{"from_node_id":"module:a883d05f3bb8edb262fd18c8c2849417","reference_name":"scripts/agent-eval/allocation-fixtures.js","reference_kind":"references","line":79,"col":49,"candidates":null,"file_path":"README.md","language":"markdown","status":"failed","name_tail":"allocation-fixtures.js"} +{"from_node_id":"module:f8d63ed69e573ade27e2d1d4fc0ae8b4","reference_name":"internal/gen/fkit/payroll/store.go","reference_kind":"references","line":87,"col":56,"candidates":null,"file_path":"README.md","language":"markdown","status":"failed","name_tail":"store.go"} {"from_node_id":"variable:02a9b4868be4f999189e4d930af29b95","reference_name":"grpc.ServiceDesc","reference_kind":"instantiates","line":95,"col":33,"candidates":null,"file_path":"internal/gen/payrollpb/payroll_grpc.pb.go","language":"go","status":"failed","name_tail":"ServiceDesc"} {"from_node_id":"variable:6b7bef84822056845bdabeb41a821f0a","reference_name":"errors.New","reference_kind":"calls","line":16,"col":21,"candidates":null,"file_path":"internal/usecase/payroll/cycle.go","language":"go","status":"failed","name_tail":"New","failure_reason":"unknown-receiver"} {"from_node_id":"variable:baa4aee814b380ae99f1b1829407b106","reference_name":"errors.New","reference_kind":"calls","line":19,"col":21,"candidates":null,"file_path":"internal/usecase/payroll/cycle.go","language":"go","status":"failed","name_tail":"New","failure_reason":"unknown-receiver"} diff --git a/__tests__/fixtures/golden/torture-multilang.dump b/__tests__/fixtures/golden/torture-multilang.dump index f09433d52c..2f44d9da02 100644 --- a/__tests__/fixtures/golden/torture-multilang.dump +++ b/__tests__/fixtures/golden/torture-multilang.dump @@ -1,4 +1,4 @@ -== nodes (1784) +== nodes (1795) {"id":"class:0083dc18baa2b629e8572db20ba2b464","kind":"class","name":"Widget","qualified_name":"Widget","file_path":"torture.cpp","language":"cpp","start_line":98,"end_line":104,"start_column":0,"end_column":1,"docstring":"UE-macro shapes — every one below is recovered by the hoisted preParse\n(export macro, reflection markup, inline specifier, API member prefix).","signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"class:00e4c67aff13a714886744f6829e0cea","kind":"class","name":"Cfg","qualified_name":"Cfg","file_path":"TortureDoubleWalk.dart","language":"dart","start_line":60,"end_line":62,"start_column":0,"end_column":1,"docstring":null,"signature":null,"visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"class:01a3945fa12793f1af9ac531aedcc8cd","kind":"class","name":"Widget","qualified_name":"Widget","file_path":"TortureMini.dart","language":"dart","start_line":34,"end_line":52,"start_column":0,"end_column":1,"docstring":null,"signature":null,"visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} @@ -587,7 +587,7 @@ {"id":"file:torture.c","kind":"file","name":"torture.c","qualified_name":"torture.c","file_path":"torture.c","language":"c","start_line":1,"end_line":184,"start_column":0,"end_column":0,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"file:torture.cpp","kind":"file","name":"torture.cpp","qualified_name":"torture.cpp","file_path":"torture.cpp","language":"cpp","start_line":1,"end_line":156,"start_column":0,"end_column":0,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"file:torture.dart","kind":"file","name":"torture.dart","qualified_name":"torture.dart","file_path":"torture.dart","language":"dart","start_line":1,"end_line":247,"start_column":0,"end_column":0,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} -{"id":"file:torture.go","kind":"file","name":"torture.go","qualified_name":"torture.go","file_path":"torture.go","language":"go","start_line":1,"end_line":90,"start_column":0,"end_column":0,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} +{"id":"file:torture.go","kind":"file","name":"torture.go","qualified_name":"torture.go","file_path":"torture.go","language":"go","start_line":1,"end_line":116,"start_column":0,"end_column":0,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"file:torture.hpp","kind":"file","name":"torture.hpp","qualified_name":"torture.hpp","file_path":"torture.hpp","language":"cpp","start_line":1,"end_line":32,"start_column":0,"end_column":0,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"file:torture.js","kind":"file","name":"torture.js","qualified_name":"torture.js","file_path":"torture.js","language":"javascript","start_line":1,"end_line":111,"start_column":0,"end_column":0,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"file:torture.kt","kind":"file","name":"torture.kt","qualified_name":"torture.kt","file_path":"torture.kt","language":"kotlin","start_line":1,"end_line":307,"start_column":0,"end_column":0,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} @@ -603,7 +603,6 @@ {"id":"function:004e9b066e8a0c7377d6fbe8ed9fa046","kind":"function","name":"weird_singleton","qualified_name":"weird_singleton","file_path":"torture.rb","language":"ruby","start_line":232,"end_line":232,"start_column":0,"end_column":28,"docstring":null,"signature":null,"visibility":"private","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:00765ec56fe71745c9d59a36a20c8d9e","kind":"function","name":"NestedHandlers","qualified_name":"NestedHandlers","file_path":"torture.tsx","language":"tsx","start_line":245,"end_line":252,"start_column":7,"end_column":1,"docstring":"- const-bound functions inside a body (#1669) -----------------------------","signature":"({ items, onPick }: { items: string[]; onPick: (a: unknown, b: unknown) => void })","visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:00b2d8219547c7844062cc5b8250525f","kind":"function","name":"multiRet","qualified_name":"multiRet","file_path":"torture.luau","language":"luau","start_line":22,"end_line":24,"start_column":0,"end_column":3,"docstring":null,"signature":"(): (number, string)","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} -{"id":"function:013bc08b4895f4830b19ced189245d75","kind":"function","name":"TargetCb","qualified_name":"TargetCb","file_path":"torture.go","language":"go","start_line":65,"end_line":65,"start_column":0,"end_column":23,"docstring":null,"signature":"(n int)","visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:014e5ac4509fb26b0b2a1c00f6183b50","kind":"function","name":"genericDef","qualified_name":"genericDef","file_path":"torture.scala","language":"scala","start_line":18,"end_line":18,"start_column":0,"end_column":54,"docstring":null,"signature":"[A: Numeric, B <: BoundT]: B","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"B"} {"id":"function:01bcb742f299ec5ccc188b8a5f1937b5","kind":"function","name":"create","qualified_name":"actionsMap::create","file_path":"torture.tsx","language":"tsx","start_line":146,"end_line":148,"start_column":10,"end_column":3,"docstring":null,"signature":"(input: CreateInput)","visibility":null,"is_exported":1,"is_async":1,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:0499cc5aef87518260d547d716f829ce","kind":"function","name":"getWidget","qualified_name":"getWidget","file_path":"torture.tsx","language":"tsx","start_line":173,"end_line":173,"start_column":13,"end_column":44,"docstring":null,"signature":"(id: string)","visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} @@ -612,7 +611,6 @@ {"id":"function:05bd0201aeabe1c81ea0141ab10b271b","kind":"function","name":"computed_string","qualified_name":"computed_string","file_path":"docstrings.py","language":"python","start_line":74,"end_line":76,"start_column":0,"end_column":12,"docstring":null,"signature":"()","visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:071bd15751854933bac820b5850a2641","kind":"function","name":"<+>","qualified_name":"<+>","file_path":"torture.swift","language":"swift","start_line":131,"end_line":131,"start_column":0,"end_column":56,"docstring":null,"signature":null,"visibility":"internal","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:077abb142cd38f50fd7aeb4958ac54b7","kind":"function","name":"register","qualified_name":"register","file_path":"TortureInitializers.dart","language":"dart","start_line":7,"end_line":7,"start_column":0,"end_column":23,"docstring":null,"signature":"void (Object o)","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} -{"id":"function:081d63a8e9c39d959398cdaa85b2188d","kind":"function","name":"mdLoad","qualified_name":"mdLoad","file_path":"torture.go","language":"go","start_line":87,"end_line":89,"start_column":0,"end_column":1,"docstring":null,"signature":"()","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:0895c62bceeb532232ed40a5b616db73","kind":"function","name":"km","qualified_name":"Meters::km","file_path":"TortureDeclarationDocs.dart","language":"dart","start_line":97,"end_line":97,"start_column":11,"end_column":22,"docstring":"Extension type external doc.","signature":"double ()","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"double"} {"id":"function:08bb60eead5364c3bf26d67ebca2639d","kind":"function","name":"lambdas","qualified_name":"lambdas","file_path":"TortureSigs.dart","language":"dart","start_line":21,"end_line":28,"start_column":0,"end_column":26,"docstring":null,"signature":"void (List xs)","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:09bc8c99cfea997f2b613b053cdeacc9","kind":"function","name":"onSave","qualified_name":"handlerMap::onSave","file_path":"torture.js","language":"javascript","start_line":80,"end_line":80,"start_column":29,"end_column":55,"docstring":null,"signature":"()","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} @@ -683,6 +681,7 @@ {"id":"function:349630601c52f479b2e4cb36d8f4e83f","kind":"function","name":"bodiless","qualified_name":"C::bodiless","file_path":"TortureAnnotatedComments.dart","language":"dart","start_line":74,"end_line":74,"start_column":2,"end_column":17,"docstring":"A comment.","signature":"void ()","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:37c390abd50c0167008655a9d88fe073","kind":"function","name":"inner","qualified_name":"render::inner","file_path":"torture.lua","language":"lua","start_line":49,"end_line":51,"start_column":1,"end_column":4,"docstring":null,"signature":"()","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:3837aa2b3854571c305f1f95df883277","kind":"function","name":"eq_fn","qualified_name":"eq_fn","file_path":"torture.R","language":"r","start_line":14,"end_line":14,"start_column":0,"end_column":25,"docstring":null,"signature":"(x)","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} +{"id":"function:38bd0a279764bd368f0732f5abecc4f8","kind":"function","name":"useAlias","qualified_name":"useAlias","file_path":"torture.go","language":"go","start_line":70,"end_line":72,"start_column":0,"end_column":1,"docstring":null,"signature":"(a *Alias) LocalAlias","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"LocalAlias"} {"id":"function:38d7bae04a29fa8be9d0997ef3ccb42e","kind":"function","name":"freeReader","qualified_name":"freeReader","file_path":"TortureVrefDart.dart","language":"dart","start_line":22,"end_line":24,"start_column":0,"end_column":17,"docstring":null,"signature":"void ()","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:39329e3ed919e959c06a8c0eb791ad7f","kind":"function","name":"defaults","qualified_name":"com.example.torture::defaults","file_path":"torture.kt","language":"kotlin","start_line":247,"end_line":247,"start_column":0,"end_column":52,"docstring":null,"signature":"(a: Int = compute(), b: String = \"x\")","visibility":"public","is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:3b5974da24c560449effd9d014e81123","kind":"function","name":"optional","qualified_name":"optional","file_path":"TortureSigs.dart","language":"dart","start_line":6,"end_line":6,"start_column":0,"end_column":37,"docstring":null,"signature":"void ([int b = 0, Widget? w])","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} @@ -702,7 +701,6 @@ {"id":"function:452af9ab50e94846870ef417047aa76b","kind":"function","name":"useUpdateWidgetMutation","qualified_name":"useUpdateWidgetMutation","file_path":"torture.tsx","language":"tsx","start_line":184,"end_line":184,"start_column":34,"end_column":57,"docstring":null,"signature":"= RTK Query generated hook","visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:4660fc91e385ee68f62141cd432fb335","kind":"function","name":"uninitShadow","qualified_name":"uninitShadow","file_path":"TortureVrefDart.dart","language":"dart","start_line":26,"end_line":30,"start_column":0,"end_column":19,"docstring":null,"signature":"void ()","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:466dea0452e4cb4198514e9b7cc2907f","kind":"function","name":"afterVariable","qualified_name":"afterVariable","file_path":"TortureAnnotatedComments.dart","language":"dart","start_line":59,"end_line":59,"start_column":0,"end_column":20,"docstring":"A comment.","signature":"void ()","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} -{"id":"function:468f41958dcbb0ea1e0fd73b3eafab10","kind":"function","name":"reads","qualified_name":"reads","file_path":"torture.go","language":"go","start_line":72,"end_line":74,"start_column":0,"end_column":1,"docstring":null,"signature":"() int","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"int"} {"id":"function:4759298ab8dfe6cd0b2c0574d59fdde3","kind":"function","name":"top_singleton","qualified_name":"top_singleton","file_path":"torture.rb","language":"ruby","start_line":228,"end_line":230,"start_column":0,"end_column":3,"docstring":null,"signature":null,"visibility":"private","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:497300b7c8854deb5abe80f7446e4447","kind":"function","name":"top","qualified_name":"top","file_path":"TortureAnnotatedDocs.dart","language":"dart","start_line":12,"end_line":12,"start_column":0,"end_column":10,"docstring":"Top function doc.","signature":"void ()","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:49b34d67e6038f90631cab938e6ba5e8","kind":"function","name":"generic","qualified_name":"generic","file_path":"torture.dart","language":"dart","start_line":52,"end_line":52,"start_column":0,"end_column":17,"docstring":null,"signature":"T (T v)","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"T"} @@ -828,6 +826,7 @@ {"id":"function:9d0b5cf639ecbd60bfec25961cf971c9","kind":"function","name":"fnRefExtras","qualified_name":"fnRefExtras","file_path":"torture.swift","language":"swift","start_line":205,"end_line":210,"start_column":0,"end_column":1,"docstring":null,"signature":null,"visibility":"internal","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:9e3562bbb17ae055c07f19c4cf7a017f","kind":"function","name":"mdLoad","qualified_name":"mdLoad","file_path":"torture.swift","language":"swift","start_line":226,"end_line":228,"start_column":0,"end_column":1,"docstring":null,"signature":null,"visibility":"internal","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:9e4b2ec4bd1b887f3b4e4bd8bcf6b222","kind":"function","name":"nested_string","qualified_name":"nested_string","file_path":"docstrings.py","language":"python","start_line":95,"end_line":98,"start_column":0,"end_column":12,"docstring":null,"signature":"()","visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} +{"id":"function:9f483e5d42c66f7b48795991a0de7f31","kind":"function","name":"reads","qualified_name":"reads","file_path":"torture.go","language":"go","start_line":98,"end_line":100,"start_column":0,"end_column":1,"docstring":null,"signature":"() int","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"int"} {"id":"function:9fc22e42ff0c3978213f406b53b4c028","kind":"function","name":"raw_bytes_doc","qualified_name":"raw_bytes_doc","file_path":"docstrings.py","language":"python","start_line":49,"end_line":51,"start_column":0,"end_column":12,"docstring":null,"signature":"()","visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:a05bea6931867f84f2b6b742c7b287a1","kind":"function","name":"handler","qualified_name":"handler","file_path":"torture.rs","language":"rust","start_line":233,"end_line":233,"start_column":0,"end_column":15,"docstring":null,"signature":"()","visibility":"private","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:a07d8ba50e32351d67a5892444ec6842","kind":"function","name":"clamp_value","qualified_name":"clamp_value","file_path":"torture.hpp","language":"cpp","start_line":16,"end_line":18,"start_column":0,"end_column":1,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"T"} @@ -835,7 +834,6 @@ {"id":"function:a115cbc1056e4c2b609bc8d94095850a","kind":"function","name":"describe","qualified_name":"describe","file_path":"torture.R","language":"r","start_line":80,"end_line":82,"start_column":0,"end_column":2,"docstring":null,"signature":"(obj)","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:a1446dbde46fbb3ff4ae7c57e9e9e7ed","kind":"function","name":"docBelow","qualified_name":"docBelow","file_path":"TortureAnnotatedComments.dart","language":"dart","start_line":20,"end_line":20,"start_column":0,"end_column":15,"docstring":"Doc below an annotation.","signature":"void ()","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:a1998a5fcada0a95b1aebb30d3f1222c","kind":"function","name":"bodyShapes","qualified_name":"bodyShapes","file_path":"torture.dart","language":"dart","start_line":60,"end_line":96,"start_column":0,"end_column":39,"docstring":null,"signature":"void (List xs, Object o)","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} -{"id":"function:a1ea154d9e5e5d67a05afd07d2206001","kind":"function","name":"shadowed","qualified_name":"shadowed","file_path":"torture.go","language":"go","start_line":67,"end_line":70,"start_column":0,"end_column":1,"docstring":null,"signature":"()","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:a4b908646a5315f4505eec1514c9da0a","kind":"function","name":"plainDoc","qualified_name":"plainDoc","file_path":"torture.dart","language":"dart","start_line":17,"end_line":17,"start_column":0,"end_column":17,"docstring":"Plain comment doc.","signature":"String ()","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"String"} {"id":"function:a6b241930489a440e4bc3a5a7f47d98a","kind":"function","name":"genericFn","qualified_name":"genericFn","file_path":"torture.luau","language":"luau","start_line":18,"end_line":20,"start_column":0,"end_column":3,"docstring":null,"signature":"(v: T): T","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:a7e6f3daf2b9428e6be15b312b88381c","kind":"function","name":"nativeCount","qualified_name":"Store::nativeCount","file_path":"TortureDeclarationDocs.dart","language":"dart","start_line":26,"end_line":26,"start_column":11,"end_column":28,"docstring":"Block dartdoc on an external member.","signature":"int ()","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"int"} @@ -856,13 +854,13 @@ {"id":"function:b06c2d27fa49d5e97af860ffe032fbdd","kind":"function","name":"listRet","qualified_name":"listRet","file_path":"torture.dart","language":"dart","start_line":49,"end_line":49,"start_column":0,"end_column":45,"docstring":null,"signature":"List (Map m)","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"List"} {"id":"function:b23e44d63824a6cb8592791623a8d66f","kind":"function","name":"mount","qualified_name":"mount","file_path":"torture.rs","language":"rust","start_line":280,"end_line":284,"start_column":0,"end_column":1,"docstring":null,"signature":"()","visibility":"private","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:b2796746ed44ab5bef76be2c9278aac2","kind":"function","name":"mixedLineThenBlock","qualified_name":"mixedLineThenBlock","file_path":"TortureDocs.scala","language":"scala","start_line":16,"end_line":16,"start_column":0,"end_column":33,"docstring":"leading line\nblock after line","signature":"(): Int","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"Int"} +{"id":"function:b2a0ca120e76741ae6bb879a2f0aea6f","kind":"function","name":"mdLoad","qualified_name":"mdLoad","file_path":"torture.go","language":"go","start_line":113,"end_line":115,"start_column":0,"end_column":1,"docstring":null,"signature":"()","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:b2e873527f650881fbc6a4f56fb4e377","kind":"function","name":"numback","qualified_name":"numback","file_path":"torture.dart","language":"dart","start_line":46,"end_line":46,"start_column":0,"end_column":18,"docstring":null,"signature":"num (num n)","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"num"} {"id":"function:b3507638f9afee3fde16cc943fd670cf","kind":"function","name":"constructions","qualified_name":"app::constructions","file_path":"torture-macros-ctors.cpp","language":"cpp","start_line":37,"end_line":51,"start_column":0,"end_column":1,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:b37e2f22c8e8740c4b4af7720ef91073","kind":"function","name":"genStar","qualified_name":"genStar","file_path":"torture.dart","language":"dart","start_line":39,"end_line":41,"start_column":0,"end_column":21,"docstring":null,"signature":"Stream ()","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"Stream"} {"id":"function:b3acc2cf7d22f273bc07354ae4991ac2","kind":"function","name":"documented_fn","qualified_name":"documented_fn","file_path":"torture.rb","language":"ruby","start_line":216,"end_line":216,"start_column":0,"end_column":22,"docstring":"Doc for documented_fn.\nSecond line.","signature":null,"visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:b3ad19a43be1e5f721f95a33c306075a","kind":"function","name":"walk","qualified_name":"Walker::walk","file_path":"TortureDeclarationDocs.dart","language":"dart","start_line":86,"end_line":86,"start_column":2,"end_column":13,"docstring":"Mixin abstract doc.","signature":"void ()","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:b67d89cb05fb830ccc666734f2335062","kind":"function","name":"draw","qualified_name":"com.example.torture::caller::draw","file_path":"torture.kt","language":"kotlin","start_line":231,"end_line":231,"start_column":8,"end_column":40,"docstring":null,"signature":"()","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} -{"id":"function:b7ab4c33e93fae58f533755e245826ea","kind":"function","name":"NewRegistry","qualified_name":"NewRegistry","file_path":"torture.go","language":"go","start_line":48,"end_line":55,"start_column":0,"end_column":1,"docstring":null,"signature":"() *Registry","visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"Registry"} {"id":"function:b7ca967fea03e27557d0c5f386de2439","kind":"function","name":"ratelimited_warn","qualified_name":"ratelimited_warn","file_path":"torture.c","language":"c","start_line":151,"end_line":154,"start_column":0,"end_column":1,"docstring":"block-scope prefixed declaration macro","signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:b816fb5ed257d41cc58189ef5c8e00ec","kind":"function","name":"old","qualified_name":"old","file_path":"torture.scala","language":"scala","start_line":32,"end_line":32,"start_column":0,"end_column":45,"docstring":null,"signature":"(): Int","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"Int"} {"id":"function:b951abb9eb66b92ce671266642a2a7f4","kind":"function","name":"render","qualified_name":"render","file_path":"torture.rs","language":"rust","start_line":180,"end_line":180,"start_column":4,"end_column":23,"docstring":null,"signature":"(&self)","visibility":"private","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} @@ -900,6 +898,7 @@ {"id":"function:d44c0c526394abe8b8527106f7f7c208","kind":"function","name":"vec_ret","qualified_name":"vec_ret","file_path":"torture.rs","language":"rust","start_line":204,"end_line":206,"start_column":0,"end_column":1,"docstring":null,"signature":"(w: &Widget) -> Vec","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"Vec"} {"id":"function:d462abbd6321ff43ca557c0c82e26cfc","kind":"function","name":"attributed","qualified_name":"attributed","file_path":"torture.swift","language":"swift","start_line":129,"end_line":129,"start_column":0,"end_column":26,"docstring":"kept over attribute","signature":null,"visibility":"internal","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:d48f5be24010704d02ad9d861b7fc7db","kind":"function","name":"afterConst","qualified_name":"afterConst","file_path":"TortureAnnotatedDocs.dart","language":"dart","start_line":58,"end_line":58,"start_column":0,"end_column":17,"docstring":null,"signature":"void ()","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} +{"id":"function:d58344b89f92bcdaf87bfd139418e73d","kind":"function","name":"shadowed","qualified_name":"shadowed","file_path":"torture.go","language":"go","start_line":93,"end_line":96,"start_column":0,"end_column":1,"docstring":null,"signature":"()","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:d6218e84934f1b87a8e3b9a46d44010a","kind":"function","name":"deleteItem","qualified_name":"deleteItem","file_path":"torture.js","language":"javascript","start_line":84,"end_line":84,"start_column":28,"end_column":89,"docstring":null,"signature":"(req, res)","visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:d748cd0b58e5e4b510fa32d428851b25","kind":"function","name":"helper","qualified_name":"helper","file_path":"torture-macros.c","language":"c","start_line":8,"end_line":8,"start_column":0,"end_column":38,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:d7da4b402ad97a64a1766fcf09e392e0","kind":"function","name":"handle_recv","qualified_name":"handle_recv","file_path":"torture.c","language":"c","start_line":60,"end_line":64,"start_column":0,"end_column":1,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} @@ -928,6 +927,7 @@ {"id":"function:e14a4344276cfa3f79d081355b15c00f","kind":"function","name":"sync","qualified_name":"Store::sync","file_path":"TortureDeclarationDocs.dart","language":"dart","start_line":14,"end_line":14,"start_column":2,"end_column":13,"docstring":null,"signature":"void ()","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:e1fec70cd107a9489ab5d8030b3c3cbb","kind":"function","name":"numRet","qualified_name":"numRet","file_path":"TortureSigs.dart","language":"dart","start_line":8,"end_line":8,"start_column":0,"end_column":17,"docstring":null,"signature":"num (num n)","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"num"} {"id":"function:e20384b6ac1f9559f78e2c0bd7b0328e","kind":"function","name":"hdr_raw","qualified_name":"hdr_raw","file_path":"torture.c","language":"c","start_line":170,"end_line":170,"start_column":0,"end_column":67,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} +{"id":"function:e2ce1de9e98005e0d5b6bb7119c7978a","kind":"function","name":"TargetCb","qualified_name":"TargetCb","file_path":"torture.go","language":"go","start_line":91,"end_line":91,"start_column":0,"end_column":23,"docstring":null,"signature":"(n int)","visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:e3c4c3687e202992a190d48e854e0c60","kind":"function","name":"mixedDoc","qualified_name":"mixedDoc","file_path":"TortureCtors.dart","language":"dart","start_line":6,"end_line":6,"start_column":0,"end_column":15,"docstring":"Line doc kept.\nPlain comment also kept?","signature":"void ()","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:e435e03b8be136b114b6446dd7f7e827","kind":"function","name":"bare_tuple","qualified_name":"bare_tuple","file_path":"docstrings.py","language":"python","start_line":82,"end_line":84,"start_column":0,"end_column":12,"docstring":null,"signature":"()","visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:e47ec30588abc2975d100ed00651ad8a","kind":"function","name":"blockDoc","qualified_name":"blockDoc","file_path":"torture.dart","language":"dart","start_line":14,"end_line":14,"start_column":0,"end_column":14,"docstring":"Block dartdoc kept.","signature":"int ()","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"int"} @@ -947,6 +947,7 @@ {"id":"function:f00a265ed388e20f65b3357ab065773e","kind":"function","name":"generatorFn","qualified_name":"generatorFn","file_path":"torture.tsx","language":"tsx","start_line":100,"end_line":102,"start_column":0,"end_column":1,"docstring":null,"signature":"(items: Item[])","visibility":null,"is_exported":0,"is_async":1,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:f09484bbbd7f1af828cd929c8f3dffe9","kind":"function","name":"after_unicode","qualified_name":"after_unicode","file_path":"torture.lua","language":"lua","start_line":105,"end_line":105,"start_column":0,"end_column":28,"docstring":null,"signature":"()","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:f0c3f454f6a81e937bb1347926ed1f97","kind":"function","name":"nullableRet","qualified_name":"com.example.torture::nullableRet","file_path":"torture.kt","language":"kotlin","start_line":32,"end_line":32,"start_column":0,"end_column":34,"docstring":null,"signature":"(): WidgetK?","visibility":"public","is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"WidgetK"} +{"id":"function:f0f9e8fdbfce4addf8bfac6085255496","kind":"function","name":"NewRegistry","qualified_name":"NewRegistry","file_path":"torture.go","language":"go","start_line":74,"end_line":81,"start_column":0,"end_column":1,"docstring":null,"signature":"() *Registry","visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"Registry"} {"id":"function:f11355267c15b025e65a025974c2a4d1","kind":"function","name":"readsTable","qualified_name":"readsTable","file_path":"torture.tsx","language":"tsx","start_line":193,"end_line":195,"start_column":7,"end_column":1,"docstring":null,"signature":"()","visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:f196b629dadd3e8bafa791d7053774ee","kind":"function","name":"md_load","qualified_name":"md_load","file_path":"torture.cpp","language":"cpp","start_line":155,"end_line":155,"start_column":0,"end_column":64,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"function:f4175a0ec088921527968e2183a05349","kind":"function","name":"nested_def","qualified_name":"Documented::container_method::nested_def","file_path":"torture.rb","language":"ruby","start_line":190,"end_line":190,"start_column":4,"end_column":23,"docstring":null,"signature":null,"visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} @@ -1103,6 +1104,7 @@ {"id":"interface:ac832273445e5a59ca5679d25afdcdc1","kind":"interface","name":"IWidgetRepo","qualified_name":"Torture.Alpha::IWidgetRepo","file_path":"Torture.cs","language":"csharp","start_line":158,"end_line":163,"start_column":4,"end_column":5,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"interface:b3282ee81a952dfabf759c45dabb86de","kind":"interface","name":"Number","qualified_name":"Number","file_path":"torture.go","language":"go","start_line":40,"end_line":42,"start_column":5,"end_column":1,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"interface:cb62fc849789b030a200ee6cef9c98b0","kind":"interface","name":"Core","qualified_name":"Core","file_path":"torture.go","language":"go","start_line":31,"end_line":38,"start_column":5,"end_column":1,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} +{"id":"interface:eb24239549242ce0007b1ae6f6844c48","kind":"interface","name":"IfaceAlias","qualified_name":"IfaceAlias","file_path":"torture.go","language":"go","start_line":64,"end_line":66,"start_column":5,"end_column":1,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"interface:fa3f5e7c386fbbd874e9de21b328cc5c","kind":"interface","name":"Marker","qualified_name":"com.example.torture::Marker","file_path":"Torture.java","language":"java","start_line":130,"end_line":132,"start_column":0,"end_column":1,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"method:008f2ecd9ce97a5947e4c945b89814b6","kind":"method","name":"prev","qualified_name":"Point::prev","file_path":"TortureDeclarationDocs.dart","language":"dart","start_line":62,"end_line":62,"start_column":2,"end_column":28,"docstring":null,"signature":"(this.x, this.y)","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"Point"} {"id":"method:033ac2ba34fbab2bc8e4448a87dd89ce","kind":"method","name":"computed","qualified_name":"Holder::computed","file_path":"TortureDoubleWalk.dart","language":"dart","start_line":34,"end_line":34,"start_column":2,"end_column":18,"docstring":null,"signature":"int","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"int"} @@ -1135,6 +1137,7 @@ {"id":"method:147793eea51c535cc684f2a3ae4157a7","kind":"method","name":"now_private","qualified_name":"Documented::now_private","file_path":"torture.rb","language":"ruby","start_line":101,"end_line":101,"start_column":2,"end_column":22,"docstring":null,"signature":null,"visibility":"private","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"method:14e2f2067b3db3d9c4de91be86bfae4d","kind":"method","name":"TortureService","qualified_name":"com.example.torture::TortureService::TortureService","file_path":"Torture.java","language":"java","start_line":36,"end_line":48,"start_column":2,"end_column":3,"docstring":"Ctor javadoc.","signature":"(List names)","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"method:14fb931f9d612796f38d84c71078c9ae","kind":"method","name":"Dispose","qualified_name":"Torture.Alpha::TortureClass::Dispose","file_path":"Torture.cs","language":"csharp","start_line":57,"end_line":57,"start_column":8,"end_column":49,"docstring":null,"signature":null,"visibility":"private","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} +{"id":"method:1516d9f02e3a409a1257f9f857fa65ee","kind":"method","name":"Render","qualified_name":"IfaceAlias::Render","file_path":"torture.go","language":"go","start_line":65,"end_line":65,"start_column":1,"end_column":16,"docstring":null,"signature":"() string","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"method:17231adc3d18205998834087d654a9b4","kind":"method","name":"apply_confirm","qualified_name":"ServiceList::apply_confirm","file_path":"torture.tsx","language":"tsx","start_line":86,"end_line":86,"start_column":2,"end_column":37,"docstring":null,"signature":"Service<'apply_confirm', Req, Resp>","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"method:17a50ac15273395d815f92781e5e4dad","kind":"method","name":"validate_it","qualified_name":"Stack::validate_it","file_path":"torture.R","language":"r","start_line":105,"end_line":105,"start_column":4,"end_column":33,"docstring":null,"signature":"()","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"method:17ab262472e1d06bbe20faf7c5d6b6ce","kind":"method","name":"classFunc","qualified_name":"Session::classFunc","file_path":"torture.swift","language":"swift","start_line":19,"end_line":19,"start_column":2,"end_column":32,"docstring":null,"signature":null,"visibility":"internal","is_exported":0,"is_async":0,"is_static":1,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} @@ -1315,7 +1318,6 @@ {"id":"method:7d23fed280253ae0540584261ccad3be","kind":"method","name":"PP","qualified_name":"Torture.Alpha::TortureClass::PP","file_path":"Torture.cs","language":"csharp","start_line":60,"end_line":60,"start_column":8,"end_column":39,"docstring":null,"signature":null,"visibility":"private","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"method:7d5eac3e4b8be20f0d31544fbeb06f59","kind":"method","name":"on_stop","qualified_name":"M.callbacks::on_stop","file_path":"torture.lua","language":"lua","start_line":75,"end_line":77,"start_column":15,"end_column":4,"docstring":null,"signature":"()","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"method:7d5ee6ac8b49ce375f19dc27d0484e80","kind":"method","name":"load","qualified_name":"Config::load","file_path":"TortureReadsTypes.dart","language":"dart","start_line":80,"end_line":80,"start_column":2,"end_column":13,"docstring":null,"signature":"void ()","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} -{"id":"method:7d8ded560e29b4ffe08aa4244f7ad020","kind":"method","name":"Push","qualified_name":"Stack::Push","file_path":"torture.go","language":"go","start_line":57,"end_line":59,"start_column":0,"end_column":1,"docstring":null,"signature":"(item T)","visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"method:7e714128b5b3c81a4e4f571a142197ba","kind":"method","name":"Widget","qualified_name":"app::Widget::Widget","file_path":"torture-macros-ctors.cpp","language":"cpp","start_line":28,"end_line":28,"start_column":0,"end_column":41,"docstring":null,"signature":"()","visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"method:7f480d6918f98c10a04c91362d661b3e","kind":"method","name":"named","qualified_name":"_ImplT::named","file_path":"TortureConstCtors.dart","language":"dart","start_line":56,"end_line":56,"start_column":2,"end_column":22,"docstring":null,"signature":"()","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"_ImplT"} {"id":"method:7fe7437d650d086d067a7cc00ec38cca","kind":"method","name":"hint","qualified_name":"Render::hint","file_path":"torture.rs","language":"rust","start_line":43,"end_line":45,"start_column":4,"end_column":5,"docstring":null,"signature":"(&self) -> Size","visibility":"private","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"Size"} @@ -1481,7 +1483,6 @@ {"id":"method:e0a8a040d70ccfd94ca5a46979094dae","kind":"method","name":"size2","qualified_name":"Map::size2","file_path":"TortureReadsTypes.dart","language":"dart","start_line":19,"end_line":19,"start_column":2,"end_column":15,"docstring":null,"signature":"int","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"int"} {"id":"method:e13588704c75e83833678ebaf5efcf3a","kind":"method","name":"pm","qualified_name":"Late::pm","file_path":"torture.R","language":"r","start_line":139,"end_line":139,"start_column":16,"end_column":40,"docstring":null,"signature":"()","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"method:e1784780370a76048ebe0d56ffbd60d1","kind":"method","name":"stm","qualified_name":"SelfTyped::stm","file_path":"TortureMisc.scala","language":"scala","start_line":5,"end_line":5,"start_column":2,"end_column":20,"docstring":null,"signature":"(): Int","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"Int"} -{"id":"method:e1f80971e6bc3a4e161d867b6a28844d","kind":"method","name":"Render","qualified_name":"Widget::Render","file_path":"torture.go","language":"go","start_line":61,"end_line":63,"start_column":0,"end_column":1,"docstring":null,"signature":"() string","visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"string"} {"id":"method:e20a7ba4db91a5635fcd83cfeb744be8","kind":"method","name":"other","qualified_name":"Holder::other","file_path":"TortureDoubleWalk.dart","language":"dart","start_line":26,"end_line":26,"start_column":2,"end_column":16,"docstring":null,"signature":"()","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"Holder"} {"id":"method:e349908111fb7121ad8228137a8e2647","kind":"method","name":"compute","qualified_name":"BaseService::compute","file_path":"torture.tsx","language":"tsx","start_line":44,"end_line":46,"start_column":2,"end_column":3,"docstring":null,"signature":"(n: number): number","visibility":"private","is_exported":0,"is_async":0,"is_static":1,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"method:e3a8fd81bee3f156fa7af9c4f66a1329","kind":"method","name":"HandleThing","qualified_name":"Torture.Alpha::TortureClass::HandleThing","file_path":"Torture.cs","language":"csharp","start_line":66,"end_line":66,"start_column":8,"end_column":43,"docstring":null,"signature":null,"visibility":"private","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} @@ -1508,6 +1509,7 @@ {"id":"method:ee9f186763c4ce9337ce728d7b8c54e9","kind":"method","name":"render","qualified_name":"BufSource::render","file_path":"torture.rs","language":"rust","start_line":166,"end_line":166,"start_column":4,"end_column":23,"docstring":null,"signature":"(&self)","visibility":"private","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"method:eee1838bebaf66382792b5d17ddaaa5f","kind":"method","name":"Go","qualified_name":"Torture.Scoped::Svc::Go","file_path":"TortureFileScoped.cs","language":"csharp","start_line":23,"end_line":23,"start_column":4,"end_column":38,"docstring":null,"signature":null,"visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"method:ef770437bcc4f4b3a1dbf17b8a1720a6","kind":"method","name":"area","qualified_name":"Drawable::area","file_path":"torture.scala","language":"scala","start_line":82,"end_line":82,"start_column":2,"end_column":26,"docstring":null,"signature":"(): Double","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"Double"} +{"id":"method:f04601d0017a75d44928a1341b1d2aa3","kind":"method","name":"Push","qualified_name":"Stack::Push","file_path":"torture.go","language":"go","start_line":83,"end_line":85,"start_column":0,"end_column":1,"docstring":null,"signature":"(item T)","visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"method:f098ed2b4228c5a44ad43c16f8606866","kind":"method","name":"inline","qualified_name":"C::inline","file_path":"TortureAnnotatedComments.dart","language":"dart","start_line":70,"end_line":70,"start_column":18,"end_column":31,"docstring":"inline","signature":"void ()","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"method:f103d82d67ab4848ea3da2f93e9961ac","kind":"method","name":"toString","qualified_name":"Mode::toString","file_path":"TortureAnnotatedDocs.dart","language":"dart","start_line":181,"end_line":181,"start_column":2,"end_column":19,"docstring":"Enum member doc.","signature":"String ()","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"String"} {"id":"method:f11a1632c933108942e00d3b2c940adc","kind":"method","name":"render","qualified_name":"WidgetS::render","file_path":"torture.scala","language":"scala","start_line":50,"end_line":54,"start_column":2,"end_column":3,"docstring":null,"signature":"(): RenderResult","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"RenderResult"} @@ -1522,6 +1524,7 @@ {"id":"method:f73f31bd6cc45134433935ad6eaae9f9","kind":"method","name":"Widget","qualified_name":"app::Widget::Widget","file_path":"torture-macros-ctors.cpp","language":"cpp","start_line":21,"end_line":21,"start_column":2,"end_column":51,"docstring":null,"signature":"(int a, int b = 2)","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"method:f7a955e2e4b4da5329f235be6578e7cb","kind":"method","name":"value","qualified_name":"C::value","file_path":"TortureAnnotatedComments.dart","language":"dart","start_line":102,"end_line":102,"start_column":2,"end_column":15,"docstring":"A comment.","signature":"int","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"int"} {"id":"method:f8290ba8d17cc93c787ba897a8aef206","kind":"method","name":"onX","qualified_name":"Point::onX","file_path":"TortureDeclarationDocs.dart","language":"dart","start_line":44,"end_line":44,"start_column":2,"end_column":18,"docstring":"Redirecting constructor doc.","signature":"(int x)","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"Point"} +{"id":"method:f8796162f1bd1fa332ec680173c6c80a","kind":"method","name":"Render","qualified_name":"Widget::Render","file_path":"torture.go","language":"go","start_line":87,"end_line":89,"start_column":0,"end_column":1,"docstring":null,"signature":"() string","visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"string"} {"id":"method:f93cac5b4e9fd958cce4400d8a1fb21a","kind":"method","name":"total","qualified_name":"ListX::total","file_path":"TortureReadsTypes.dart","language":"dart","start_line":11,"end_line":11,"start_column":2,"end_column":15,"docstring":null,"signature":"int","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"int"} {"id":"method:fb3814b6e956f06c3746e1758bfb591d","kind":"method","name":"draw","qualified_name":"Drawable::draw","file_path":"torture.scala","language":"scala","start_line":81,"end_line":81,"start_column":2,"end_column":18,"docstring":null,"signature":"(): Unit","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"Unit"} {"id":"method:fbb2a0dca224ce63879bc13f1b316257","kind":"method","name":"name","qualified_name":"Config::name","file_path":"TortureReadsTypes.dart","language":"dart","start_line":79,"end_line":79,"start_column":2,"end_column":17,"docstring":null,"signature":"String","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":"String"} @@ -1594,6 +1597,7 @@ {"id":"struct:0817bec85383d0bce329cdea6a6d811b","kind":"struct","name":"Unit","qualified_name":"Unit","file_path":"torture.rs","language":"rust","start_line":18,"end_line":18,"start_column":0,"end_column":16,"docstring":null,"signature":null,"visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"struct:08acded06d18f155491d1e66e44ba80b","kind":"struct","name":"Vec2","qualified_name":"Vec2","file_path":"torture.cpp","language":"cpp","start_line":82,"end_line":87,"start_column":0,"end_column":1,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"struct:0c309a8ed6f33456724d94ba73599e04","kind":"struct","name":"Widget","qualified_name":"Widget","file_path":"torture.rs","language":"rust","start_line":12,"end_line":16,"start_column":0,"end_column":1,"docstring":"Widget docs.","signature":null,"visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} +{"id":"struct:182a7933759975da57345ac013fe7619","kind":"struct","name":"AnonAlias","qualified_name":"AnonAlias","file_path":"torture.go","language":"go","start_line":59,"end_line":62,"start_column":5,"end_column":1,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"struct:1a02b04e584e146642ddb911b81ce419","kind":"struct","name":"Stack","qualified_name":"Stack","file_path":"torture.go","language":"go","start_line":27,"end_line":29,"start_column":5,"end_column":1,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"struct:22cb841d7a3265379018f5b56364e98c","kind":"struct","name":"BufSource","qualified_name":"BufSource","file_path":"torture.rs","language":"rust","start_line":136,"end_line":138,"start_column":0,"end_column":1,"docstring":null,"signature":null,"visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"struct:27fbb507111460b65bffde24338f1495","kind":"struct","name":"flex_tail","qualified_name":"flex_tail","file_path":"torture.c","language":"c","start_line":119,"end_line":122,"start_column":0,"end_column":1,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} @@ -1636,9 +1640,11 @@ {"id":"type_alias:14815ab9ef5358d5c7df8b3c5c636ba0","kind":"type_alias","name":"Dur","qualified_name":"Dur","file_path":"torture.go","language":"go","start_line":46,"end_line":46,"start_column":5,"end_column":12,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"type_alias:1b5eb3ef72c3df6177bec86b60d9b4ab","kind":"type_alias","name":"Handle","qualified_name":"Handle","file_path":"torture.cpp","language":"cpp","start_line":94,"end_line":94,"start_column":0,"end_column":27,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"type_alias:1cd665599e50d4f472ddeda93071b27a","kind":"type_alias","name":"Member","qualified_name":"Outer2::Member","file_path":"TortureMisc.scala","language":"scala","start_line":8,"end_line":8,"start_column":2,"end_column":25,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} +{"id":"type_alias:2eadd35ab3a35039204ee7e485c9d3f4","kind":"type_alias","name":"PtrAlias","qualified_name":"PtrAlias","file_path":"torture.go","language":"go","start_line":53,"end_line":53,"start_column":1,"end_column":25,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"type_alias:428bef2eb5ba989d4984c0c1fb4a1e02","kind":"type_alias","name":"WidgetList","qualified_name":"com.example.torture::WidgetList","file_path":"torture.kt","language":"kotlin","start_line":168,"end_line":168,"start_column":0,"end_column":36,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"type_alias:4eac8834dd1ceb7059cdc0ebd6a23f96","kind":"type_alias","name":"Handler","qualified_name":"Handler","file_path":"torture.swift","language":"swift","start_line":75,"end_line":75,"start_column":0,"end_column":34,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"type_alias:544f3be472935dfbe579fa190ead63ab","kind":"type_alias","name":"MapAlias","qualified_name":"MapAlias","file_path":"torture.dart","language":"dart","start_line":210,"end_line":210,"start_column":0,"end_column":40,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} +{"id":"type_alias:5703c449ce310e218eeb262059f2b2cd","kind":"type_alias","name":"WordAlias","qualified_name":"WordAlias","file_path":"torture.go","language":"go","start_line":68,"end_line":68,"start_column":5,"end_column":21,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"type_alias:58bcebb7feaaca7a0be3c251dfa21bd7","kind":"type_alias","name":"Item","qualified_name":"Item","file_path":"torture.rs","language":"rust","start_line":151,"end_line":151,"start_column":4,"end_column":20,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"type_alias:5cd4631080c4eec3535487f74a11a6e5","kind":"type_alias","name":"Handler","qualified_name":"Handler","file_path":"torture.luau","language":"luau","start_line":9,"end_line":9,"start_column":0,"end_column":41,"docstring":"- doc for exported Handler","signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"type_alias:600473b4a3158649443b2f214febf906","kind":"type_alias","name":"Handle","qualified_name":"Handle","file_path":"torture.tsx","language":"tsx","start_line":78,"end_line":82,"start_column":7,"end_column":2,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} @@ -1649,12 +1655,17 @@ {"id":"type_alias:813393b308ca1d8f278ce99d55bf11d7","kind":"type_alias","name":"IntFn","qualified_name":"IntFn","file_path":"torture.dart","language":"dart","start_line":208,"end_line":208,"start_column":0,"end_column":34,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"type_alias:82325cc15cfda33fe564374ff3fd49d0","kind":"type_alias","name":"PlatformClock","qualified_name":"com.example.torture::PlatformClock","file_path":"torture.kt","language":"kotlin","start_line":178,"end_line":178,"start_column":0,"end_column":48,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":"[\"actual\"]","type_parameters":null,"return_type":null} {"id":"type_alias:8d09bbd71ebfe2d1aa557e0e79aaab99","kind":"type_alias","name":"BuilderAlias","qualified_name":"BuilderAlias","file_path":"torture.swift","language":"swift","start_line":76,"end_line":76,"start_column":0,"end_column":35,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} +{"id":"type_alias:8e43bdc4a5230913e6bb71478f4d3ce9","kind":"type_alias","name":"FnAlias","qualified_name":"FnAlias","file_path":"torture.go","language":"go","start_line":54,"end_line":54,"start_column":1,"end_column":34,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} +{"id":"type_alias:90db322d3e0eb78357aa837fa1f92170","kind":"type_alias","name":"Alias","qualified_name":"Alias","file_path":"torture.go","language":"go","start_line":49,"end_line":49,"start_column":5,"end_column":24,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"type_alias:a4b2f4669bb9b6a430127b2bd835436f","kind":"type_alias","name":"FromTypeof","qualified_name":"FromTypeof","file_path":"torture.luau","language":"luau","start_line":11,"end_line":11,"start_column":0,"end_column":55,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"type_alias:afe0d22243bb9ac1575ca6df41bf921c","kind":"type_alias","name":"WidgetList","qualified_name":"WidgetList","file_path":"torture.scala","language":"scala","start_line":123,"end_line":123,"start_column":0,"end_column":31,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"type_alias:c5961e802ae860f4834791477a9aa980","kind":"type_alias","name":"OldStyle","qualified_name":"OldStyle","file_path":"TortureMini.dart","language":"dart","start_line":74,"end_line":74,"start_column":0,"end_column":29,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"type_alias:cd058f941742f057073628ddb35e0b9c","kind":"type_alias","name":"Handler","qualified_name":"com.example.torture::Handler","file_path":"torture.kt","language":"kotlin","start_line":167,"end_line":167,"start_column":0,"end_column":33,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"type_alias:cdaae8f951716ba1d5b3f5faa698cf1c","kind":"type_alias","name":"MaybeShape","qualified_name":"MaybeShape","file_path":"torture.tsx","language":"tsx","start_line":89,"end_line":89,"start_column":7,"end_column":38,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} +{"id":"type_alias:d6c6403a980c3fa8265ad86510c9888f","kind":"type_alias","name":"Defined","qualified_name":"Defined","file_path":"torture.go","language":"go","start_line":56,"end_line":56,"start_column":1,"end_column":18,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} +{"id":"type_alias:e443846c686bab2d9b4f68123bff60b2","kind":"type_alias","name":"LocalAlias","qualified_name":"LocalAlias","file_path":"torture.go","language":"go","start_line":52,"end_line":52,"start_column":1,"end_column":20,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"type_alias:ea7602cc1268d15f573f37f43c58d4e8","kind":"type_alias","name":"conn_pool_t","qualified_name":"conn_pool_t","file_path":"torture.c","language":"c","start_line":33,"end_line":33,"start_column":0,"end_column":37,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} +{"id":"type_alias:f009af79ecd90d8b4e99e21e99f148e8","kind":"type_alias","name":"MapAlias","qualified_name":"MapAlias","file_path":"torture.go","language":"go","start_line":55,"end_line":55,"start_column":1,"end_column":36,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"type_alias:f11a5d6472370edc6684dfe077359e68","kind":"type_alias","name":"Generic","qualified_name":"Generic","file_path":"torture.luau","language":"luau","start_line":10,"end_line":10,"start_column":0,"end_column":37,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"type_alias:f84ae3400c1d88837e6d67a42d5eea5a","kind":"type_alias","name":"ServiceList","qualified_name":"ServiceList","file_path":"torture.tsx","language":"tsx","start_line":84,"end_line":87,"start_column":7,"end_column":2,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"type_alias:feafa08c2035b9892203463ba89e6d20","kind":"type_alias","name":"Alias","qualified_name":"Alias","file_path":"torture.rs","language":"rust","start_line":38,"end_line":38,"start_column":0,"end_column":29,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} @@ -1673,7 +1684,6 @@ {"id":"variable:144c41f1fe7d4cdfb643e499172e0f2c","kind":"variable","name":"MD_TWO_IN_ONE","qualified_name":"MD_TWO_IN_ONE","file_path":"torture.rb","language":"ruby","start_line":243,"end_line":243,"start_column":0,"end_column":45,"docstring":null,"signature":"= 'see a.md and also sub/b.mdx'","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:150a636395ceb800837ea1ef750c2979","kind":"variable","name":"Signal","qualified_name":"Signal","file_path":"torture.luau","language":"luau","start_line":4,"end_line":4,"start_column":6,"end_column":12,"docstring":null,"signature":"= require(script.Parent.Signal)","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:1837cd19ce5f7a8f45b7fa4b8fc8a0e6","kind":"variable","name":"DefaultRegistry","qualified_name":"DefaultRegistry","file_path":"torture.go","language":"go","start_line":11,"end_line":11,"start_column":4,"end_column":35,"docstring":null,"signature":"= NewRegistry()","visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} -{"id":"variable:193a260abddbacd4b8812616870236ab","kind":"variable","name":"mdRooted","qualified_name":"mdRooted","file_path":"torture.go","language":"go","start_line":81,"end_line":81,"start_column":4,"end_column":32,"docstring":null,"signature":"= \"/docs/rooted.md\"","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:1a91cb73462576a0b51b09168dd5c9d3","kind":"variable","name":"topVar","qualified_name":"com.example.torture::topVar","file_path":"torture.kt","language":"kotlin","start_line":47,"end_line":47,"start_column":0,"end_column":16,"docstring":null,"signature":null,"visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:1c2f423d65ed4cdd847201e9ee1dab10","kind":"variable","name":"MD_QUERIED","qualified_name":"MD_QUERIED","file_path":"torture.rs","language":"rust","start_line":308,"end_line":308,"start_column":6,"end_column":16,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:247f05d883ce1a98f012960b2a5ad930","kind":"variable","name":"twoB","qualified_name":"twoB","file_path":"torture.lua","language":"lua","start_line":11,"end_line":11,"start_column":12,"end_column":16,"docstring":null,"signature":"= require(\"two.b\")","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} @@ -1686,8 +1696,8 @@ {"id":"variable:2c97371982bc8cb00d33dea8739ea6e0","kind":"variable","name":"MD_QUERIED","qualified_name":"MD_QUERIED","file_path":"torture.rb","language":"ruby","start_line":242,"end_line":242,"start_column":0,"end_column":35,"docstring":null,"signature":"= './notes.md?raw=1#top'","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:2f8caf198d5da7166a16f55b9b05ec3e","kind":"variable","name":"md_bare","qualified_name":"md_bare","file_path":"torture.R","language":"r","start_line":211,"end_line":211,"start_column":0,"end_column":22,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:31334faedc88ad501a488504f781e9a8","kind":"variable","name":"MD_ROOTED","qualified_name":"MD_ROOTED","file_path":"torture.rs","language":"rust","start_line":305,"end_line":305,"start_column":6,"end_column":15,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} +{"id":"variable:338a23f1f938ffd8fa2b896287ab699f","kind":"variable","name":"mdBare","qualified_name":"mdBare","file_path":"torture.go","language":"go","start_line":106,"end_line":106,"start_column":4,"end_column":24,"docstring":null,"signature":"= \"README.md\"","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:36e608fb368763b7f8a74a69f477cec7","kind":"variable","name":"callbacks","qualified_name":"callbacks","file_path":"torture.py","language":"python","start_line":49,"end_line":49,"start_column":0,"end_column":29,"docstring":null,"signature":"= [target_cb, view]","visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} -{"id":"variable:3731c4860c0eac76475f173e3f52e850","kind":"variable","name":"mdBare","qualified_name":"mdBare","file_path":"torture.go","language":"go","start_line":80,"end_line":80,"start_column":4,"end_column":24,"docstring":null,"signature":"= \"README.md\"","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:3a1b0855fa589b2743e29e04375449c5","kind":"variable","name":"counter","qualified_name":"counter","file_path":"torture.c","language":"c","start_line":14,"end_line":14,"start_column":11,"end_column":22,"docstring":null,"signature":"= 0","visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:3cd4d572d8ccf445444d6889213b6b1e","kind":"variable","name":"plainLet","qualified_name":"plainLet","file_path":"torture.tsx","language":"tsx","start_line":16,"end_line":16,"start_column":4,"end_column":17,"docstring":null,"signature":"= 42","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:3e0e73251b80b2c57244a337e1df5cc6","kind":"variable","name":"core","qualified_name":"core","file_path":"torture.luau","language":"luau","start_line":3,"end_line":3,"start_column":6,"end_column":10,"docstring":"!strict\nheader comment for torture.luau","signature":"= require(\"app.core\")","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} @@ -1702,7 +1712,9 @@ {"id":"variable:4d047bb8d85f42d385cb6dae9e471c81","kind":"variable","name":"préfixe","qualified_name":"préfixe","file_path":"torture.lua","language":"lua","start_line":104,"end_line":104,"start_column":6,"end_column":13,"docstring":null,"signature":"= \"café\"","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:4f1c6c79b07af2e483190bf7c504c633","kind":"variable","name":"md_remote","qualified_name":"md_remote","file_path":"torture.R","language":"r","start_line":214,"end_line":214,"start_column":0,"end_column":44,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:52d1de85bc7cd2d0848da751d815b1e3","kind":"variable","name":"MD_REMOTE","qualified_name":"MD_REMOTE","file_path":"torture.rs","language":"rust","start_line":307,"end_line":307,"start_column":6,"end_column":15,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} +{"id":"variable:53163f1d6bc66b7bc2c6979841ca1abc","kind":"variable","name":"mdTwoInOne","qualified_name":"mdTwoInOne","file_path":"torture.go","language":"go","start_line":111,"end_line":111,"start_column":4,"end_column":46,"docstring":null,"signature":"= \"see a.md and also sub/b.mdx\"","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:533018efedb8f077960a6df1eff5dac2","kind":"variable","name":"ret_val","qualified_name":"ret_val","file_path":"torture.R","language":"r","start_line":184,"end_line":184,"start_column":0,"end_column":17,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} +{"id":"variable:5491cc9401b25cc313dfe1dfeb28d23c","kind":"variable","name":"mdRemote","qualified_name":"mdRemote","file_path":"torture.go","language":"go","start_line":109,"end_line":109,"start_column":4,"end_column":46,"docstring":null,"signature":"= \"https://example.com/remote.md\"","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:56e160aa3dbd02e9b407f1a673a9ef50","kind":"variable","name":"X_WITH_NEW","qualified_name":"X_WITH_NEW","file_path":"torture.rb","language":"ruby","start_line":18,"end_line":18,"start_column":0,"end_column":23,"docstring":null,"signature":"= Widget.new","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:5bd0b7aea699a56bf2affd88c24f6f47","kind":"variable","name":"md_bare","qualified_name":"md_bare","file_path":"torture.lua","language":"lua","start_line":113,"end_line":113,"start_column":6,"end_column":13,"docstring":null,"signature":"= \"README.md\"","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:5c2476d778ac4e63728eb013a773fbac","kind":"variable","name":"x2","qualified_name":"x2","file_path":"torture.R","language":"r","start_line":28,"end_line":28,"start_column":0,"end_column":8,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} @@ -1728,14 +1740,14 @@ {"id":"variable:8259711bc12da5017d83b9e8f3bd51a7","kind":"variable","name":"md_bare","qualified_name":"md_bare","file_path":"torture.luau","language":"luau","start_line":66,"end_line":66,"start_column":6,"end_column":13,"docstring":null,"signature":"= \"README.md\"","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:82fce40eeaabd8ccadc6779d37a57209","kind":"variable","name":"one","qualified_name":"one","file_path":"torture.lua","language":"lua","start_line":98,"end_line":98,"start_column":6,"end_column":9,"docstring":null,"signature":"= 1","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:84a830aad0caa242dcb5751c48dd954f","kind":"variable","name":"md_queried","qualified_name":"md_queried","file_path":"torture.R","language":"r","start_line":215,"end_line":215,"start_column":0,"end_column":36,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} -{"id":"variable:86594ca8f3b6dae35209d56363bcd5a7","kind":"variable","name":"mdTwoInOne","qualified_name":"mdTwoInOne","file_path":"torture.go","language":"go","start_line":85,"end_line":85,"start_column":4,"end_column":46,"docstring":null,"signature":"= \"see a.md and also sub/b.mdx\"","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} +{"id":"variable:84b3675fd4032c00792c256c0ba41496","kind":"variable","name":"mdRooted","qualified_name":"mdRooted","file_path":"torture.go","language":"go","start_line":107,"end_line":107,"start_column":4,"end_column":32,"docstring":null,"signature":"= \"/docs/rooted.md\"","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} +{"id":"variable:86bbaa5a32491e5a70aeb77a6373bdc7","kind":"variable","name":"mdQueried","qualified_name":"mdQueried","file_path":"torture.go","language":"go","start_line":110,"end_line":110,"start_column":4,"end_column":38,"docstring":null,"signature":"= \"./notes.md?raw=1#top\"","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:8ba939f37f98d071b9c0a5d1c73590cd","kind":"variable","name":"md_two_in_one","qualified_name":"md_two_in_one","file_path":"torture.lua","language":"lua","start_line":118,"end_line":118,"start_column":6,"end_column":19,"docstring":null,"signature":"= \"see a.md and also sub/b.mdx\"","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:8c9a81bef79763c9c67726355afcb403","kind":"variable","name":"two","qualified_name":"two","file_path":"torture.lua","language":"lua","start_line":98,"end_line":98,"start_column":20,"end_column":23,"docstring":null,"signature":"= 2","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:8fef25ea8abe486ee94190ed47c3644b","kind":"variable","name":"RETRY_LIMITS","qualified_name":"RETRY_LIMITS","file_path":"torture.py","language":"python","start_line":8,"end_line":8,"start_column":0,"end_column":23,"docstring":null,"signature":"= {\"a\": 1}","visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} -{"id":"variable:9567fda1765028e68645e39a624d61c7","kind":"variable","name":"mdGuide","qualified_name":"mdGuide","file_path":"torture.go","language":"go","start_line":79,"end_line":79,"start_column":4,"end_column":40,"docstring":"Markdown path references: code -> documentation edges. Every shape the\nnormalizer branches on, so the two arms have to agree about the rejections\n(URL, escape above the root) as well as the emissions.","signature":"= \"../docs/guide.md#install\"","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} -{"id":"variable:96073374f298db55700de2091389c955","kind":"variable","name":"mdEscapes","qualified_name":"mdEscapes","file_path":"torture.go","language":"go","start_line":82,"end_line":82,"start_column":4,"end_column":40,"docstring":null,"signature":"= \"../../../../outside.md\"","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:96a031e283e601ba4b3a8a5a2e9cc77f","kind":"variable","name":"core","qualified_name":"core","file_path":"torture.lua","language":"lua","start_line":4,"end_line":4,"start_column":6,"end_column":10,"docstring":"File-header comment run line 1\nline 2 of the run","signature":"= require(\"app.core\")","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:9b11e2d2271460cf5d5c12d00f607ca1","kind":"variable","name":"INIT_EAGER","qualified_name":"INIT_EAGER","file_path":"torture.py","language":"python","start_line":52,"end_line":52,"start_column":0,"end_column":21,"docstring":null,"signature":"= helper()","visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} +{"id":"variable:9efcdcca3961f25f73cb6ef8b0912799","kind":"variable","name":"mdGuide","qualified_name":"mdGuide","file_path":"torture.go","language":"go","start_line":105,"end_line":105,"start_column":4,"end_column":40,"docstring":"Markdown path references: code -> documentation edges. Every shape the\nnormalizer branches on, so the two arms have to agree about the rejections\n(URL, escape above the root) as well as the emissions.","signature":"= \"../docs/guide.md#install\"","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:9fb41be398c637882380aa0ca5e183b4","kind":"variable","name":"UPPER_CONST","qualified_name":"UPPER_CONST","file_path":"torture.lua","language":"lua","start_line":33,"end_line":33,"start_column":6,"end_column":17,"docstring":null,"signature":"= 42","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:9fc45d7622662ba5576f1caf82a1db04","kind":"variable","name":"x","qualified_name":"x","file_path":"torture.lua","language":"lua","start_line":103,"end_line":103,"start_column":6,"end_column":7,"docstring":null,"signature":"= 99","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:a3127f3f791b568d650e0e4f33895030","kind":"variable","name":"bracketed","qualified_name":"bracketed","file_path":"torture.lua","language":"lua","start_line":7,"end_line":7,"start_column":6,"end_column":15,"docstring":null,"signature":"= require [[app.bracketed]]","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} @@ -1743,10 +1755,10 @@ {"id":"variable:a65b93dcc6eb903cc15613a8ae87c378","kind":"variable","name":"rs","qualified_name":"rs","file_path":"torture.R","language":"r","start_line":196,"end_line":196,"start_column":0,"end_column":26,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:a811f3247c619993a74bd06169ef8003","kind":"variable","name":"dup_var","qualified_name":"dup_var","file_path":"torture.R","language":"r","start_line":188,"end_line":188,"start_column":0,"end_column":12,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:a811f3247c619993a74bd06169ef8003:14","kind":"variable","name":"dup_var","qualified_name":"dup_var","file_path":"torture.R","language":"r","start_line":188,"end_line":188,"start_column":14,"end_column":26,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} +{"id":"variable:a862a8826882bccf9eeee7d2218227f7","kind":"variable","name":"mdEscapes","qualified_name":"mdEscapes","file_path":"torture.go","language":"go","start_line":108,"end_line":108,"start_column":4,"end_column":40,"docstring":null,"signature":"= \"../../../../outside.md\"","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:acd0d7053b14d1a5ab405005581fcc7b","kind":"variable","name":"INIT_ALIAS","qualified_name":"INIT_ALIAS","file_path":"torture.rs","language":"rust","start_line":291,"end_line":291,"start_column":7,"end_column":17,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:ae80428a1b53633b9c3cbce4411dc5e0","kind":"variable","name":"md_two_in_one","qualified_name":"md_two_in_one","file_path":"torture.R","language":"r","start_line":216,"end_line":216,"start_column":0,"end_column":46,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:b0140d6f939baa22cf37e9ceb649296a","kind":"variable","name":"counter","qualified_name":"Session::counter","file_path":"torture.swift","language":"swift","start_line":10,"end_line":10,"start_column":2,"end_column":24,"docstring":null,"signature":null,"visibility":"internal","is_exported":0,"is_async":0,"is_static":1,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} -{"id":"variable:b0a4dbba6b6298349ca2c95d0d1a8136","kind":"variable","name":"mdQueried","qualified_name":"mdQueried","file_path":"torture.go","language":"go","start_line":84,"end_line":84,"start_column":4,"end_column":38,"docstring":null,"signature":"= \"./notes.md?raw=1#top\"","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:b2096ca0d0fb7b488116cbaa8064123a","kind":"variable","name":"MD_ESCAPES","qualified_name":"MD_ESCAPES","file_path":"torture.rb","language":"ruby","start_line":240,"end_line":240,"start_column":0,"end_column":37,"docstring":null,"signature":"= '../../../../outside.md'","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:b2213338e7f6b0c3bef9ff3bf3a681a8","kind":"variable","name":"lower_var","qualified_name":"lower_var","file_path":"torture.R","language":"r","start_line":26,"end_line":26,"start_column":0,"end_column":20,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:b34170d53b880e257e51d8c3941edba1","kind":"variable","name":"msg","qualified_name":"msg","file_path":"torture.R","language":"r","start_line":203,"end_line":203,"start_column":0,"end_column":17,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} @@ -1755,7 +1767,6 @@ {"id":"variable:b59829086b1004bb76baec4cba8a8b6d","kind":"variable","name":"count","qualified_name":"com.example.torture::Registry::count","file_path":"torture.kt","language":"kotlin","start_line":145,"end_line":145,"start_column":4,"end_column":17,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:b5c7b8633a304233212e0af8f98ee3db","kind":"variable","name":"SCALE","qualified_name":"SCALE","file_path":"torture.rs","language":"rust","start_line":69,"end_line":69,"start_column":10,"end_column":15,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:b8b7f8664f4966dc591b25a8976bc6a2","kind":"variable","name":"OTHER_LIMIT","qualified_name":"OTHER_LIMIT","file_path":"torture.rs","language":"rust","start_line":261,"end_line":261,"start_column":6,"end_column":17,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} -{"id":"variable:bb83e0760d206687873ef97791edf0b3","kind":"variable","name":"mdRemote","qualified_name":"mdRemote","file_path":"torture.go","language":"go","start_line":83,"end_line":83,"start_column":4,"end_column":46,"docstring":null,"signature":"= \"https://example.com/remote.md\"","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:bb8c8aae5a2d8e80d2b55a950d68bbc4","kind":"variable","name":"md_guide","qualified_name":"md_guide","file_path":"torture.lua","language":"lua","start_line":112,"end_line":112,"start_column":6,"end_column":14,"docstring":"Markdown path references: code -> documentation edges. Every shape the\nnormalizer branches on, so the two arms have to agree about the rejections\n(URL, escape above the root) as well as the emissions.","signature":"= \"../docs/guide.md#install\"","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:bd3123c2d8b795f8be0ffe79b04b4849","kind":"variable","name":"RETRY_MAX","qualified_name":"RETRY_MAX","file_path":"torture.rb","language":"ruby","start_line":11,"end_line":11,"start_column":0,"end_column":13,"docstring":null,"signature":"= 3","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:bf2954d02fcbda60d24c41d412ca4ee9","kind":"variable","name":"MD_ESCAPES","qualified_name":"MD_ESCAPES","file_path":"torture.rs","language":"rust","start_line":306,"end_line":306,"start_column":6,"end_column":16,"docstring":null,"signature":null,"visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} @@ -1783,8 +1794,8 @@ {"id":"variable:fb450996c12f335b36201ead67a34f64","kind":"variable","name":"INIT_MAP","qualified_name":"INIT_MAP","file_path":"torture.py","language":"python","start_line":54,"end_line":54,"start_column":0,"end_column":26,"docstring":null,"signature":"= {\"a\": helper()}","visibility":null,"is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:feb330d554c5d0caaa8978323a006e52","kind":"variable","name":"MD_BARE","qualified_name":"MD_BARE","file_path":"torture.rb","language":"ruby","start_line":238,"end_line":238,"start_column":0,"end_column":21,"docstring":null,"signature":"= 'README.md'","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"variable:fedacda34e94b85718ac655a74368326","kind":"variable","name":"okG","qualified_name":"okG","file_path":"torture.lua","language":"lua","start_line":12,"end_line":12,"start_column":6,"end_column":9,"docstring":null,"signature":"= pcall(require, \"app.guarded\")","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} -== edges (2380) -{"source":"class:0083dc18baa2b629e8572db20ba2b464","target":"class:25244cbdbe3e9bc720df9fc4d54fac0a","kind":"extends","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"qualified-name\",\"refName\":\"app::Config\"}","line":98,"col":35,"provenance":null} +== edges (2399) +{"source":"class:0083dc18baa2b629e8572db20ba2b464","target":"class:25244cbdbe3e9bc720df9fc4d54fac0a","kind":"extends","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"qualified-name\",\"refName\":\"app::Config\"}","line":98,"col":35,"provenance":null} {"source":"class:0083dc18baa2b629e8572db20ba2b464","target":"method:0795ac46f5bef3826c20039a05014ebc","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"class:0083dc18baa2b629e8572db20ba2b464","target":"method:35a476809e2727e80d7f2c8c404180b2","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"class:00e4c67aff13a714886744f6829e0cea","target":"constant:a5310d04cc7b8c2f1b1e86c6377b37ff","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} @@ -1921,8 +1932,8 @@ {"source":"class:54c0d1e485f298b4f9827706f928657f","target":"method:f11a1632c933108942e00d3b2c940adc","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"class:54c0d1e485f298b4f9827706f928657f","target":"trait:f91851c14912462ae9592cb13a51f3b1","kind":"extends","metadata":"{\"confidence\":0.4,\"resolvedBy\":\"exact-match\",\"refName\":\"Drawable\"}","line":42,"col":86,"provenance":null} {"source":"class:5502ac035e1ca2d56c14e093db0d8cec","target":"constant:918bdd1cc2ff2b2cd79d7560e2a8d3e4","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"class:5545c7494bbd6c81f0a1d85a3cf99570","target":"class:0939ad4dcc3f8b9686a6f2915ddc27f6","kind":"extends","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Base\"}","line":73,"col":23,"provenance":null} -{"source":"class:5545c7494bbd6c81f0a1d85a3cf99570","target":"class:25244cbdbe3e9bc720df9fc4d54fac0a","kind":"extends","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"qualified-name\",\"refName\":\"app::Config\"}","line":73,"col":42,"provenance":null} +{"source":"class:5545c7494bbd6c81f0a1d85a3cf99570","target":"class:0939ad4dcc3f8b9686a6f2915ddc27f6","kind":"extends","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"qualified-name\",\"refName\":\"Base\"}","line":73,"col":23,"provenance":null} +{"source":"class:5545c7494bbd6c81f0a1d85a3cf99570","target":"class:25244cbdbe3e9bc720df9fc4d54fac0a","kind":"extends","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"qualified-name\",\"refName\":\"app::Config\"}","line":73,"col":42,"provenance":null} {"source":"class:5545c7494bbd6c81f0a1d85a3cf99570","target":"method:3279340c86e9bdc524388fe76f0cf53c","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"class:5545c7494bbd6c81f0a1d85a3cf99570","target":"method:74ccbc991e3d062e15f1bd6d59f49361","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"class:555fa7a8e0dcaf3765cd04fadb6e6b7d","target":"constant:0949e99df00b827dfb914664fd66f38c","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} @@ -2099,7 +2110,7 @@ {"source":"class:a689d35e6a3cf7ce25db3ca75198b4e9","target":"class:fa355652331eee519e97b9ef15a61b2d","kind":"extends","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"WidgetT\"}","line":153,"col":23,"provenance":null} {"source":"class:a689d35e6a3cf7ce25db3ca75198b4e9","target":"method:e56906659412f1f464fdd82d5518f5de","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"class:a7406d8c57660dbb4fa7f571dbcdae30","target":"method:4da489ebd8c02fe5877b587c9b2c8b67","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"class:a7b34ad56c9fc903425f501a0cf34383","target":"class:0939ad4dcc3f8b9686a6f2915ddc27f6","kind":"extends","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Base\"}","line":59,"col":19,"provenance":null} +{"source":"class:a7b34ad56c9fc903425f501a0cf34383","target":"class:0939ad4dcc3f8b9686a6f2915ddc27f6","kind":"extends","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"qualified-name\",\"refName\":\"Base\"}","line":59,"col":19,"provenance":null} {"source":"class:a7b34ad56c9fc903425f501a0cf34383","target":"method:ca8bb20195aa616a66dba20985a35fe8","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"class:a7b34ad56c9fc903425f501a0cf34383","target":"method:d768a3f8cc0fe5ab53663ca7efd83d5f","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"class:a943b56b8518b75c9e40c82c8acdb80a","target":"method:cf35001d0df7e3da271058ffe461803e","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} @@ -3034,31 +3045,41 @@ {"source":"file:torture.dart","target":"type_alias:544f3be472935dfbe579fa190ead63ab","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:torture.dart","target":"type_alias:813393b308ca1d8f278ce99d55bf11d7","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:torture.go","target":"constant:0558b57204d51fcce0a73629df252447","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"file:torture.go","target":"function:013bc08b4895f4830b19ced189245d75","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"file:torture.go","target":"function:013bc08b4895f4830b19ced189245d75","kind":"references","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"function-ref\",\"refName\":\"TargetCb\",\"refKind\":\"function_ref\",\"fnRef\":true}","line":14,"col":9,"provenance":null} -{"source":"file:torture.go","target":"function:081d63a8e9c39d959398cdaa85b2188d","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"file:torture.go","target":"function:468f41958dcbb0ea1e0fd73b3eafab10","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"file:torture.go","target":"function:a1ea154d9e5e5d67a05afd07d2206001","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"file:torture.go","target":"function:b7ab4c33e93fae58f533755e245826ea","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"file:torture.go","target":"function:38bd0a279764bd368f0732f5abecc4f8","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"file:torture.go","target":"function:9f483e5d42c66f7b48795991a0de7f31","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"file:torture.go","target":"function:b2a0ca120e76741ae6bb879a2f0aea6f","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"file:torture.go","target":"function:d58344b89f92bcdaf87bfd139418e73d","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"file:torture.go","target":"function:e2ce1de9e98005e0d5b6bb7119c7978a","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"file:torture.go","target":"function:e2ce1de9e98005e0d5b6bb7119c7978a","kind":"references","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"function-ref\",\"refName\":\"TargetCb\",\"refKind\":\"function_ref\",\"fnRef\":true}","line":14,"col":9,"provenance":null} +{"source":"file:torture.go","target":"function:f0f9e8fdbfce4addf8bfac6085255496","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:torture.go","target":"import:17cf6081e0b98e781b1b457772f3dd34","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:torture.go","target":"import:64743408a7371afefc7768c546b83fb2","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:torture.go","target":"interface:6d9879a05b65ba3dcb91e66ab1e8b323","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:torture.go","target":"interface:b3282ee81a952dfabf759c45dabb86de","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:torture.go","target":"interface:cb62fc849789b030a200ee6cef9c98b0","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"file:torture.go","target":"method:7d8ded560e29b4ffe08aa4244f7ad020","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"file:torture.go","target":"method:e1f80971e6bc3a4e161d867b6a28844d","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"file:torture.go","target":"interface:eb24239549242ce0007b1ae6f6844c48","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"file:torture.go","target":"method:f04601d0017a75d44928a1341b1d2aa3","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"file:torture.go","target":"method:f8796162f1bd1fa332ec680173c6c80a","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"file:torture.go","target":"struct:182a7933759975da57345ac013fe7619","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:torture.go","target":"struct:1a02b04e584e146642ddb911b81ce419","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:torture.go","target":"struct:4c4abd243b1894739d7060ec73d2c7a0","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:torture.go","target":"type_alias:14815ab9ef5358d5c7df8b3c5c636ba0","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"file:torture.go","target":"type_alias:2eadd35ab3a35039204ee7e485c9d3f4","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"file:torture.go","target":"type_alias:5703c449ce310e218eeb262059f2b2cd","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"file:torture.go","target":"type_alias:8e43bdc4a5230913e6bb71478f4d3ce9","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"file:torture.go","target":"type_alias:90db322d3e0eb78357aa837fa1f92170","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"file:torture.go","target":"type_alias:d6c6403a980c3fa8265ad86510c9888f","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"file:torture.go","target":"type_alias:e443846c686bab2d9b4f68123bff60b2","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"file:torture.go","target":"type_alias:f009af79ecd90d8b4e99e21e99f148e8","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:torture.go","target":"variable:1837cd19ce5f7a8f45b7fa4b8fc8a0e6","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"file:torture.go","target":"variable:193a260abddbacd4b8812616870236ab","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"file:torture.go","target":"variable:3731c4860c0eac76475f173e3f52e850","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"file:torture.go","target":"variable:338a23f1f938ffd8fa2b896287ab699f","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"file:torture.go","target":"variable:53163f1d6bc66b7bc2c6979841ca1abc","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"file:torture.go","target":"variable:5491cc9401b25cc313dfe1dfeb28d23c","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:torture.go","target":"variable:6b7262d2883839c6f601126f4dd59e9b","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"file:torture.go","target":"variable:86594ca8f3b6dae35209d56363bcd5a7","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"file:torture.go","target":"variable:9567fda1765028e68645e39a624d61c7","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"file:torture.go","target":"variable:96073374f298db55700de2091389c955","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"file:torture.go","target":"variable:b0a4dbba6b6298349ca2c95d0d1a8136","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"file:torture.go","target":"variable:bb83e0760d206687873ef97791edf0b3","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"file:torture.go","target":"variable:84b3675fd4032c00792c256c0ba41496","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"file:torture.go","target":"variable:86bbaa5a32491e5a70aeb77a6373bdc7","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"file:torture.go","target":"variable:9efcdcca3961f25f73cb6ef8b0912799","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"file:torture.go","target":"variable:a862a8826882bccf9eeee7d2218227f7","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:torture.hpp","target":"class:da855a76803320806ba08457a72b59fa","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:torture.hpp","target":"function:a07d8ba50e32351d67a5892444ec6842","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:torture.hpp","target":"method:ca160b191e7fb257769f213a8450ab4c","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} @@ -3518,6 +3539,9 @@ {"source":"function:3452b9cd7da90dc21a02f4cf5d3558e5","target":"class:ca530cf4e2520283a176c1937131d988","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"ProviderT\"}","line":82,"col":18,"provenance":null} {"source":"function:3452b9cd7da90dc21a02f4cf5d3558e5","target":"method:8cc04f699d9a44f457ad849e48334cf2","kind":"calls","metadata":"{\"confidence\":0.85,\"resolvedBy\":\"qualified-name\",\"refName\":\"ProviderT.value\"}","line":82,"col":33,"provenance":null} {"source":"function:37c390abd50c0167008655a9d88fe073","target":"function:6f1b9bec32998e16cfe282d768bf4888","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"topFn\"}","line":50,"col":9,"provenance":null} +{"source":"function:38bd0a279764bd368f0732f5abecc4f8","target":"type_alias:90db322d3e0eb78357aa837fa1f92170","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Alias\"}","line":70,"col":17,"provenance":null} +{"source":"function:38bd0a279764bd368f0732f5abecc4f8","target":"type_alias:e443846c686bab2d9b4f68123bff60b2","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"LocalAlias\"}","line":71,"col":8,"provenance":null} +{"source":"function:38bd0a279764bd368f0732f5abecc4f8","target":"type_alias:e443846c686bab2d9b4f68123bff60b2","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"LocalAlias\"}","line":70,"col":24,"provenance":null} {"source":"function:38d7bae04a29fa8be9d0997ef3ccb42e","target":"constant:c16c1270c8b36ef2a45b0ab1edf2c720","kind":"references","metadata":"{\"valueRef\":true}","line":null,"col":null,"provenance":null} {"source":"function:3b5974da24c560449effd9d014e81123","target":"class:01a3945fa12793f1af9ac531aedcc8cd","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Widget\"}","line":6,"col":26,"provenance":null} {"source":"function:3cd744b2c3fc300e165d610ec46b61e9","target":"class:93144489f327d63399f919b71820325e","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Store\"}","line":29,"col":18,"provenance":null} @@ -3652,9 +3676,8 @@ {"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"method:7e714128b5b3c81a4e4f571a142197ba","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"qualified-name\",\"refName\":\"Widget\"}","line":46,"col":2,"provenance":null} {"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"method:f73f31bd6cc45134433935ad6eaae9f9","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"qualified-name\",\"refName\":\"Widget::Widget/2\"}","line":42,"col":2,"provenance":null} {"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"method:f73f31bd6cc45134433935ad6eaae9f9","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"qualified-name\",\"refName\":\"Widget::Widget/2\"}","line":43,"col":2,"provenance":null} -{"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"struct:dc1fb066a6c08cc705216a045fa93a24","kind":"instantiates","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Box\"}","line":45,"col":2,"provenance":null} +{"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"struct:dc1fb066a6c08cc705216a045fa93a24","kind":"instantiates","metadata":"{\"confidence\":0.4,\"resolvedBy\":\"exact-match\",\"refName\":\"Box\"}","line":45,"col":2,"provenance":null} {"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"struct:dcb8897c9696246c318260fb9188d7fb","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Aggregate\"}","line":38,"col":2,"provenance":null} -{"source":"function:b7ab4c33e93fae58f533755e245826ea","target":"struct:4c4abd243b1894739d7060ec73d2c7a0","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Widget\"}","line":49,"col":6,"provenance":null} {"source":"function:bb0cdf86bfb5dc9391a3d5fca445ebc2","target":"function:bb0cdf86bfb5dc9391a3d5fca445ebc2","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"tailer\"}","line":40,"col":53,"provenance":null} {"source":"function:bcafa05abfafe5a4b4bc5c8b3f7e3e0d","target":"class:fa355652331eee519e97b9ef15a61b2d","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"WidgetT\"}","line":50,"col":0,"provenance":null} {"source":"function:c062ca02de706ffe85e7dfb2f9586331","target":"class:c2dfee99ca5361d8de7ae7de6858d0bf","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"WidgetK\",\"refKind\":\"calls\"}","line":13,"col":11,"provenance":null} @@ -3676,6 +3699,7 @@ {"source":"function:e4aabfa896d12d328d860b0b6db8b41f","target":"function:ba9868d0ac07f6aadd40bfb18b1f232e","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"helper\"}","line":10,"col":8,"provenance":null} {"source":"function:e611a10f53cb2352067120211e7632b2","target":"class:54c0d1e485f298b4f9827706f928657f","kind":"instantiates","metadata":"{\"confidence\":0.4,\"resolvedBy\":\"exact-match\",\"refName\":\"WidgetS\"}","line":10,"col":32,"provenance":null} {"source":"function:e611a10f53cb2352067120211e7632b2","target":"class:54c0d1e485f298b4f9827706f928657f","kind":"references","metadata":"{\"confidence\":0.4,\"resolvedBy\":\"exact-match\",\"refName\":\"WidgetS\"}","line":10,"col":22,"provenance":null} +{"source":"function:f0f9e8fdbfce4addf8bfac6085255496","target":"struct:4c4abd243b1894739d7060ec73d2c7a0","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Widget\"}","line":75,"col":6,"provenance":null} {"source":"function:f11355267c15b025e65a025974c2a4d1","target":"constant:4d2266cb0f452fd0233976ec146c28eb","kind":"references","metadata":"{\"valueRef\":true}","line":null,"col":null,"provenance":null} {"source":"function:f11355267c15b025e65a025974c2a4d1","target":"constant:7af11b36e8d4ddbbdc72a75e5d163a1f","kind":"references","metadata":"{\"valueRef\":true}","line":null,"col":null,"provenance":null} {"source":"function:f42d18d9932ecf98835384d2a581205f","target":"function:e611a10f53cb2352067120211e7632b2","kind":"calls","metadata":"{\"confidence\":0.4,\"resolvedBy\":\"exact-match\",\"refName\":\"topLevel\"}","line":30,"col":26,"provenance":null} @@ -3701,6 +3725,7 @@ {"source":"interface:ac832273445e5a59ca5679d25afdcdc1","target":"property:41c90a4508dcf7006081c68981b6ae80","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"interface:cb62fc849789b030a200ee6cef9c98b0","target":"method:9310925a4f25f6bc4ce75ba243f7ede5","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"interface:cb62fc849789b030a200ee6cef9c98b0","target":"method:c9c8044a5ff012815e20baac8f5920fe","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"interface:eb24239549242ce0007b1ae6f6844c48","target":"method:1516d9f02e3a409a1257f9f857fa65ee","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"method:008f2ecd9ce97a5947e4c945b89814b6","target":"constant:eb6cd271fc2a02cd2b8d3cd99be4946b","kind":"decorates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"first\"}","line":61,"col":2,"provenance":null} {"source":"method:04144e2b7ad6716799068da09f298b79","target":"method:49ef12db2b51cf77b001a38cd46fe1a5","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"value\"}","line":14,"col":19,"provenance":null} {"source":"method:04a53200fc347c7f6addb793e5b4a417","target":"struct:0c309a8ed6f33456724d94ba73599e04","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Widget\"}","line":94,"col":32,"provenance":null} @@ -3715,6 +3740,7 @@ {"source":"method:14e2f2067b3db3d9c4de91be86bfae4d","target":"method:4e2dcce2a08ccc87afdd0c6b45fe1771","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"function-ref\",\"refName\":\"TortureService::compute\",\"refKind\":\"function_ref\",\"fnRef\":true}","line":39,"col":26,"provenance":null} {"source":"method:14e2f2067b3db3d9c4de91be86bfae4d","target":"method:56bad0d79d71c79e0a83b72148669a47","kind":"references","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"function-ref\",\"refName\":\"this.onEvent\",\"refKind\":\"function_ref\",\"fnRef\":true}","line":38,"col":19,"provenance":null} {"source":"method:14e2f2067b3db3d9c4de91be86bfae4d","target":"method:830203603e74b357be75b6353bc5377e","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"helper\"}","line":41,"col":23,"provenance":null} +{"source":"method:1516d9f02e3a409a1257f9f857fa65ee","target":"method:f8796162f1bd1fa332ec680173c6c80a","kind":"calls","metadata":"{\"synthesizedBy\":\"interface-impl\",\"via\":\"Render\",\"registeredAt\":\"torture.go:87\"}","line":65,"col":null,"provenance":"heuristic"} {"source":"method:1998d968a8cb7b7f76953859e2554fd2","target":"function:298f2af334b85aa54702a158c6583f1f","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"accept\"}","line":39,"col":37,"provenance":null} {"source":"method:1edca63b63663ee826e2349f6d040e1c","target":"method:64996a1706443006da9f4a15b38abbe2","kind":"calls","metadata":"{\"synthesizedBy\":\"interface-impl\",\"via\":\"render\",\"registeredAt\":\"torture.rs:117\"}","line":42,"col":null,"provenance":"heuristic"} {"source":"method:1edca63b63663ee826e2349f6d040e1c","target":"method:ee9f186763c4ce9337ce728d7b8c54e9","kind":"calls","metadata":"{\"synthesizedBy\":\"interface-impl\",\"via\":\"render\",\"registeredAt\":\"torture.rs:166\"}","line":42,"col":null,"provenance":"heuristic"} @@ -4117,13 +4143,14 @@ {"source":"struct:0c309a8ed6f33456724d94ba73599e04","target":"method:fc93d61b2caa73e9feea003464e6332d","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"struct:0c309a8ed6f33456724d94ba73599e04","target":"trait:a58fd1ab5cf02f43676433b760728c37","kind":"implements","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Render\"}","line":56,"col":5,"provenance":null} {"source":"struct:0c309a8ed6f33456724d94ba73599e04","target":"trait:dcec472700533c7d65fd2f90ea3c6830","kind":"implements","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Base\"}","line":163,"col":5,"provenance":null} -{"source":"struct:1a02b04e584e146642ddb911b81ce419","target":"method:7d8ded560e29b4ffe08aa4244f7ad020","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"struct:1a02b04e584e146642ddb911b81ce419","target":"method:f04601d0017a75d44928a1341b1d2aa3","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"struct:22cb841d7a3265379018f5b56364e98c","target":"method:ebf2f082376e23dff7ba2531c9a9d71f","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"struct:22cb841d7a3265379018f5b56364e98c","target":"method:ee9f186763c4ce9337ce728d7b8c54e9","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"struct:22cb841d7a3265379018f5b56364e98c","target":"trait:7cbd29a1473d80a74da70be5024bc1d6","kind":"implements","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Source\"}","line":140,"col":8,"provenance":null} {"source":"struct:22cb841d7a3265379018f5b56364e98c","target":"trait:a58fd1ab5cf02f43676433b760728c37","kind":"implements","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Render\"}","line":165,"col":8,"provenance":null} {"source":"struct:487d89e7a287a16bb02adabeb6c061a0","target":"property:13da6cd7fb88d8425ef100ac799b81ef","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"struct:4c4abd243b1894739d7060ec73d2c7a0","target":"method:e1f80971e6bc3a4e161d867b6a28844d","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"struct:4c4abd243b1894739d7060ec73d2c7a0","target":"interface:eb24239549242ce0007b1ae6f6844c48","kind":"implements","metadata":"{\"synthesizedBy\":\"go-implements\",\"via\":\"IfaceAlias\",\"registeredAt\":\"torture.go:17\"}","line":17,"col":null,"provenance":"heuristic"} +{"source":"struct:4c4abd243b1894739d7060ec73d2c7a0","target":"method:f8796162f1bd1fa332ec680173c6c80a","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"struct:4c4abd243b1894739d7060ec73d2c7a0","target":"struct:1a02b04e584e146642ddb911b81ce419","kind":"extends","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Stack\"}","line":22,"col":1,"provenance":null} {"source":"struct:4ea1b361173ba3f6b1b2e8dfe74e13d3","target":"method:b739f420ee12a433408dfb1f4a2911de","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"struct:68ba8bd4bb0864aeb77213901028683d","target":"field:67881ece80318447542caf9c533fa2c9","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} @@ -4152,19 +4179,22 @@ {"source":"trait:f91851c14912462ae9592cb13a51f3b1","target":"field:3ee68904db11f62059a076599040234d","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"trait:f91851c14912462ae9592cb13a51f3b1","target":"method:ef770437bcc4f4b3a1dbf17b8a1720a6","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"trait:f91851c14912462ae9592cb13a51f3b1","target":"method:fb3814b6e956f06c3746e1758bfb591d","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"type_alias:2eadd35ab3a35039204ee7e485c9d3f4","target":"struct:1a02b04e584e146642ddb911b81ce419","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Stack\"}","line":53,"col":15,"provenance":null} {"source":"type_alias:600473b4a3158649443b2f214febf906","target":"method:137fead1f10b4a2a010aa8787ef9782e","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"type_alias:600473b4a3158649443b2f214febf906","target":"method:5c2e8db84b29af68fc13a77bb8cbbc71","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"type_alias:600473b4a3158649443b2f214febf906","target":"property:e9465b48a55d84277b38665854459a7a","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} +{"source":"type_alias:8e43bdc4a5230913e6bb71478f4d3ce9","target":"struct:4c4abd243b1894739d7060ec73d2c7a0","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Widget\"}","line":54,"col":21,"provenance":null} {"source":"type_alias:cdaae8f951716ba1d5b3f5faa698cf1c","target":"interface:a05baa699fc9324830859524f09604c9","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Shape\"}","line":89,"col":25,"provenance":null} +{"source":"type_alias:e443846c686bab2d9b4f68123bff60b2","target":"struct:4c4abd243b1894739d7060ec73d2c7a0","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Widget\"}","line":52,"col":14,"provenance":null} {"source":"type_alias:f84ae3400c1d88837e6d67a42d5eea5a","target":"method:17231adc3d18205998834087d654a9b4","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"type_alias:f84ae3400c1d88837e6d67a42d5eea5a","target":"method:3694d7519adf9e129231e61b011b3738","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"union:87328bb244dc25e95ff76f307ddb5a57","target":"struct:aa7ce416b52984c7ea2b1b6d43e70da9","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"union:dca892bf5affbe24bc5af9c2fb3f133d","target":"trait:dcec472700533c7d65fd2f90ea3c6830","kind":"implements","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Base\"}","line":298,"col":5,"provenance":null} -{"source":"variable:1837cd19ce5f7a8f45b7fa4b8fc8a0e6","target":"function:b7ab4c33e93fae58f533755e245826ea","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"NewRegistry\"}","line":11,"col":22,"provenance":null} +{"source":"variable:1837cd19ce5f7a8f45b7fa4b8fc8a0e6","target":"function:f0f9e8fdbfce4addf8bfac6085255496","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"NewRegistry\"}","line":11,"col":22,"provenance":null} {"source":"variable:36e608fb368763b7f8a74a69f477cec7","target":"function:692077921d04b315880bc35a0bc872a9","kind":"references","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"function-ref\",\"refName\":\"view\",\"refKind\":\"function_ref\",\"fnRef\":true}","line":49,"col":24,"provenance":null} -{"source":"variable:6b7262d2883839c6f601126f4dd59e9b","target":"function:013bc08b4895f4830b19ced189245d75","kind":"references","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"function-ref\",\"refName\":\"TargetCb\",\"refKind\":\"function_ref\",\"fnRef\":true}","line":14,"col":9,"provenance":null} +{"source":"variable:6b7262d2883839c6f601126f4dd59e9b","target":"function:e2ce1de9e98005e0d5b6bb7119c7978a","kind":"references","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"function-ref\",\"refName\":\"TargetCb\",\"refKind\":\"function_ref\",\"fnRef\":true}","line":14,"col":9,"provenance":null} {"source":"variable:788d75a828c79f9847e0be0e2b73a196","target":"variable:8fef25ea8abe486ee94190ed47c3644b","kind":"references","metadata":"{\"valueRef\":true}","line":null,"col":null,"provenance":null} -== refs (1706) +== refs (1709) {"from_node_id":"class:01a3945fa12793f1af9ac531aedcc8cd","reference_name":"Mix2","reference_kind":"implements","line":34,"col":37,"candidates":null,"file_path":"TortureMini.dart","language":"dart","status":"failed","name_tail":"Mix2"} {"from_node_id":"class:01a3945fa12793f1af9ac531aedcc8cd","reference_name":"Paint","reference_kind":"implements","line":34,"col":59,"candidates":null,"file_path":"TortureMini.dart","language":"dart","status":"failed","name_tail":"Paint"} {"from_node_id":"class:027779b3581d9f4aee2aef4dbd5c8873","reference_name":"immutable","reference_kind":"decorates","line":144,"col":0,"candidates":null,"file_path":"TortureAnnotatedDocs.dart","language":"dart","status":"failed","name_tail":"immutable"} @@ -4322,7 +4352,7 @@ {"from_node_id":"component:7481b0bd84a22478679ecbebc3d262fb","reference_name":"useStateAlias","reference_kind":"calls","line":114,"col":28,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"useStateAlias"} {"from_node_id":"constant:030c0c1a5b7a383bafdd1693b847ff66","reference_name":"Report","reference_kind":"references","line":69,"col":30,"candidates":null,"file_path":"TortureReadsTypes.dart","language":"dart","status":"failed","name_tail":"Report"} {"from_node_id":"constant:0d769d307193815f098b9b0b5fbefe77","reference_name":"a.md","reference_kind":"references","line":189,"col":24,"candidates":null,"file_path":"torture.scala","language":"scala","status":"failed","name_tail":"md"} -{"from_node_id":"constant:0d769d307193815f098b9b0b5fbefe77","reference_name":"sub/b.mdx","reference_kind":"references","line":189,"col":38,"candidates":null,"file_path":"torture.scala","language":"scala","status":"failed","name_tail":"mdx"} +{"from_node_id":"constant:0d769d307193815f098b9b0b5fbefe77","reference_name":"sub/b.mdx","reference_kind":"references","line":189,"col":38,"candidates":null,"file_path":"torture.scala","language":"scala","status":"failed","name_tail":"b.mdx"} {"from_node_id":"constant:15f84901ff87cded61f155103e042937","reference_name":"loadConfig","reference_kind":"calls","line":91,"col":65,"candidates":null,"file_path":"torture.js","language":"javascript","status":"failed","name_tail":"loadConfig"} {"from_node_id":"constant:190c7c86b148186ed38f0157f9c3aebe","reference_name":"Map","reference_kind":"calls","line":4,"col":21,"candidates":null,"file_path":"TortureVref.scala","language":"scala","status":"failed","name_tail":"Map"} {"from_node_id":"constant:1d780ceba0f753b7c3c7fd7c16cd2ebd","reference_name":"Grade.a","reference_kind":"references","line":57,"col":33,"candidates":null,"file_path":"TortureReadsTypes.dart","language":"dart","status":"failed","name_tail":"a"} @@ -4330,32 +4360,32 @@ {"from_node_id":"constant:20c1adca9b006cfdbef3b2a842e8bb6d","reference_name":"README.md","reference_kind":"references","line":191,"col":36,"candidates":null,"file_path":"Torture.cs","language":"csharp","status":"failed","name_tail":"md"} {"from_node_id":"constant:22b2a846b667819d59226bf79b550e93","reference_name":"Formatter","reference_kind":"calls","line":71,"col":36,"candidates":null,"file_path":"TortureInitializers.dart","language":"dart","status":"failed","name_tail":"Formatter"} {"from_node_id":"constant:236c639ef8d468b8c5f0edbe3c511fe6","reference_name":"a.md","reference_kind":"references","line":225,"col":24,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"md"} -{"from_node_id":"constant:236c639ef8d468b8c5f0edbe3c511fe6","reference_name":"sub/b.mdx","reference_kind":"references","line":225,"col":38,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"mdx"} +{"from_node_id":"constant:236c639ef8d468b8c5f0edbe3c511fe6","reference_name":"sub/b.mdx","reference_kind":"references","line":225,"col":38,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"b.mdx"} {"from_node_id":"constant:24ff749e7e0b74dc051099e855860b0e","reference_name":"lazy","reference_kind":"calls","line":49,"col":20,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"lazy"} {"from_node_id":"constant:2ae565e022daebc1e27ec2ec6f991d63","reference_name":"persist","reference_kind":"calls","line":81,"col":24,"candidates":null,"file_path":"torture.js","language":"javascript","status":"failed","name_tail":"persist"} -{"from_node_id":"constant:2bb83d75d5c26493f735f88d54196889","reference_name":"example.com/remote.md","reference_kind":"references","line":223,"col":22,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"md"} +{"from_node_id":"constant:2bb83d75d5c26493f735f88d54196889","reference_name":"example.com/remote.md","reference_kind":"references","line":223,"col":22,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"remote.md"} {"from_node_id":"constant:2bd57e751463c5edb3066eee0c17581b","reference_name":"Counter","reference_kind":"calls","line":12,"col":76,"candidates":null,"file_path":"TortureInitializers.dart","language":"dart","status":"failed","name_tail":"Counter"} {"from_node_id":"constant:2bd57e751463c5edb3066eee0c17581b","reference_name":"Counter","reference_kind":"references","line":12,"col":46,"candidates":null,"file_path":"TortureInitializers.dart","language":"dart","status":"failed","name_tail":"Counter"} {"from_node_id":"constant:2bd57e751463c5edb3066eee0c17581b","reference_name":"StateNotifierProvider","reference_kind":"calls","line":12,"col":45,"candidates":null,"file_path":"TortureInitializers.dart","language":"dart","status":"failed","name_tail":"StateNotifierProvider"} {"from_node_id":"constant:2bd57e751463c5edb3066eee0c17581b","reference_name":"ref.read","reference_kind":"calls","line":12,"col":85,"candidates":null,"file_path":"TortureInitializers.dart","language":"dart","status":"failed","name_tail":"read"} {"from_node_id":"constant:2c9b8b9f0fc59dd9b622aa94a370dd2e","reference_name":"loadConfig","reference_kind":"calls","line":80,"col":65,"candidates":null,"file_path":"torture.js","language":"javascript","status":"failed","name_tail":"loadConfig"} {"from_node_id":"constant:2fb5597adb8d74ddf363fc76403671b1","reference_name":"notes.md#top","reference_kind":"references","line":195,"col":39,"candidates":null,"file_path":"Torture.cs","language":"csharp","status":"failed","name_tail":"md#top"} -{"from_node_id":"constant:3009d9f471eb7762bd124804705edd04","reference_name":"docs/rooted.md","reference_kind":"references","line":140,"col":32,"candidates":null,"file_path":"Torture.java","language":"java","status":"failed","name_tail":"md"} -{"from_node_id":"constant:33f910c46b7ceee638d2e27597b6efe9","reference_name":"example.com/remote.md","reference_kind":"references","line":187,"col":24,"candidates":null,"file_path":"torture.scala","language":"scala","status":"failed","name_tail":"md"} +{"from_node_id":"constant:3009d9f471eb7762bd124804705edd04","reference_name":"docs/rooted.md","reference_kind":"references","line":140,"col":32,"candidates":null,"file_path":"Torture.java","language":"java","status":"failed","name_tail":"rooted.md"} +{"from_node_id":"constant:33f910c46b7ceee638d2e27597b6efe9","reference_name":"example.com/remote.md","reference_kind":"references","line":187,"col":24,"candidates":null,"file_path":"torture.scala","language":"scala","status":"failed","name_tail":"remote.md"} {"from_node_id":"constant:342374b797067fd348d243baa5d427dc","reference_name":"compute","reference_kind":"calls","line":36,"col":19,"candidates":null,"file_path":"torture.scala","language":"scala","status":"failed","name_tail":"compute"} {"from_node_id":"constant:37538675d8cf7487a68e75423fac3308","reference_name":"Report","reference_kind":"references","line":33,"col":36,"candidates":null,"file_path":"TortureReadsTypes.dart","language":"dart","status":"failed","name_tail":"Report"} {"from_node_id":"constant:3c38de0e0ed3e748396ac32a56feb99b","reference_name":"Option","reference_kind":"calls","line":38,"col":22,"candidates":null,"file_path":"torture.scala","language":"scala","status":"failed","name_tail":"Option"} {"from_node_id":"constant:414592809f0c65f5ad4aabfee42415ce","reference_name":"DefaultThing","reference_kind":"function_ref","line":201,"col":31,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"DefaultThing"} {"from_node_id":"constant:414592809f0c65f5ad4aabfee42415ce","reference_name":"helperFn","reference_kind":"function_ref","line":201,"col":21,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"helperFn"} {"from_node_id":"constant:420a70ef479e25b430174d1d9f654e26","reference_name":"a.md","reference_kind":"references","line":285,"col":22,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"md"} -{"from_node_id":"constant:420a70ef479e25b430174d1d9f654e26","reference_name":"sub/b.mdx","reference_kind":"references","line":285,"col":36,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"mdx"} +{"from_node_id":"constant:420a70ef479e25b430174d1d9f654e26","reference_name":"sub/b.mdx","reference_kind":"references","line":285,"col":36,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"b.mdx"} {"from_node_id":"constant:43abe4e12f25b30df02f747f8f466c6b","reference_name":"Mode.values","reference_kind":"references","line":79,"col":46,"candidates":null,"file_path":"TortureInitializers.dart","language":"dart","status":"failed","name_tail":"values"} {"from_node_id":"constant:43abe4e12f25b30df02f747f8f466c6b","reference_name":"m.name","reference_kind":"references","line":79,"col":56,"candidates":null,"file_path":"TortureInitializers.dart","language":"dart","status":"failed","name_tail":"name"} {"from_node_id":"constant:457e5170ecaeb8b72d849f94c2b28591","reference_name":"Dio","reference_kind":"references","line":14,"col":14,"candidates":null,"file_path":"TortureInitializers.dart","language":"dart","status":"failed","name_tail":"Dio"} {"from_node_id":"constant:457e5170ecaeb8b72d849f94c2b28591","reference_name":"Dio.create","reference_kind":"calls","line":14,"col":24,"candidates":null,"file_path":"TortureInitializers.dart","language":"dart","status":"failed","name_tail":"create"} {"from_node_id":"constant:457e5170ecaeb8b72d849f94c2b28591","reference_name":"Provider","reference_kind":"calls","line":13,"col":30,"candidates":null,"file_path":"TortureInitializers.dart","language":"dart","status":"failed","name_tail":"Provider"} {"from_node_id":"constant:457e5170ecaeb8b72d849f94c2b28591","reference_name":"Repository","reference_kind":"calls","line":19,"col":19,"candidates":null,"file_path":"TortureInitializers.dart","language":"dart","status":"failed","name_tail":"Repository"} -{"from_node_id":"constant:49387da9bb358cb2519ea5c794bc8273","reference_name":"example.com/remote.md","reference_kind":"references","line":194,"col":44,"candidates":null,"file_path":"Torture.cs","language":"csharp","status":"failed","name_tail":"md"} +{"from_node_id":"constant:49387da9bb358cb2519ea5c794bc8273","reference_name":"example.com/remote.md","reference_kind":"references","line":194,"col":44,"candidates":null,"file_path":"Torture.cs","language":"csharp","status":"failed","name_tail":"remote.md"} {"from_node_id":"constant:4e23875b14fe177abec1c4fd28d56767","reference_name":"Provider","reference_kind":"calls","line":11,"col":29,"candidates":null,"file_path":"TortureInitializers.dart","language":"dart","status":"failed","name_tail":"Provider"} {"from_node_id":"constant:4e23875b14fe177abec1c4fd28d56767","reference_name":"Repository","reference_kind":"calls","line":11,"col":49,"candidates":null,"file_path":"TortureInitializers.dart","language":"dart","status":"failed","name_tail":"Repository"} {"from_node_id":"constant:4e23875b14fe177abec1c4fd28d56767","reference_name":"ref.watch","reference_kind":"calls","line":11,"col":59,"candidates":null,"file_path":"TortureInitializers.dart","language":"dart","status":"failed","name_tail":"watch"} @@ -4368,29 +4398,29 @@ {"from_node_id":"constant:6260e4d373836585ddb7c21a68324634","reference_name":"Map","reference_kind":"references","line":39,"col":37,"candidates":null,"file_path":"torture.scala","language":"scala","status":"failed","name_tail":"Map"} {"from_node_id":"constant:65aeaafee15c4794fe90f90c519d51d7","reference_name":"mutableListOf","reference_kind":"calls","line":144,"col":20,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"mutableListOf"} {"from_node_id":"constant:6633f56632bf79de51cf2e85f760be18","reference_name":"Mode.fast","reference_kind":"references","line":80,"col":31,"candidates":null,"file_path":"TortureInitializers.dart","language":"dart","status":"failed","name_tail":"fast"} -{"from_node_id":"constant:680fa2a8959b8b037053fe513f9549dc","reference_name":"docs/rooted.md","reference_kind":"references","line":221,"col":16,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"md"} +{"from_node_id":"constant:680fa2a8959b8b037053fe513f9549dc","reference_name":"docs/rooted.md","reference_kind":"references","line":221,"col":16,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"rooted.md"} {"from_node_id":"constant:6d91b4873c686cb14ad3f24f75fcbda8","reference_name":"notes.md#top","reference_kind":"references","line":143,"col":33,"candidates":null,"file_path":"Torture.java","language":"java","status":"failed","name_tail":"md#top"} {"from_node_id":"constant:6e22766403b32f1b365a165c4a34732c","reference_name":"compute","reference_kind":"calls","line":4,"col":15,"candidates":null,"file_path":"TortureIndent.scala","language":"scala","status":"failed","name_tail":"compute"} {"from_node_id":"constant:6e57d225740696938e1739625e94d243","reference_name":"FutureProvider","reference_kind":"calls","line":33,"col":36,"candidates":null,"file_path":"TortureInitializers.dart","language":"dart","status":"failed","name_tail":"FutureProvider"} {"from_node_id":"constant:6e57d225740696938e1739625e94d243","reference_name":"load","reference_kind":"calls","line":33,"col":62,"candidates":null,"file_path":"TortureInitializers.dart","language":"dart","status":"failed","name_tail":"load"} -{"from_node_id":"constant:71c6d1663daf7a2ededf66a82d0721fd","reference_name":"example.com/remote.md","reference_kind":"references","line":240,"col":24,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"md"} -{"from_node_id":"constant:72a8c16d222a91fd63387b8e91e84692","reference_name":"docs/rooted.md","reference_kind":"references","line":281,"col":16,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"md"} +{"from_node_id":"constant:71c6d1663daf7a2ededf66a82d0721fd","reference_name":"example.com/remote.md","reference_kind":"references","line":240,"col":24,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"remote.md"} +{"from_node_id":"constant:72a8c16d222a91fd63387b8e91e84692","reference_name":"docs/rooted.md","reference_kind":"references","line":281,"col":16,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"rooted.md"} {"from_node_id":"constant:7da4836f633252abd079cf61390f496c","reference_name":"notes.md#top","reference_kind":"references","line":284,"col":17,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"md#top"} {"from_node_id":"constant:7fab581a17cbf8b49300b7d3c531a7b3","reference_name":"notes.md#top","reference_kind":"references","line":224,"col":17,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"md#top"} -{"from_node_id":"constant:8276b2a2a2646d6b34d53d6a2b120077","reference_name":"docs/rooted.md","reference_kind":"references","line":238,"col":18,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"md"} +{"from_node_id":"constant:8276b2a2a2646d6b34d53d6a2b120077","reference_name":"docs/rooted.md","reference_kind":"references","line":238,"col":18,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"rooted.md"} {"from_node_id":"constant:8d5b7c0112ba1bd860a9b742a094954c","reference_name":"Report","reference_kind":"references","line":71,"col":17,"candidates":null,"file_path":"TortureReadsTypes.dart","language":"dart","status":"failed","name_tail":"Report"} {"from_node_id":"constant:8d5b7c0112ba1bd860a9b742a094954c","reference_name":"r.score","reference_kind":"references","line":71,"col":32,"candidates":null,"file_path":"TortureReadsTypes.dart","language":"dart","status":"failed","name_tail":"score"} -{"from_node_id":"constant:9094965c27fc1c253476c66075eb8df3","reference_name":"example.com/remote.md","reference_kind":"references","line":142,"col":38,"candidates":null,"file_path":"Torture.java","language":"java","status":"failed","name_tail":"md"} +{"from_node_id":"constant:9094965c27fc1c253476c66075eb8df3","reference_name":"example.com/remote.md","reference_kind":"references","line":142,"col":38,"candidates":null,"file_path":"Torture.java","language":"java","status":"failed","name_tail":"remote.md"} {"from_node_id":"constant:90ff2b973c8733cb433a85de5fb3561f","reference_name":"a.md","reference_kind":"references","line":242,"col":24,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"md"} -{"from_node_id":"constant:90ff2b973c8733cb433a85de5fb3561f","reference_name":"sub/b.mdx","reference_kind":"references","line":242,"col":38,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"mdx"} -{"from_node_id":"constant:91ebf034ebc4447c5457d0b572231ae8","reference_name":"docs/rooted.md","reference_kind":"references","line":185,"col":18,"candidates":null,"file_path":"torture.scala","language":"scala","status":"failed","name_tail":"md"} +{"from_node_id":"constant:90ff2b973c8733cb433a85de5fb3561f","reference_name":"sub/b.mdx","reference_kind":"references","line":242,"col":38,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"b.mdx"} +{"from_node_id":"constant:91ebf034ebc4447c5457d0b572231ae8","reference_name":"docs/rooted.md","reference_kind":"references","line":185,"col":18,"candidates":null,"file_path":"torture.scala","language":"scala","status":"failed","name_tail":"rooted.md"} {"from_node_id":"constant:9a3214b29eeb9ed5ba3d9b08e80d3936","reference_name":"Provider","reference_kind":"calls","line":32,"col":39,"candidates":null,"file_path":"TortureInitializers.dart","language":"dart","status":"failed","name_tail":"Provider"} -{"from_node_id":"constant:9d99c4353a441bb15265d3940df538f6","reference_name":"example.com/remote.md","reference_kind":"references","line":283,"col":22,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"md"} +{"from_node_id":"constant:9d99c4353a441bb15265d3940df538f6","reference_name":"example.com/remote.md","reference_kind":"references","line":283,"col":22,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"remote.md"} {"from_node_id":"constant:9ef5f2100c2250165378d22671ea7a78","reference_name":"Integer","reference_kind":"references","line":20,"col":34,"candidates":null,"file_path":"Torture.java","language":"java","status":"failed","name_tail":"Integer"} {"from_node_id":"constant:9ef5f2100c2250165378d22671ea7a78","reference_name":"Map","reference_kind":"references","line":20,"col":22,"candidates":null,"file_path":"Torture.java","language":"java","status":"failed","name_tail":"Map"} {"from_node_id":"constant:9ef5f2100c2250165378d22671ea7a78","reference_name":"Map.of","reference_kind":"calls","line":20,"col":58,"candidates":null,"file_path":"Torture.java","language":"java","status":"failed","name_tail":"of","failure_reason":"unknown-receiver"} {"from_node_id":"constant:a0db7b9c7b13b7e8daab04b2fc6cf190","reference_name":"a.md","reference_kind":"references","line":196,"col":44,"candidates":null,"file_path":"Torture.cs","language":"csharp","status":"failed","name_tail":"md"} -{"from_node_id":"constant:a0db7b9c7b13b7e8daab04b2fc6cf190","reference_name":"sub/b.mdx","reference_kind":"references","line":196,"col":58,"candidates":null,"file_path":"Torture.cs","language":"csharp","status":"failed","name_tail":"mdx"} +{"from_node_id":"constant:a0db7b9c7b13b7e8daab04b2fc6cf190","reference_name":"sub/b.mdx","reference_kind":"references","line":196,"col":58,"candidates":null,"file_path":"Torture.cs","language":"csharp","status":"failed","name_tail":"b.mdx"} {"from_node_id":"constant:a25c4edb62c879640812b8058257670d","reference_name":"Dio","reference_kind":"references","line":9,"col":23,"candidates":null,"file_path":"TortureGenericCalls.dart","language":"dart","status":"failed","name_tail":"Dio"} {"from_node_id":"constant:a25c4edb62c879640812b8058257670d","reference_name":"Provider","reference_kind":"calls","line":8,"col":29,"candidates":null,"file_path":"TortureGenericCalls.dart","language":"dart","status":"failed","name_tail":"Provider"} {"from_node_id":"constant:a25c4edb62c879640812b8058257670d","reference_name":"Repo","reference_kind":"calls","line":10,"col":13,"candidates":null,"file_path":"TortureGenericCalls.dart","language":"dart","status":"failed","name_tail":"Repo"} @@ -4410,7 +4440,7 @@ {"from_node_id":"constant:cf5aaa64e9d0f2840dff763fee34a693","reference_name":"Report","reference_kind":"calls","line":32,"col":39,"candidates":null,"file_path":"TortureReadsTypes.dart","language":"dart","status":"failed","name_tail":"Report"} {"from_node_id":"constant:d797d33211e7d974d31ada081051cdc6","reference_name":"loadConfig","reference_kind":"calls","line":79,"col":20,"candidates":null,"file_path":"torture.js","language":"javascript","status":"failed","name_tail":"loadConfig"} {"from_node_id":"constant:df995faec9b02592a89041c7b6e7e09a","reference_name":"a.md","reference_kind":"references","line":144,"col":40,"candidates":null,"file_path":"Torture.java","language":"java","status":"failed","name_tail":"md"} -{"from_node_id":"constant:df995faec9b02592a89041c7b6e7e09a","reference_name":"sub/b.mdx","reference_kind":"references","line":144,"col":54,"candidates":null,"file_path":"Torture.java","language":"java","status":"failed","name_tail":"mdx"} +{"from_node_id":"constant:df995faec9b02592a89041c7b6e7e09a","reference_name":"sub/b.mdx","reference_kind":"references","line":144,"col":54,"candidates":null,"file_path":"Torture.java","language":"java","status":"failed","name_tail":"b.mdx"} {"from_node_id":"constant:e80284ed7855d53eda8dcc27f6284227","reference_name":"Widget.factory","reference_kind":"calls","line":24,"col":30,"candidates":null,"file_path":"TortureInitializers.dart","language":"dart","status":"failed","name_tail":"factory"} {"from_node_id":"constant:e80284ed7855d53eda8dcc27f6284227","reference_name":"Widget.factory().configure","reference_kind":"calls","line":24,"col":42,"candidates":null,"file_path":"TortureInitializers.dart","language":"dart","status":"failed","name_tail":"configure"} {"from_node_id":"constant:e80284ed7855d53eda8dcc27f6284227","reference_name":"run","reference_kind":"calls","line":24,"col":48,"candidates":null,"file_path":"TortureInitializers.dart","language":"dart","status":"failed","name_tail":"run"} @@ -4420,7 +4450,7 @@ {"from_node_id":"constant:f1c760c1c252d6bb4d7ecdf1da9345b2","reference_name":"ref.watch","reference_kind":"calls","line":34,"col":53,"candidates":null,"file_path":"TortureInitializers.dart","language":"dart","status":"failed","name_tail":"watch"} {"from_node_id":"constant:fa1e602a772096a45d5d461885e4eecb","reference_name":"Home","reference_kind":"calls","line":13,"col":56,"candidates":null,"file_path":"TortureGenericCalls.dart","language":"dart","status":"failed","name_tail":"Home"} {"from_node_id":"constant:fa1e602a772096a45d5d461885e4eecb","reference_name":"MaterialPageRoute","reference_kind":"calls","line":13,"col":33,"candidates":null,"file_path":"TortureGenericCalls.dart","language":"dart","status":"failed","name_tail":"MaterialPageRoute"} -{"from_node_id":"constant:fcb85d08e560bb97a5a04042c6c88b55","reference_name":"docs/rooted.md","reference_kind":"references","line":192,"col":38,"candidates":null,"file_path":"Torture.cs","language":"csharp","status":"failed","name_tail":"md"} +{"from_node_id":"constant:fcb85d08e560bb97a5a04042c6c88b55","reference_name":"docs/rooted.md","reference_kind":"references","line":192,"col":38,"candidates":null,"file_path":"Torture.cs","language":"csharp","status":"failed","name_tail":"rooted.md"} {"from_node_id":"enum:185db2f09770db6e298b5452894a6826","reference_name":"Grade","reference_kind":"references","line":57,"col":15,"candidates":null,"file_path":"TortureReadsTypes.dart","language":"dart","status":"failed","name_tail":"Grade"} {"from_node_id":"enum:185db2f09770db6e298b5452894a6826","reference_name":"Report","reference_kind":"references","line":56,"col":8,"candidates":null,"file_path":"TortureReadsTypes.dart","language":"dart","status":"failed","name_tail":"Report"} {"from_node_id":"enum:1bea39061b126572ffb641ee6a98eab8","reference_name":"CustomStringConvertible","reference_kind":"extends","line":44,"col":21,"candidates":null,"file_path":"torture.swift","language":"swift","status":"failed","name_tail":"CustomStringConvertible"} @@ -4532,10 +4562,10 @@ {"from_node_id":"file:torture.R","reference_name":"do.call","reference_kind":"calls","line":172,"col":0,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"call"} {"from_node_id":"file:torture.R","reference_name":"do.call","reference_kind":"calls","line":173,"col":0,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"call"} {"from_node_id":"file:torture.R","reference_name":"docpkg","reference_kind":"imports","line":65,"col":0,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"docpkg"} -{"from_node_id":"file:torture.R","reference_name":"docs/rooted.md","reference_kind":"references","line":212,"col":14,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"md"} +{"from_node_id":"file:torture.R","reference_name":"docs/rooted.md","reference_kind":"references","line":212,"col":14,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"rooted.md"} {"from_node_id":"file:torture.R","reference_name":"dplyr","reference_kind":"imports","line":56,"col":0,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"dplyr"} {"from_node_id":"file:torture.R","reference_name":"environment","reference_kind":"calls","line":151,"col":46,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"environment"} -{"from_node_id":"file:torture.R","reference_name":"example.com/remote.md","reference_kind":"references","line":214,"col":20,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"md"} +{"from_node_id":"file:torture.R","reference_name":"example.com/remote.md","reference_kind":"references","line":214,"col":20,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"remote.md"} {"from_node_id":"file:torture.R","reference_name":"f_no","reference_kind":"calls","line":161,"col":33,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"f_no"} {"from_node_id":"file:torture.R","reference_name":"f_ph","reference_kind":"calls","line":200,"col":14,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"f_ph"} {"from_node_id":"file:torture.R","reference_name":"f_yes","reference_kind":"calls","line":161,"col":20,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"f_yes"} @@ -4578,7 +4608,7 @@ {"from_node_id":"file:torture.R","reference_name":"stats","reference_kind":"imports","line":57,"col":0,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"stats"} {"from_node_id":"file:torture.R","reference_name":"step_once","reference_kind":"calls","line":163,"col":21,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"step_once"} {"from_node_id":"file:torture.R","reference_name":"stopifnot","reference_kind":"calls","line":175,"col":0,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"stopifnot"} -{"from_node_id":"file:torture.R","reference_name":"sub/b.mdx","reference_kind":"references","line":216,"col":36,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"mdx"} +{"from_node_id":"file:torture.R","reference_name":"sub/b.mdx","reference_kind":"references","line":216,"col":36,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"b.mdx"} {"from_node_id":"file:torture.R","reference_name":"suppressPackageStartupMessages","reference_kind":"calls","line":63,"col":0,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"suppressPackageStartupMessages"} {"from_node_id":"file:torture.R","reference_name":"tools","reference_kind":"imports","line":59,"col":0,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"tools"} {"from_node_id":"file:torture.R","reference_name":"top_call","reference_kind":"calls","line":50,"col":0,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"top_call"} @@ -4591,7 +4621,7 @@ {"from_node_id":"file:torture.cpp","reference_name":"vector","reference_kind":"imports","line":8,"col":0,"candidates":null,"file_path":"torture.cpp","language":"cpp","status":"failed","name_tail":"vector"} {"from_node_id":"file:torture.cpp","reference_name":"widget_base.hpp","reference_kind":"imports","line":9,"col":0,"candidates":null,"file_path":"torture.cpp","language":"cpp","status":"failed","name_tail":"hpp"} {"from_node_id":"file:torture.dart","reference_name":"dart:async","reference_kind":"imports","line":2,"col":0,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"async"} -{"from_node_id":"file:torture.dart","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":245,"col":11,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"markdown"} +{"from_node_id":"file:torture.dart","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":245,"col":11,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"nested.markdown"} {"from_node_id":"file:torture.dart","reference_name":"package:torture/other.dart","reference_kind":"imports","line":3,"col":0,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"other"} {"from_node_id":"file:torture.dart","reference_name":"src/reexported.dart","reference_kind":"imports","line":4,"col":0,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"reexported"} {"from_node_id":"file:torture.dart","reference_name":"torture_part.dart","reference_kind":"imports","line":5,"col":0,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"dart"} @@ -4678,7 +4708,7 @@ {"from_node_id":"file:torture.tsx","reference_name":"NS","reference_kind":"imports","line":5,"col":12,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"NS"} {"from_node_id":"file:torture.tsx","reference_name":"NS.helper.deep","reference_kind":"calls","line":207,"col":0,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"deep","failure_reason":"unknown-receiver"} {"from_node_id":"file:torture.tsx","reference_name":"React","reference_kind":"imports","line":4,"col":7,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"React"} -{"from_node_id":"file:torture.tsx","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":226,"col":9,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"markdown"} +{"from_node_id":"file:torture.tsx","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":226,"col":9,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"nested.markdown"} {"from_node_id":"file:torture.tsx","reference_name":"forwardRef","reference_kind":"imports","line":4,"col":16,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"forwardRef"} {"from_node_id":"file:torture.tsx","reference_name":"helperFn","reference_kind":"function_ref","line":198,"col":16,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"helperFn"} {"from_node_id":"file:torture.tsx","reference_name":"helperFn","reference_kind":"imports","line":8,"col":9,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"helperFn"} @@ -4706,8 +4736,6 @@ {"from_node_id":"function:0499cc5aef87518260d547d716f829ce","reference_name":"fetchWidget","reference_kind":"calls","line":173,"col":29,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"fetchWidget"} {"from_node_id":"function:04b831d017f75cc1ee6393a733c8ffad","reference_name":"x","reference_kind":"decorates","line":33,"col":0,"candidates":null,"file_path":"TortureAnnotatedDocs.dart","language":"dart","status":"failed","name_tail":"x"} {"from_node_id":"function:077abb142cd38f50fd7aeb4958ac54b7","reference_name":"Object","reference_kind":"references","line":7,"col":14,"candidates":null,"file_path":"TortureInitializers.dart","language":"dart","status":"failed","name_tail":"Object"} -{"from_node_id":"function:081d63a8e9c39d959398cdaa85b2188d","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":88,"col":10,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"markdown"} -{"from_node_id":"function:081d63a8e9c39d959398cdaa85b2188d","reference_name":"loadDoc","reference_kind":"calls","line":88,"col":1,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"loadDoc"} {"from_node_id":"function:08bb60eead5364c3bf26d67ebca2639d","reference_name":"List","reference_kind":"references","line":21,"col":13,"candidates":null,"file_path":"TortureSigs.dart","language":"dart","status":"failed","name_tail":"List"} {"from_node_id":"function:08bb60eead5364c3bf26d67ebca2639d","reference_name":"a","reference_kind":"function_ref","line":24,"col":11,"candidates":null,"file_path":"TortureSigs.dart","language":"dart","status":"failed","name_tail":"a"} {"from_node_id":"function:08bb60eead5364c3bf26d67ebca2639d","reference_name":"f","reference_kind":"calls","line":26,"col":3,"candidates":null,"file_path":"TortureSigs.dart","language":"dart","status":"failed","name_tail":"f"} @@ -4732,7 +4760,7 @@ {"from_node_id":"function:11c7b584c938a133a0a90b82afcafbd9","reference_name":"emit","reference_kind":"calls","line":60,"col":6,"candidates":null,"file_path":"TortureVrefDart.dart","language":"dart","status":"failed","name_tail":"emit"} {"from_node_id":"function:141b9348d2428090cbb42de6932b54ad","reference_name":"visibleForTesting","reference_kind":"decorates","line":7,"col":0,"candidates":null,"file_path":"TortureAnnotatedComments.dart","language":"dart","status":"failed","name_tail":"visibleForTesting"} {"from_node_id":"function:15790b08cab7e8d7cb5b8a474d94c359","reference_name":"draw","reference_kind":"calls","line":253,"col":4,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"draw"} -{"from_node_id":"function:1591d6baa7249cbfb88bb162e736cb3e","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":217,"col":32,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"markdown"} +{"from_node_id":"function:1591d6baa7249cbfb88bb162e736cb3e","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":217,"col":32,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"nested.markdown"} {"from_node_id":"function:1591d6baa7249cbfb88bb162e736cb3e","reference_name":"load_doc","reference_kind":"calls","line":217,"col":22,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"load_doc"} {"from_node_id":"function:1632d6a8b93e164a76e86681614d7ba5","reference_name":"visibleForTesting","reference_kind":"decorates","line":7,"col":0,"candidates":null,"file_path":"TortureAnnotatedDocs.dart","language":"dart","status":"failed","name_tail":"visibleForTesting"} {"from_node_id":"function:178c6f5073dbe3085b9a39b8c2723861","reference_name":"standardGeneric","reference_kind":"calls","line":78,"col":37,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"standardGeneric"} @@ -4773,7 +4801,7 @@ {"from_node_id":"function:21ca45d9f9601839f9b8698f898ae7d9","reference_name":"inner","reference_kind":"calls","line":11,"col":9,"candidates":null,"file_path":"TortureDoubleWalk.dart","language":"dart","status":"failed","name_tail":"inner"} {"from_node_id":"function:21ca45d9f9601839f9b8698f898ae7d9","reference_name":"inner","reference_kind":"calls","line":11,"col":9,"candidates":null,"file_path":"TortureDoubleWalk.dart","language":"dart","status":"failed","name_tail":"inner"} {"from_node_id":"function:226a27fbd09f9beffd734a69fb7bfbd2","reference_name":"cmpTop","reference_kind":"calls","line":9,"col":39,"candidates":null,"file_path":"TortureGiven.scala","language":"scala","status":"failed","name_tail":"cmpTop"} -{"from_node_id":"function:22cb108e128e477e7df96f3f8d7aa0fc","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":312,"col":14,"candidates":null,"file_path":"torture.rs","language":"rust","status":"failed","name_tail":"markdown"} +{"from_node_id":"function:22cb108e128e477e7df96f3f8d7aa0fc","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":312,"col":14,"candidates":null,"file_path":"torture.rs","language":"rust","status":"failed","name_tail":"nested.markdown"} {"from_node_id":"function:22cb108e128e477e7df96f3f8d7aa0fc","reference_name":"load_doc","reference_kind":"calls","line":312,"col":4,"candidates":null,"file_path":"torture.rs","language":"rust","status":"failed","name_tail":"load_doc"} {"from_node_id":"function:244501f608630feb3bec114d937eb7a6","reference_name":"foo_single","reference_kind":"function_ref","line":255,"col":11,"candidates":null,"file_path":"torture.rs","language":"rust","status":"failed","name_tail":"foo_single"} {"from_node_id":"function:244501f608630feb3bec114d937eb7a6","reference_name":"invoke","reference_kind":"calls","line":255,"col":4,"candidates":null,"file_path":"torture.rs","language":"rust","status":"failed","name_tail":"invoke"} @@ -4839,7 +4867,7 @@ {"from_node_id":"function:434fc66cc90ef59c01466d52e147b60a","reference_name":"reg2","reference_kind":"calls","line":24,"col":6,"candidates":null,"file_path":"TortureFnrefDart.dart","language":"dart","status":"failed","name_tail":"reg2"} {"from_node_id":"function:434fc66cc90ef59c01466d52e147b60a","reference_name":"reg3","reference_kind":"calls","line":25,"col":6,"candidates":null,"file_path":"TortureFnrefDart.dart","language":"dart","status":"failed","name_tail":"reg3"} {"from_node_id":"function:43a5e747caf89cee51f00fde5cba14f6","reference_name":"x","reference_kind":"decorates","line":36,"col":19,"candidates":null,"file_path":"TortureAnnotatedDocs.dart","language":"dart","status":"failed","name_tail":"x"} -{"from_node_id":"function:44b4a8cf45d87bc437f1a71cb9d95ab9","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":183,"col":38,"candidates":null,"file_path":"torture.c","language":"c","status":"failed","name_tail":"markdown"} +{"from_node_id":"function:44b4a8cf45d87bc437f1a71cb9d95ab9","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":183,"col":38,"candidates":null,"file_path":"torture.c","language":"c","status":"failed","name_tail":"nested.markdown"} {"from_node_id":"function:44b4a8cf45d87bc437f1a71cb9d95ab9","reference_name":"load_doc","reference_kind":"calls","line":183,"col":28,"candidates":null,"file_path":"torture.c","language":"c","status":"failed","name_tail":"load_doc"} {"from_node_id":"function:4660fc91e385ee68f62141cd432fb335","reference_name":"use","reference_kind":"calls","line":29,"col":5,"candidates":null,"file_path":"TortureVrefDart.dart","language":"dart","status":"failed","name_tail":"use"} {"from_node_id":"function:4759298ab8dfe6cd0b2c0574d59fdde3","reference_name":"singleton_body_call","reference_kind":"calls","line":229,"col":2,"candidates":null,"file_path":"torture.rb","language":"ruby","status":"failed","name_tail":"singleton_body_call"} @@ -4856,17 +4884,17 @@ {"from_node_id":"function:4ebd50cc2fbf98c17587f1065d31ca78","reference_name":"T","reference_kind":"references","line":24,"col":26,"candidates":null,"file_path":"torture.scala","language":"scala","status":"failed","name_tail":"T"} {"from_node_id":"function:4f9a35dcf41dd8fc9d6fcec26c387fa5","reference_name":"Future","reference_kind":"references","line":42,"col":0,"candidates":null,"file_path":"TortureDoubleWalk.dart","language":"dart","status":"failed","name_tail":"Future"} {"from_node_id":"function:50887fc196113b991912647e10d8fd13","reference_name":"fetch","reference_kind":"calls","line":190,"col":2,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"fetch"} -{"from_node_id":"function:509abafa7a26567611927a7a4fbb4429","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":288,"col":13,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"markdown"} +{"from_node_id":"function:509abafa7a26567611927a7a4fbb4429","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":288,"col":13,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"nested.markdown"} {"from_node_id":"function:509abafa7a26567611927a7a4fbb4429","reference_name":"loadDoc","reference_kind":"calls","line":288,"col":4,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"loadDoc"} {"from_node_id":"function:521a5bea2bed7202dcb40e666d7bcfb2","reference_name":"use_val","reference_kind":"calls","line":125,"col":37,"candidates":null,"file_path":"torture.c","language":"c","status":"failed","name_tail":"use_val"} -{"from_node_id":"function:522d9f8bb2b74fbe8b74a3a07afd73b8","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":234,"col":13,"candidates":null,"file_path":"torture.php","language":"php","status":"failed","name_tail":"markdown"} +{"from_node_id":"function:522d9f8bb2b74fbe8b74a3a07afd73b8","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":234,"col":13,"candidates":null,"file_path":"torture.php","language":"php","status":"failed","name_tail":"nested.markdown"} {"from_node_id":"function:522d9f8bb2b74fbe8b74a3a07afd73b8","reference_name":"loadDoc","reference_kind":"calls","line":234,"col":4,"candidates":null,"file_path":"torture.php","language":"php","status":"failed","name_tail":"loadDoc"} {"from_node_id":"function:5346ad53a982756298bce064d4466e98","reference_name":"Iterable","reference_kind":"references","line":46,"col":0,"candidates":null,"file_path":"TortureDoubleWalk.dart","language":"dart","status":"failed","name_tail":"Iterable"} {"from_node_id":"function:55dc9b62ca2dd87561c5e882ec386c0c","reference_name":"override","reference_kind":"decorates","line":52,"col":0,"candidates":null,"file_path":"TortureVrefDart.dart","language":"dart","status":"failed","name_tail":"override"} {"from_node_id":"function:58cc4881761cc60fc8688b7b929434dc","reference_name":"Foo","reference_kind":"calls","line":156,"col":36,"candidates":null,"file_path":"torture.swift","language":"swift","status":"failed","name_tail":"Foo"} {"from_node_id":"function:58cc4881761cc60fc8688b7b929434dc","reference_name":"Foo","reference_kind":"references","line":156,"col":25,"candidates":null,"file_path":"torture.swift","language":"swift","status":"failed","name_tail":"Foo"} {"from_node_id":"function:595f3a14f307c8d0ca67982beeb3e842","reference_name":"x","reference_kind":"decorates","line":53,"col":0,"candidates":null,"file_path":"TortureAnnotatedDocs.dart","language":"dart","status":"failed","name_tail":"x"} -{"from_node_id":"function:59b070c7f403a9c6f4c4b8a2b77a33f2","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":246,"col":12,"candidates":null,"file_path":"torture.rb","language":"ruby","status":"failed","name_tail":"markdown"} +{"from_node_id":"function:59b070c7f403a9c6f4c4b8a2b77a33f2","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":246,"col":12,"candidates":null,"file_path":"torture.rb","language":"ruby","status":"failed","name_tail":"nested.markdown"} {"from_node_id":"function:59b070c7f403a9c6f4c4b8a2b77a33f2","reference_name":"load_doc","reference_kind":"calls","line":246,"col":2,"candidates":null,"file_path":"torture.rb","language":"ruby","status":"failed","name_tail":"load_doc"} {"from_node_id":"function:5a1cba4779c5504c58fbd64f89d70b6e","reference_name":"forEach","reference_kind":"calls","line":271,"col":14,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"forEach"} {"from_node_id":"function:5a1cba4779c5504c58fbd64f89d70b6e","reference_name":"listOf","reference_kind":"calls","line":271,"col":4,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"listOf"} @@ -4910,7 +4938,7 @@ {"from_node_id":"function:5ed5cbc95fb4049480923f2640ee9ddb","reference_name":"f","reference_kind":"calls","line":240,"col":4,"candidates":null,"file_path":"torture.rs","language":"rust","status":"failed","name_tail":"f"} {"from_node_id":"function:5f2e057a301724f40488ab6798773616","reference_name":"override","reference_kind":"decorates","line":40,"col":0,"candidates":null,"file_path":"TortureSigs.dart","language":"dart","status":"failed","name_tail":"override"} {"from_node_id":"function:5f2e057a301724f40488ab6798773616","reference_name":"protected","reference_kind":"decorates","line":41,"col":0,"candidates":null,"file_path":"TortureSigs.dart","language":"dart","status":"failed","name_tail":"protected"} -{"from_node_id":"function:6085bb162a93c9d9870d37c5486931cd","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":121,"col":12,"candidates":null,"file_path":"torture.lua","language":"lua","status":"failed","name_tail":"markdown"} +{"from_node_id":"function:6085bb162a93c9d9870d37c5486931cd","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":121,"col":12,"candidates":null,"file_path":"torture.lua","language":"lua","status":"failed","name_tail":"nested.markdown"} {"from_node_id":"function:6085bb162a93c9d9870d37c5486931cd","reference_name":"load_doc","reference_kind":"calls","line":121,"col":2,"candidates":null,"file_path":"torture.lua","language":"lua","status":"failed","name_tail":"load_doc"} {"from_node_id":"function:6091f3870156183bb3807760eb5cf320","reference_name":"chained","reference_kind":"calls","line":94,"col":4,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"chained"} {"from_node_id":"function:63ce12b80c45cde6b60050846d5be168","reference_name":"x","reference_kind":"decorates","line":62,"col":0,"candidates":null,"file_path":"TortureAnnotatedComments.dart","language":"dart","status":"failed","name_tail":"x"} @@ -4923,7 +4951,7 @@ {"from_node_id":"function:666f1a9d49d9f5ff962b59158da8ff9d","reference_name":"y","reference_kind":"decorates","line":41,"col":0,"candidates":null,"file_path":"TortureAnnotatedDocs.dart","language":"dart","status":"failed","name_tail":"y"} {"from_node_id":"function:67d932bdf3c34d2917376709a0ecbac1","reference_name":"load","reference_kind":"calls","line":38,"col":18,"candidates":null,"file_path":"TortureVrefDart.dart","language":"dart","status":"failed","name_tail":"load"} {"from_node_id":"function:67d932bdf3c34d2917376709a0ecbac1","reference_name":"util.Config","reference_kind":"references","line":38,"col":7,"candidates":null,"file_path":"TortureVrefDart.dart","language":"dart","status":"failed","name_tail":"Config"} -{"from_node_id":"function:681cc6feeb95207524e5b8defc0314e5","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":245,"col":11,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"markdown"} +{"from_node_id":"function:681cc6feeb95207524e5b8defc0314e5","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":245,"col":11,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"nested.markdown"} {"from_node_id":"function:681cc6feeb95207524e5b8defc0314e5","reference_name":"loadDoc","reference_kind":"calls","line":245,"col":9,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"loadDoc"} {"from_node_id":"function:68b0d5f644c793ef5327ead191c71b5a","reference_name":"x","reference_kind":"decorates","line":47,"col":0,"candidates":null,"file_path":"TortureAnnotatedDocs.dart","language":"dart","status":"failed","name_tail":"x"} {"from_node_id":"function:6a85d882c5450f2f3d882a290ae08fc8","reference_name":"EdgeInsets.all","reference_kind":"calls","line":38,"col":6,"candidates":null,"file_path":"TortureDoubleWalk.dart","language":"dart","status":"failed","name_tail":"all"} @@ -5075,7 +5103,6 @@ {"from_node_id":"function:a1998a5fcada0a95b1aebb30d3f1222c","reference_name":"w.render","reference_kind":"calls","line":72,"col":11,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"render"} {"from_node_id":"function:a1998a5fcada0a95b1aebb30d3f1222c","reference_name":"xs.forEach","reference_kind":"calls","line":81,"col":12,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"forEach"} {"from_node_id":"function:a1998a5fcada0a95b1aebb30d3f1222c","reference_name":"xs.map","reference_kind":"calls","line":80,"col":8,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"map"} -{"from_node_id":"function:a1ea154d9e5e5d67a05afd07d2206001","reference_name":"fmt.Println","reference_kind":"calls","line":69,"col":1,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"Println","failure_reason":"unknown-receiver"} {"from_node_id":"function:a946d3f4be94fcb7a892061e107ac66a","reference_name":"deprecated","reference_kind":"decorates","line":20,"col":0,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"deprecated"} {"from_node_id":"function:ab1059e06181b06b9c3ef776dfa5b44c","reference_name":"override","reference_kind":"decorates","line":14,"col":0,"candidates":null,"file_path":"TortureAnnotatedComments.dart","language":"dart","status":"failed","name_tail":"override"} {"from_node_id":"function:ab6308c2c4505606bd271827551f5081","reference_name":"Config.load","reference_kind":"calls","line":19,"col":13,"candidates":null,"file_path":"TortureMini.dart","language":"dart","status":"failed","name_tail":"load"} @@ -5109,6 +5136,8 @@ {"from_node_id":"function:b23e44d63824a6cb8592791623a8d66f","reference_name":"a::b::index_h","reference_kind":"references","line":281,"col":20,"candidates":null,"file_path":"torture.rs","language":"rust","status":"failed","name_tail":"index_h"} {"from_node_id":"function:b23e44d63824a6cb8592791623a8d66f","reference_name":"health_h","reference_kind":"references","line":281,"col":35,"candidates":null,"file_path":"torture.rs","language":"rust","status":"failed","name_tail":"health_h"} {"from_node_id":"function:b23e44d63824a6cb8592791623a8d66f","reference_name":"not_found_h","reference_kind":"references","line":282,"col":22,"candidates":null,"file_path":"torture.rs","language":"rust","status":"failed","name_tail":"not_found_h"} +{"from_node_id":"function:b2a0ca120e76741ae6bb879a2f0aea6f","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":114,"col":10,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"nested.markdown"} +{"from_node_id":"function:b2a0ca120e76741ae6bb879a2f0aea6f","reference_name":"loadDoc","reference_kind":"calls","line":114,"col":1,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"loadDoc"} {"from_node_id":"function:b2e873527f650881fbc6a4f56fb4e377","reference_name":"num","reference_kind":"references","line":46,"col":0,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"num"} {"from_node_id":"function:b2e873527f650881fbc6a4f56fb4e377","reference_name":"num","reference_kind":"references","line":46,"col":12,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"num"} {"from_node_id":"function:b3507638f9afee3fde16cc943fd670cf","reference_name":"APP_LOG","reference_kind":"calls","line":48,"col":2,"candidates":null,"file_path":"torture-macros-ctors.cpp","language":"cpp","status":"failed","name_tail":"APP_LOG"} @@ -5120,13 +5149,6 @@ {"from_node_id":"function:b3507638f9afee3fde16cc943fd670cf","reference_name":"app::Widget::Widget/1","reference_kind":"calls","line":44,"col":2,"candidates":null,"file_path":"torture-macros-ctors.cpp","language":"cpp","status":"failed","name_tail":"Widget"} {"from_node_id":"function:b37e2f22c8e8740c4b4af7720ef91073","reference_name":"Stream","reference_kind":"references","line":39,"col":0,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"Stream"} {"from_node_id":"function:b67d89cb05fb830ccc666734f2335062","reference_name":"helper","reference_kind":"calls","line":231,"col":30,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"helper"} -{"from_node_id":"function:b7ab4c33e93fae58f533755e245826ea","reference_name":"New","reference_kind":"calls","line":54,"col":8,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"New"} -{"from_node_id":"function:b7ab4c33e93fae58f533755e245826ea","reference_name":"New().Init","reference_kind":"calls","line":54,"col":8,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"Init"} -{"from_node_id":"function:b7ab4c33e93fae58f533755e245826ea","reference_name":"Registry","reference_kind":"references","line":48,"col":20,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"Registry"} -{"from_node_id":"function:b7ab4c33e93fae58f533755e245826ea","reference_name":"cfg.conn.Exec","reference_kind":"calls","line":53,"col":1,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"Exec","failure_reason":"unknown-receiver"} -{"from_node_id":"function:b7ab4c33e93fae58f533755e245826ea","reference_name":"fmt.Println","reference_kind":"calls","line":51,"col":1,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"Println","failure_reason":"unknown-receiver"} -{"from_node_id":"function:b7ab4c33e93fae58f533755e245826ea","reference_name":"loadConfig","reference_kind":"calls","line":52,"col":8,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"loadConfig"} -{"from_node_id":"function:b7ab4c33e93fae58f533755e245826ea","reference_name":"pkga.Widget","reference_kind":"instantiates","line":50,"col":6,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"Widget"} {"from_node_id":"function:b7ca967fea03e27557d0c5f386de2439","reference_name":"use_ptr","reference_kind":"calls","line":153,"col":2,"candidates":null,"file_path":"torture.c","language":"c","status":"failed","name_tail":"use_ptr"} {"from_node_id":"function:b816fb5ed257d41cc58189ef5c8e00ec","reference_name":"deprecated","reference_kind":"decorates","line":32,"col":0,"candidates":null,"file_path":"torture.scala","language":"scala","status":"failed","name_tail":"deprecated"} {"from_node_id":"function:bde51e54776bf116801473b468e0a03f","reference_name":"use_ptr","reference_kind":"calls","line":143,"col":4,"candidates":null,"file_path":"torture.c","language":"c","status":"failed","name_tail":"use_ptr"} @@ -5152,6 +5174,7 @@ {"from_node_id":"function:d44c0c526394abe8b8527106f7f7c208","reference_name":"build_list","reference_kind":"calls","line":205,"col":4,"candidates":null,"file_path":"torture.rs","language":"rust","status":"failed","name_tail":"build_list"} {"from_node_id":"function:d462abbd6321ff43ca557c0c82e26cfc","reference_name":"objc","reference_kind":"decorates","line":129,"col":0,"candidates":null,"file_path":"torture.swift","language":"swift","status":"failed","name_tail":"objc"} {"from_node_id":"function:d48f5be24010704d02ad9d861b7fc7db","reference_name":"x","reference_kind":"decorates","line":57,"col":0,"candidates":null,"file_path":"TortureAnnotatedDocs.dart","language":"dart","status":"failed","name_tail":"x"} +{"from_node_id":"function:d58344b89f92bcdaf87bfd139418e73d","reference_name":"fmt.Println","reference_kind":"calls","line":95,"col":1,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"Println","failure_reason":"unknown-receiver"} {"from_node_id":"function:d6218e84934f1b87a8e3b9a46d44010a","reference_name":"removeItem","reference_kind":"calls","line":84,"col":50,"candidates":null,"file_path":"torture.js","language":"javascript","status":"failed","name_tail":"removeItem"} {"from_node_id":"function:d6218e84934f1b87a8e3b9a46d44010a","reference_name":"res.end","reference_kind":"calls","line":84,"col":77,"candidates":null,"file_path":"torture.js","language":"javascript","status":"failed","name_tail":"end","failure_reason":"unknown-receiver"} {"from_node_id":"function:d7da4b402ad97a64a1766fcf09e392e0","reference_name":"printf","reference_kind":"calls","line":63,"col":2,"candidates":null,"file_path":"torture.c","language":"c","status":"failed","name_tail":"printf"} @@ -5161,7 +5184,7 @@ {"from_node_id":"function:dab52c2b772d941b5ea5314b8ae2a808","reference_name":"log_it","reference_kind":"calls","line":40,"col":2,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"log_it"} {"from_node_id":"function:db861ef0a9e57545fc7c8fe82604448b","reference_name":"log","reference_kind":"calls","line":99,"col":5,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"log"} {"from_node_id":"function:db861ef0a9e57545fc7c8fe82604448b","reference_name":"refresh","reference_kind":"calls","line":99,"col":51,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"refresh"} -{"from_node_id":"function:dbc50f190263eff3790bd84c1cd3a6f5","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":74,"col":12,"candidates":null,"file_path":"torture.luau","language":"luau","status":"failed","name_tail":"markdown"} +{"from_node_id":"function:dbc50f190263eff3790bd84c1cd3a6f5","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":74,"col":12,"candidates":null,"file_path":"torture.luau","language":"luau","status":"failed","name_tail":"nested.markdown"} {"from_node_id":"function:dbc50f190263eff3790bd84c1cd3a6f5","reference_name":"load_doc","reference_kind":"calls","line":74,"col":2,"candidates":null,"file_path":"torture.luau","language":"luau","status":"failed","name_tail":"load_doc"} {"from_node_id":"function:dd54d7d1bdb0ef2012631716e516c4a7","reference_name":"applyPatch","reference_kind":"calls","line":177,"col":15,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"applyPatch"} {"from_node_id":"function:ddc411c968c34e5378f6800c1ed2b1e1","reference_name":"Deprecated","reference_kind":"decorates","line":21,"col":0,"candidates":null,"file_path":"TortureAnnotatedDocs.dart","language":"dart","status":"failed","name_tail":"Deprecated"} @@ -5184,7 +5207,14 @@ {"from_node_id":"function:f00a265ed388e20f65b3357ab065773e","reference_name":"Item","reference_kind":"references","line":100,"col":35,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"Item"} {"from_node_id":"function:f00a265ed388e20f65b3357ab065773e","reference_name":"items.map","reference_kind":"calls","line":101,"col":9,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"map","failure_reason":"unknown-receiver"} {"from_node_id":"function:f00a265ed388e20f65b3357ab065773e","reference_name":"transform","reference_kind":"calls","line":101,"col":26,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"transform"} -{"from_node_id":"function:f196b629dadd3e8bafa791d7053774ee","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":155,"col":34,"candidates":null,"file_path":"torture.cpp","language":"cpp","status":"failed","name_tail":"markdown"} +{"from_node_id":"function:f0f9e8fdbfce4addf8bfac6085255496","reference_name":"New","reference_kind":"calls","line":80,"col":8,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"New"} +{"from_node_id":"function:f0f9e8fdbfce4addf8bfac6085255496","reference_name":"New().Init","reference_kind":"calls","line":80,"col":8,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"Init"} +{"from_node_id":"function:f0f9e8fdbfce4addf8bfac6085255496","reference_name":"Registry","reference_kind":"references","line":74,"col":20,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"Registry"} +{"from_node_id":"function:f0f9e8fdbfce4addf8bfac6085255496","reference_name":"cfg.conn.Exec","reference_kind":"calls","line":79,"col":1,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"Exec","failure_reason":"unknown-receiver"} +{"from_node_id":"function:f0f9e8fdbfce4addf8bfac6085255496","reference_name":"fmt.Println","reference_kind":"calls","line":77,"col":1,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"Println","failure_reason":"unknown-receiver"} +{"from_node_id":"function:f0f9e8fdbfce4addf8bfac6085255496","reference_name":"loadConfig","reference_kind":"calls","line":78,"col":8,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"loadConfig"} +{"from_node_id":"function:f0f9e8fdbfce4addf8bfac6085255496","reference_name":"pkga.Widget","reference_kind":"instantiates","line":76,"col":6,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"Widget"} +{"from_node_id":"function:f196b629dadd3e8bafa791d7053774ee","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":155,"col":34,"candidates":null,"file_path":"torture.cpp","language":"cpp","status":"failed","name_tail":"nested.markdown"} {"from_node_id":"function:f196b629dadd3e8bafa791d7053774ee","reference_name":"load_doc","reference_kind":"calls","line":155,"col":24,"candidates":null,"file_path":"torture.cpp","language":"cpp","status":"failed","name_tail":"load_doc"} {"from_node_id":"function:f42d18d9932ecf98835384d2a581205f","reference_name":"main","reference_kind":"decorates","line":30,"col":0,"candidates":null,"file_path":"torture.scala","language":"scala","status":"failed","name_tail":"main"} {"from_node_id":"function:f5457d2ff2cc26a720336e1d0b45dc3b","reference_name":"Array","reference_kind":"references","line":154,"col":34,"candidates":null,"file_path":"torture.swift","language":"swift","status":"failed","name_tail":"Array"} @@ -5414,9 +5444,6 @@ {"from_node_id":"method:78be4d08fb33c0be65e429ee23a0d708","reference_name":"T","reference_kind":"references","line":16,"col":21,"candidates":null,"file_path":"TortureConstCtors.dart","language":"dart","status":"failed","name_tail":"T"} {"from_node_id":"method:78be4d08fb33c0be65e429ee23a0d708","reference_name":"visibleForTesting","reference_kind":"decorates","line":15,"col":2,"candidates":null,"file_path":"TortureConstCtors.dart","language":"dart","status":"failed","name_tail":"visibleForTesting"} {"from_node_id":"method:7a8afa1e8551b1d529469ec5c6dd2d56","reference_name":"this.#items.add","reference_kind":"calls","line":241,"col":25,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"add"} -{"from_node_id":"method:7d8ded560e29b4ffe08aa4244f7ad020","reference_name":"T","reference_kind":"references","line":57,"col":29,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"T"} -{"from_node_id":"method:7d8ded560e29b4ffe08aa4244f7ad020","reference_name":"append","reference_kind":"calls","line":58,"col":11,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"append"} -{"from_node_id":"method:7d8ded560e29b4ffe08aa4244f7ad020","reference_name":"s.items","reference_kind":"function_ref","line":58,"col":20,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"items"} {"from_node_id":"method:7fe7437d650d086d067a7cc00ec38cca","reference_name":"Size","reference_kind":"references","line":43,"col":22,"candidates":null,"file_path":"torture.rs","language":"rust","status":"failed","name_tail":"Size"} {"from_node_id":"method:7fe7437d650d086d067a7cc00ec38cca","reference_name":"Size::default","reference_kind":"calls","line":44,"col":8,"candidates":null,"file_path":"torture.rs","language":"rust","status":"failed","name_tail":"default"} {"from_node_id":"method:80200e91146fd1e5e21c07e28e5de064","reference_name":"Init","reference_kind":"calls","line":54,"col":62,"candidates":null,"file_path":"Torture.cs","language":"csharp","status":"failed","name_tail":"Init"} @@ -5427,7 +5454,7 @@ {"from_node_id":"method:83f5ff48266a5d96c4d5b98a95728023","reference_name":"Override","reference_kind":"decorates","line":84,"col":6,"candidates":null,"file_path":"Torture.java","language":"java","status":"failed","name_tail":"Override"} {"from_node_id":"method:846b59388a77b469427475c32a0d11a3","reference_name":"Element","reference_kind":"references","line":67,"col":19,"candidates":null,"file_path":"torture.swift","language":"swift","status":"failed","name_tail":"Element"} {"from_node_id":"method:863029c5e6527c6a09cbeb30cf832583","reference_name":"pragma","reference_kind":"decorates","line":160,"col":2,"candidates":null,"file_path":"TortureAnnotatedDocs.dart","language":"dart","status":"failed","name_tail":"pragma"} -{"from_node_id":"method:87061ec6d836f4168578a67c784dad64","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":191,"col":32,"candidates":null,"file_path":"torture.scala","language":"scala","status":"failed","name_tail":"markdown"} +{"from_node_id":"method:87061ec6d836f4168578a67c784dad64","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":191,"col":32,"candidates":null,"file_path":"torture.scala","language":"scala","status":"failed","name_tail":"nested.markdown"} {"from_node_id":"method:87061ec6d836f4168578a67c784dad64","reference_name":"loadDoc","reference_kind":"calls","line":191,"col":23,"candidates":null,"file_path":"torture.scala","language":"scala","status":"failed","name_tail":"loadDoc"} {"from_node_id":"method:87219976feb420ee1a686ba7a6e03a5e","reference_name":"helper_build","reference_kind":"calls","line":80,"col":4,"candidates":null,"file_path":"torture.rb","language":"ruby","status":"failed","name_tail":"helper_build"} {"from_node_id":"method:885e8296329717d47d51ec83bd8e83ef","reference_name":"Object","reference_kind":"references","line":98,"col":19,"candidates":null,"file_path":"TortureAnnotatedComments.dart","language":"dart","status":"failed","name_tail":"Object"} @@ -5663,7 +5690,7 @@ {"from_node_id":"method:e530b69dfedb3e6da7735d250c91870b","reference_name":"w.hashCode","reference_kind":"references","line":75,"col":17,"candidates":null,"file_path":"TortureCommentChains.dart","language":"dart","status":"failed","name_tail":"hashCode"} {"from_node_id":"method:e7d630f55d4cc4a049d086c16d2a303c","reference_name":"Map","reference_kind":"references","line":58,"col":24,"candidates":null,"file_path":"TortureConstCtors.dart","language":"dart","status":"failed","name_tail":"Map"} {"from_node_id":"method:e7d630f55d4cc4a049d086c16d2a303c","reference_name":"Object","reference_kind":"references","line":58,"col":36,"candidates":null,"file_path":"TortureConstCtors.dart","language":"dart","status":"failed","name_tail":"Object"} -{"from_node_id":"method:e84efe61b136dab9b9c5b68dd202ce0a","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":147,"col":13,"candidates":null,"file_path":"Torture.java","language":"java","status":"failed","name_tail":"markdown"} +{"from_node_id":"method:e84efe61b136dab9b9c5b68dd202ce0a","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":147,"col":13,"candidates":null,"file_path":"Torture.java","language":"java","status":"failed","name_tail":"nested.markdown"} {"from_node_id":"method:e84efe61b136dab9b9c5b68dd202ce0a","reference_name":"loadDoc","reference_kind":"calls","line":147,"col":4,"candidates":null,"file_path":"Torture.java","language":"java","status":"failed","name_tail":"loadDoc"} {"from_node_id":"method:e89f3c8e64c364ec06afa7469eda6118","reference_name":"x","reference_kind":"decorates","line":92,"col":2,"candidates":null,"file_path":"TortureAnnotatedComments.dart","language":"dart","status":"failed","name_tail":"x"} {"from_node_id":"method:e8f6501f3f9b5ea421f36f19fb3d3e52","reference_name":"handleScroll","reference_kind":"calls","line":30,"col":4,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"handleScroll"} @@ -5727,6 +5754,9 @@ {"from_node_id":"method:ed44e0684be9ba73336d1065e17e8d44","reference_name":"toList","reference_kind":"calls","line":38,"col":55,"candidates":null,"file_path":"TortureGenericCalls.dart","language":"dart","status":"failed","name_tail":"toList"} {"from_node_id":"method:ed44e0684be9ba73336d1065e17e8d44","reference_name":"where","reference_kind":"calls","line":38,"col":34,"candidates":null,"file_path":"TortureGenericCalls.dart","language":"dart","status":"failed","name_tail":"where"} {"from_node_id":"method:eee1838bebaf66382792b5d17ddaaa5f","reference_name":"repo.Save","reference_kind":"calls","line":23,"col":23,"candidates":null,"file_path":"TortureFileScoped.cs","language":"csharp","status":"failed","name_tail":"Save"} +{"from_node_id":"method:f04601d0017a75d44928a1341b1d2aa3","reference_name":"T","reference_kind":"references","line":83,"col":29,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"T"} +{"from_node_id":"method:f04601d0017a75d44928a1341b1d2aa3","reference_name":"append","reference_kind":"calls","line":84,"col":11,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"append"} +{"from_node_id":"method:f04601d0017a75d44928a1341b1d2aa3","reference_name":"s.items","reference_kind":"function_ref","line":84,"col":20,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"items"} {"from_node_id":"method:f098ed2b4228c5a44ad43c16f8606866","reference_name":"x","reference_kind":"decorates","line":70,"col":2,"candidates":null,"file_path":"TortureAnnotatedComments.dart","language":"dart","status":"failed","name_tail":"x"} {"from_node_id":"method:f103d82d67ab4848ea3da2f93e9961ac","reference_name":"override","reference_kind":"decorates","line":180,"col":2,"candidates":null,"file_path":"TortureAnnotatedDocs.dart","language":"dart","status":"failed","name_tail":"override"} {"from_node_id":"method:f11a1632c933108942e00d3b2c940adc","reference_name":"Registry.register","reference_kind":"calls","line":52,"col":4,"candidates":null,"file_path":"torture.scala","language":"scala","status":"failed","name_tail":"register"} @@ -5736,7 +5766,7 @@ {"from_node_id":"method:f19b0653b3199dcdafc81fb0184920f4","reference_name":"h.touch","reference_kind":"calls","line":30,"col":13,"candidates":null,"file_path":"TortureDoubleWalk.dart","language":"dart","status":"failed","name_tail":"touch"} {"from_node_id":"method:f2ddef5919b8d397881e6bdf64253226","reference_name":"override","reference_kind":"decorates","line":125,"col":2,"candidates":null,"file_path":"TortureAnnotatedComments.dart","language":"dart","status":"failed","name_tail":"override"} {"from_node_id":"method:f301c9e6fba089a4da4226986340249b","reference_name":"LoadDoc","reference_kind":"calls","line":200,"col":12,"candidates":null,"file_path":"Torture.cs","language":"csharp","status":"failed","name_tail":"LoadDoc"} -{"from_node_id":"method:f301c9e6fba089a4da4226986340249b","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":200,"col":21,"candidates":null,"file_path":"Torture.cs","language":"csharp","status":"failed","name_tail":"markdown"} +{"from_node_id":"method:f301c9e6fba089a4da4226986340249b","reference_name":"docs/deep/nested.markdown","reference_kind":"references","line":200,"col":21,"candidates":null,"file_path":"Torture.cs","language":"csharp","status":"failed","name_tail":"nested.markdown"} {"from_node_id":"method:f3482e937ba1d2aedef0d9ec82048058","reference_name":"Bar","reference_kind":"references","line":74,"col":36,"candidates":null,"file_path":"torture.php","language":"php","status":"failed","name_tail":"Bar"} {"from_node_id":"method:f3482e937ba1d2aedef0d9ec82048058","reference_name":"Foo","reference_kind":"instantiates","line":76,"col":15,"candidates":null,"file_path":"torture.php","language":"php","status":"failed","name_tail":"Foo"} {"from_node_id":"method:f3482e937ba1d2aedef0d9ec82048058","reference_name":"Foo","reference_kind":"references","line":74,"col":32,"candidates":null,"file_path":"torture.php","language":"php","status":"failed","name_tail":"Foo"} @@ -5766,14 +5796,14 @@ {"from_node_id":"namespace:3baf9c91466e4a5e6aba9db27cf8a1fd","reference_name":"Countable","reference_kind":"imports","line":9,"col":0,"candidates":null,"file_path":"torture.php","language":"php","status":"failed","name_tail":"Countable"} {"from_node_id":"namespace:3baf9c91466e4a5e6aba9db27cf8a1fd","reference_name":"README.md","reference_kind":"references","line":225,"col":11,"candidates":null,"file_path":"torture.php","language":"php","status":"failed","name_tail":"md"} {"from_node_id":"namespace:3baf9c91466e4a5e6aba9db27cf8a1fd","reference_name":"a.md","reference_kind":"references","line":230,"col":19,"candidates":null,"file_path":"torture.php","language":"php","status":"failed","name_tail":"md"} -{"from_node_id":"namespace:3baf9c91466e4a5e6aba9db27cf8a1fd","reference_name":"docs/rooted.md","reference_kind":"references","line":226,"col":13,"candidates":null,"file_path":"torture.php","language":"php","status":"failed","name_tail":"md"} -{"from_node_id":"namespace:3baf9c91466e4a5e6aba9db27cf8a1fd","reference_name":"example.com/remote.md","reference_kind":"references","line":228,"col":19,"candidates":null,"file_path":"torture.php","language":"php","status":"failed","name_tail":"md"} +{"from_node_id":"namespace:3baf9c91466e4a5e6aba9db27cf8a1fd","reference_name":"docs/rooted.md","reference_kind":"references","line":226,"col":13,"candidates":null,"file_path":"torture.php","language":"php","status":"failed","name_tail":"rooted.md"} +{"from_node_id":"namespace:3baf9c91466e4a5e6aba9db27cf8a1fd","reference_name":"example.com/remote.md","reference_kind":"references","line":228,"col":19,"candidates":null,"file_path":"torture.php","language":"php","status":"failed","name_tail":"remote.md"} {"from_node_id":"namespace:3baf9c91466e4a5e6aba9db27cf8a1fd","reference_name":"lib/inc.php","reference_kind":"imports","line":16,"col":0,"candidates":null,"file_path":"torture.php","language":"php","status":"failed","name_tail":"inc"} {"from_node_id":"namespace:3baf9c91466e4a5e6aba9db27cf8a1fd","reference_name":"lib/inc_once.php","reference_kind":"imports","line":17,"col":0,"candidates":null,"file_path":"torture.php","language":"php","status":"failed","name_tail":"inc_once"} {"from_node_id":"namespace:3baf9c91466e4a5e6aba9db27cf8a1fd","reference_name":"lib/parens.php","reference_kind":"imports","line":15,"col":0,"candidates":null,"file_path":"torture.php","language":"php","status":"failed","name_tail":"parens"} {"from_node_id":"namespace:3baf9c91466e4a5e6aba9db27cf8a1fd","reference_name":"lib/plain.php","reference_kind":"imports","line":14,"col":0,"candidates":null,"file_path":"torture.php","language":"php","status":"failed","name_tail":"plain"} {"from_node_id":"namespace:3baf9c91466e4a5e6aba9db27cf8a1fd","reference_name":"notes.md#top","reference_kind":"references","line":229,"col":14,"candidates":null,"file_path":"torture.php","language":"php","status":"failed","name_tail":"md#top"} -{"from_node_id":"namespace:3baf9c91466e4a5e6aba9db27cf8a1fd","reference_name":"sub/b.mdx","reference_kind":"references","line":230,"col":33,"candidates":null,"file_path":"torture.php","language":"php","status":"failed","name_tail":"mdx"} +{"from_node_id":"namespace:3baf9c91466e4a5e6aba9db27cf8a1fd","reference_name":"sub/b.mdx","reference_kind":"references","line":230,"col":33,"candidates":null,"file_path":"torture.php","language":"php","status":"failed","name_tail":"b.mdx"} {"from_node_id":"namespace:6b86c02eb0a1ee666c13dab0e8585770","reference_name":"Short","reference_kind":"imports","line":2,"col":0,"candidates":null,"file_path":"TortureFileScoped.cs","language":"csharp","status":"failed","name_tail":"Short"} {"from_node_id":"namespace:6b86c02eb0a1ee666c13dab0e8585770","reference_name":"System","reference_kind":"imports","line":3,"col":0,"candidates":null,"file_path":"TortureFileScoped.cs","language":"csharp","status":"failed","name_tail":"System"} {"from_node_id":"namespace:6b86c02eb0a1ee666c13dab0e8585770","reference_name":"System.Collections.Generic.Dictionary","reference_kind":"imports","line":1,"col":0,"candidates":null,"file_path":"TortureFileScoped.cs","language":"csharp","status":"failed","name_tail":"Dictionary"} @@ -5807,6 +5837,7 @@ {"from_node_id":"property:b907b8e69e94f2fd52096df43b482b7b","reference_name":"Widget","reference_kind":"references","line":36,"col":20,"candidates":null,"file_path":"Torture.cs","language":"csharp","status":"failed","name_tail":"Widget"} {"from_node_id":"property:d0f96e89ea9b060af016b84a2b5c71ef","reference_name":"FontConfig","reference_kind":"references","line":27,"col":16,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"FontConfig"} {"from_node_id":"struct:0c309a8ed6f33456724d94ba73599e04","reference_name":"fmt::Display","reference_kind":"implements","line":62,"col":5,"candidates":null,"file_path":"torture.rs","language":"rust","status":"failed","name_tail":"Display"} +{"from_node_id":"struct:182a7933759975da57345ac013fe7619","reference_name":"Base","reference_kind":"extends","line":60,"col":2,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"Base"} {"from_node_id":"struct:487d89e7a287a16bb02adabeb6c061a0","reference_name":"View","reference_kind":"references","line":122,"col":17,"candidates":null,"file_path":"torture.swift","language":"swift","status":"failed","name_tail":"View"} {"from_node_id":"struct:4c4abd243b1894739d7060ec73d2c7a0","reference_name":"Base","reference_kind":"extends","line":18,"col":2,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"Base"} {"from_node_id":"struct:4c4abd243b1894739d7060ec73d2c7a0","reference_name":"Embedded","reference_kind":"extends","line":20,"col":6,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"Embedded"} @@ -5826,6 +5857,8 @@ {"from_node_id":"type_alias:600473b4a3158649443b2f214febf906","reference_name":"Promise","reference_kind":"references","line":81,"col":27,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"Promise"} {"from_node_id":"type_alias:8d09bbd71ebfe2d1aa557e0e79aaab99","reference_name":"Builder","reference_kind":"references","line":76,"col":28,"candidates":null,"file_path":"torture.swift","language":"swift","status":"failed","name_tail":"Builder"} {"from_node_id":"type_alias:8d09bbd71ebfe2d1aa557e0e79aaab99","reference_name":"KF","reference_kind":"references","line":76,"col":25,"candidates":null,"file_path":"torture.swift","language":"swift","status":"failed","name_tail":"KF"} +{"from_node_id":"type_alias:90db322d3e0eb78357aa837fa1f92170","reference_name":"Widget","reference_kind":"references","line":49,"col":18,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"Widget"} +{"from_node_id":"type_alias:f009af79ecd90d8b4e99e21e99f148e8","reference_name":"Item","reference_kind":"references","line":55,"col":32,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"Item"} {"from_node_id":"type_alias:f84ae3400c1d88837e6d67a42d5eea5a","reference_name":"Req","reference_kind":"references","line":85,"col":32,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"Req"} {"from_node_id":"type_alias:f84ae3400c1d88837e6d67a42d5eea5a","reference_name":"Req","reference_kind":"references","line":86,"col":27,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"Req"} {"from_node_id":"type_alias:f84ae3400c1d88837e6d67a42d5eea5a","reference_name":"Resp","reference_kind":"references","line":85,"col":37,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"Resp"} @@ -5833,45 +5866,45 @@ {"from_node_id":"type_alias:f84ae3400c1d88837e6d67a42d5eea5a","reference_name":"Service","reference_kind":"references","line":85,"col":2,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"Service"} {"from_node_id":"type_alias:f84ae3400c1d88837e6d67a42d5eea5a","reference_name":"Service","reference_kind":"references","line":86,"col":2,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"Service"} {"from_node_id":"variable:0560d6516d21bba56151a91b46a3458f","reference_name":"target_cb","reference_kind":"function_ref","line":48,"col":20,"candidates":null,"file_path":"torture.py","language":"python","status":"failed","name_tail":"target_cb"} -{"from_node_id":"variable:07f4c15ac5517d372045b53113f115ec","reference_name":"example.com/remote.md","reference_kind":"references","line":116,"col":25,"candidates":null,"file_path":"torture.lua","language":"lua","status":"failed","name_tail":"md"} +{"from_node_id":"variable:07f4c15ac5517d372045b53113f115ec","reference_name":"example.com/remote.md","reference_kind":"references","line":116,"col":25,"candidates":null,"file_path":"torture.lua","language":"lua","status":"failed","name_tail":"remote.md"} {"from_node_id":"variable:107967afd523982bfd99c4fe3b90a2a1","reference_name":"notes.md#top","reference_kind":"references","line":117,"col":20,"candidates":null,"file_path":"torture.lua","language":"lua","status":"failed","name_tail":"md#top"} {"from_node_id":"variable:144c41f1fe7d4cdfb643e499172e0f2c","reference_name":"a.md","reference_kind":"references","line":243,"col":21,"candidates":null,"file_path":"torture.rb","language":"ruby","status":"failed","name_tail":"md"} -{"from_node_id":"variable:144c41f1fe7d4cdfb643e499172e0f2c","reference_name":"sub/b.mdx","reference_kind":"references","line":243,"col":35,"candidates":null,"file_path":"torture.rb","language":"ruby","status":"failed","name_tail":"mdx"} -{"from_node_id":"variable:193a260abddbacd4b8812616870236ab","reference_name":"docs/rooted.md","reference_kind":"references","line":81,"col":16,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"md"} +{"from_node_id":"variable:144c41f1fe7d4cdfb643e499172e0f2c","reference_name":"sub/b.mdx","reference_kind":"references","line":243,"col":35,"candidates":null,"file_path":"torture.rb","language":"ruby","status":"failed","name_tail":"b.mdx"} {"from_node_id":"variable:1c2f423d65ed4cdd847201e9ee1dab10","reference_name":"notes.md#top","reference_kind":"references","line":308,"col":26,"candidates":null,"file_path":"torture.rs","language":"rust","status":"failed","name_tail":"md#top"} {"from_node_id":"variable:24ba9f675d787f6d3fe83c1c31297927","reference_name":"compute_len","reference_kind":"calls","line":289,"col":26,"candidates":null,"file_path":"torture.rs","language":"rust","status":"failed","name_tail":"compute_len"} -{"from_node_id":"variable:2b617ae851391aec3cce4ab34663df46","reference_name":"docs/rooted.md","reference_kind":"references","line":67,"col":19,"candidates":null,"file_path":"torture.luau","language":"luau","status":"failed","name_tail":"md"} +{"from_node_id":"variable:2b617ae851391aec3cce4ab34663df46","reference_name":"docs/rooted.md","reference_kind":"references","line":67,"col":19,"candidates":null,"file_path":"torture.luau","language":"luau","status":"failed","name_tail":"rooted.md"} {"from_node_id":"variable:2c97371982bc8cb00d33dea8739ea6e0","reference_name":"notes.md#top","reference_kind":"references","line":242,"col":14,"candidates":null,"file_path":"torture.rb","language":"ruby","status":"failed","name_tail":"md#top"} -{"from_node_id":"variable:31334faedc88ad501a488504f781e9a8","reference_name":"docs/rooted.md","reference_kind":"references","line":305,"col":25,"candidates":null,"file_path":"torture.rs","language":"rust","status":"failed","name_tail":"md"} +{"from_node_id":"variable:31334faedc88ad501a488504f781e9a8","reference_name":"docs/rooted.md","reference_kind":"references","line":305,"col":25,"candidates":null,"file_path":"torture.rs","language":"rust","status":"failed","name_tail":"rooted.md"} +{"from_node_id":"variable:338a23f1f938ffd8fa2b896287ab699f","reference_name":"README.md","reference_kind":"references","line":106,"col":14,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"md"} {"from_node_id":"variable:36e608fb368763b7f8a74a69f477cec7","reference_name":"target_cb","reference_kind":"function_ref","line":49,"col":13,"candidates":null,"file_path":"torture.py","language":"python","status":"failed","name_tail":"target_cb"} -{"from_node_id":"variable:3731c4860c0eac76475f173e3f52e850","reference_name":"README.md","reference_kind":"references","line":80,"col":14,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"md"} {"from_node_id":"variable:459f624660c51fd045cc765047a12ec3","reference_name":"target_cb","reference_kind":"calls","line":53,"col":22,"candidates":null,"file_path":"torture.py","language":"python","status":"failed","name_tail":"target_cb"} -{"from_node_id":"variable:52d1de85bc7cd2d0848da751d815b1e3","reference_name":"example.com/remote.md","reference_kind":"references","line":307,"col":31,"candidates":null,"file_path":"torture.rs","language":"rust","status":"failed","name_tail":"md"} +{"from_node_id":"variable:52d1de85bc7cd2d0848da751d815b1e3","reference_name":"example.com/remote.md","reference_kind":"references","line":307,"col":31,"candidates":null,"file_path":"torture.rs","language":"rust","status":"failed","name_tail":"remote.md"} +{"from_node_id":"variable:53163f1d6bc66b7bc2c6979841ca1abc","reference_name":"a.md","reference_kind":"references","line":111,"col":22,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"md"} +{"from_node_id":"variable:53163f1d6bc66b7bc2c6979841ca1abc","reference_name":"sub/b.mdx","reference_kind":"references","line":111,"col":36,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"b.mdx"} +{"from_node_id":"variable:5491cc9401b25cc313dfe1dfeb28d23c","reference_name":"example.com/remote.md","reference_kind":"references","line":109,"col":22,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"remote.md"} {"from_node_id":"variable:5bd0b7aea699a56bf2affd88c24f6f47","reference_name":"README.md","reference_kind":"references","line":113,"col":17,"candidates":null,"file_path":"torture.lua","language":"lua","status":"failed","name_tail":"md"} {"from_node_id":"variable:5cbfbd2a59a71a50f9a1ce36a6858ef9","reference_name":"a.md","reference_kind":"references","line":309,"col":33,"candidates":null,"file_path":"torture.rs","language":"rust","status":"failed","name_tail":"md"} -{"from_node_id":"variable:5cbfbd2a59a71a50f9a1ce36a6858ef9","reference_name":"sub/b.mdx","reference_kind":"references","line":309,"col":47,"candidates":null,"file_path":"torture.rs","language":"rust","status":"failed","name_tail":"mdx"} -{"from_node_id":"variable:751570e9a1f8514a268f013d78119aad","reference_name":"docs/rooted.md","reference_kind":"references","line":239,"col":13,"candidates":null,"file_path":"torture.rb","language":"ruby","status":"failed","name_tail":"md"} +{"from_node_id":"variable:5cbfbd2a59a71a50f9a1ce36a6858ef9","reference_name":"sub/b.mdx","reference_kind":"references","line":309,"col":47,"candidates":null,"file_path":"torture.rs","language":"rust","status":"failed","name_tail":"b.mdx"} +{"from_node_id":"variable:751570e9a1f8514a268f013d78119aad","reference_name":"docs/rooted.md","reference_kind":"references","line":239,"col":13,"candidates":null,"file_path":"torture.rb","language":"ruby","status":"failed","name_tail":"rooted.md"} {"from_node_id":"variable:788d75a828c79f9847e0be0e2b73a196","reference_name":"compute","reference_kind":"calls","line":10,"col":4,"candidates":null,"file_path":"torture.py","language":"python","status":"failed","name_tail":"compute"} -{"from_node_id":"variable:81d0ff7eaabc267206fbd5529a482e62","reference_name":"docs/rooted.md","reference_kind":"references","line":114,"col":19,"candidates":null,"file_path":"torture.lua","language":"lua","status":"failed","name_tail":"md"} +{"from_node_id":"variable:81d0ff7eaabc267206fbd5529a482e62","reference_name":"docs/rooted.md","reference_kind":"references","line":114,"col":19,"candidates":null,"file_path":"torture.lua","language":"lua","status":"failed","name_tail":"rooted.md"} {"from_node_id":"variable:8259711bc12da5017d83b9e8f3bd51a7","reference_name":"README.md","reference_kind":"references","line":66,"col":17,"candidates":null,"file_path":"torture.luau","language":"luau","status":"failed","name_tail":"md"} -{"from_node_id":"variable:86594ca8f3b6dae35209d56363bcd5a7","reference_name":"a.md","reference_kind":"references","line":85,"col":22,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"md"} -{"from_node_id":"variable:86594ca8f3b6dae35209d56363bcd5a7","reference_name":"sub/b.mdx","reference_kind":"references","line":85,"col":36,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"mdx"} +{"from_node_id":"variable:84b3675fd4032c00792c256c0ba41496","reference_name":"docs/rooted.md","reference_kind":"references","line":107,"col":16,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"rooted.md"} +{"from_node_id":"variable:86bbaa5a32491e5a70aeb77a6373bdc7","reference_name":"notes.md#top","reference_kind":"references","line":110,"col":17,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"md#top"} {"from_node_id":"variable:8ba939f37f98d071b9c0a5d1c73590cd","reference_name":"a.md","reference_kind":"references","line":118,"col":27,"candidates":null,"file_path":"torture.lua","language":"lua","status":"failed","name_tail":"md"} -{"from_node_id":"variable:8ba939f37f98d071b9c0a5d1c73590cd","reference_name":"sub/b.mdx","reference_kind":"references","line":118,"col":41,"candidates":null,"file_path":"torture.lua","language":"lua","status":"failed","name_tail":"mdx"} +{"from_node_id":"variable:8ba939f37f98d071b9c0a5d1c73590cd","reference_name":"sub/b.mdx","reference_kind":"references","line":118,"col":41,"candidates":null,"file_path":"torture.lua","language":"lua","status":"failed","name_tail":"b.mdx"} {"from_node_id":"variable:9b11e2d2271460cf5d5c12d00f607ca1","reference_name":"helper","reference_kind":"calls","line":52,"col":13,"candidates":null,"file_path":"torture.py","language":"python","status":"failed","name_tail":"helper"} -{"from_node_id":"variable:b0a4dbba6b6298349ca2c95d0d1a8136","reference_name":"notes.md#top","reference_kind":"references","line":84,"col":17,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"md#top"} {"from_node_id":"variable:b414249421e00ef2a2debd7c2865720f","reference_name":"a.md","reference_kind":"references","line":71,"col":27,"candidates":null,"file_path":"torture.luau","language":"luau","status":"failed","name_tail":"md"} -{"from_node_id":"variable:b414249421e00ef2a2debd7c2865720f","reference_name":"sub/b.mdx","reference_kind":"references","line":71,"col":41,"candidates":null,"file_path":"torture.luau","language":"luau","status":"failed","name_tail":"mdx"} -{"from_node_id":"variable:bb83e0760d206687873ef97791edf0b3","reference_name":"example.com/remote.md","reference_kind":"references","line":83,"col":22,"candidates":null,"file_path":"torture.go","language":"go","status":"failed","name_tail":"md"} +{"from_node_id":"variable:b414249421e00ef2a2debd7c2865720f","reference_name":"sub/b.mdx","reference_kind":"references","line":71,"col":41,"candidates":null,"file_path":"torture.luau","language":"luau","status":"failed","name_tail":"b.mdx"} {"from_node_id":"variable:c23cd3435fea1505587b5a4809da3f5e","reference_name":"Lazy::new","reference_kind":"calls","line":290,"col":30,"candidates":null,"file_path":"torture.rs","language":"rust","status":"failed","name_tail":"new"} {"from_node_id":"variable:c23cd3435fea1505587b5a4809da3f5e","reference_name":"build_cfg","reference_kind":"calls","line":290,"col":43,"candidates":null,"file_path":"torture.rs","language":"rust","status":"failed","name_tail":"build_cfg"} {"from_node_id":"variable:cd9e9fe748d0f3513f7f6c6c74264f2c","reference_name":"README.md","reference_kind":"references","line":304,"col":23,"candidates":null,"file_path":"torture.rs","language":"rust","status":"failed","name_tail":"md"} -{"from_node_id":"variable:dc32e43e11cbd6a5aa9ed4fd06aa4e45","reference_name":"example.com/remote.md","reference_kind":"references","line":241,"col":19,"candidates":null,"file_path":"torture.rb","language":"ruby","status":"failed","name_tail":"md"} -{"from_node_id":"variable:e226fd913c4b21d87da0115d1007adab","reference_name":"example.com/remote.md","reference_kind":"references","line":69,"col":25,"candidates":null,"file_path":"torture.luau","language":"luau","status":"failed","name_tail":"md"} +{"from_node_id":"variable:dc32e43e11cbd6a5aa9ed4fd06aa4e45","reference_name":"example.com/remote.md","reference_kind":"references","line":241,"col":19,"candidates":null,"file_path":"torture.rb","language":"ruby","status":"failed","name_tail":"remote.md"} +{"from_node_id":"variable:e226fd913c4b21d87da0115d1007adab","reference_name":"example.com/remote.md","reference_kind":"references","line":69,"col":25,"candidates":null,"file_path":"torture.luau","language":"luau","status":"failed","name_tail":"remote.md"} {"from_node_id":"variable:f90ca95a6ecd4291f24e8cc99492595f","reference_name":"notes.md#top","reference_kind":"references","line":70,"col":20,"candidates":null,"file_path":"torture.luau","language":"luau","status":"failed","name_tail":"md#top"} {"from_node_id":"variable:fb450996c12f335b36201ead67a34f64","reference_name":"helper","reference_kind":"calls","line":54,"col":17,"candidates":null,"file_path":"torture.py","language":"python","status":"failed","name_tail":"helper"} {"from_node_id":"variable:feb330d554c5d0caaa8978323a006e52","reference_name":"README.md","reference_kind":"references","line":238,"col":11,"candidates":null,"file_path":"torture.rb","language":"ruby","status":"failed","name_tail":"md"} -== bindings (795) +== bindings (807) {"file_path":"Torture.java","name":"","kind":"local","node_id":"class:c4ed77533cd6f14e451fe7962f484e20","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":27,"scope_end":32,"storage":null,"line":27} {"file_path":"Torture.java","name":"","kind":"local","node_id":"class:66085d60d16ebfd037ddd1bd9273a82e","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":36,"scope_end":48,"storage":null,"line":42} {"file_path":"Torture.java","name":"API_BASE","kind":"local","node_id":"constant:027c429f02116e6725f83e648b4778cf","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":16,"scope_end":106,"storage":null,"line":21} @@ -6162,43 +6195,55 @@ {"file_path":"torture.cpp","name":"vector","kind":"import","node_id":null,"target_spec":"vector","target_name":"*","exported_as":null,"export_form":null,"scope_start":1,"scope_end":156,"storage":null,"line":8} {"file_path":"torture.cpp","name":"widget_base","kind":"import","node_id":null,"target_spec":"widget_base.hpp","target_name":"*","exported_as":null,"export_form":null,"scope_start":1,"scope_end":156,"storage":null,"line":9} {"file_path":"torture.cpp","name":"~Session","kind":"local","node_id":"method:2328804ffd8c133163e907a0d7dac30c","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":38,"scope_end":44,"storage":null,"line":41} -{"file_path":"torture.go","name":"Core","kind":"decl","node_id":"interface:cb62fc849789b030a200ee6cef9c98b0","target_spec":null,"target_name":null,"exported_as":"Core","export_form":"public","scope_start":1,"scope_end":90,"storage":null,"line":31} -{"file_path":"torture.go","name":"DefaultRegistry","kind":"decl","node_id":"variable:1837cd19ce5f7a8f45b7fa4b8fc8a0e6","target_spec":null,"target_name":null,"exported_as":"DefaultRegistry","export_form":"public","scope_start":1,"scope_end":90,"storage":null,"line":11} -{"file_path":"torture.go","name":"Dur","kind":"decl","node_id":"type_alias:14815ab9ef5358d5c7df8b3c5c636ba0","target_spec":null,"target_name":null,"exported_as":"Dur","export_form":"public","scope_start":1,"scope_end":90,"storage":null,"line":46} -{"file_path":"torture.go","name":"Exact","kind":"decl","node_id":"interface:6d9879a05b65ba3dcb91e66ab1e8b323","target_spec":null,"target_name":null,"exported_as":"Exact","export_form":"public","scope_start":1,"scope_end":90,"storage":null,"line":44} -{"file_path":"torture.go","name":"MAX_ITEMS","kind":"decl","node_id":"constant:0558b57204d51fcce0a73629df252447","target_spec":null,"target_name":null,"exported_as":"MAX_ITEMS","export_form":"public","scope_start":1,"scope_end":90,"storage":null,"line":9} -{"file_path":"torture.go","name":"MAX_ITEMS","kind":"local","node_id":null,"target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":67,"scope_end":70,"storage":null,"line":68} +{"file_path":"torture.go","name":"Alias","kind":"decl","node_id":"type_alias:90db322d3e0eb78357aa837fa1f92170","target_spec":null,"target_name":null,"exported_as":"Alias","export_form":"public","scope_start":1,"scope_end":116,"storage":null,"line":49} +{"file_path":"torture.go","name":"AnonAlias","kind":"decl","node_id":"struct:182a7933759975da57345ac013fe7619","target_spec":null,"target_name":null,"exported_as":"AnonAlias","export_form":"public","scope_start":1,"scope_end":116,"storage":null,"line":59} +{"file_path":"torture.go","name":"Core","kind":"decl","node_id":"interface:cb62fc849789b030a200ee6cef9c98b0","target_spec":null,"target_name":null,"exported_as":"Core","export_form":"public","scope_start":1,"scope_end":116,"storage":null,"line":31} +{"file_path":"torture.go","name":"DefaultRegistry","kind":"decl","node_id":"variable:1837cd19ce5f7a8f45b7fa4b8fc8a0e6","target_spec":null,"target_name":null,"exported_as":"DefaultRegistry","export_form":"public","scope_start":1,"scope_end":116,"storage":null,"line":11} +{"file_path":"torture.go","name":"Defined","kind":"decl","node_id":"type_alias:d6c6403a980c3fa8265ad86510c9888f","target_spec":null,"target_name":null,"exported_as":"Defined","export_form":"public","scope_start":1,"scope_end":116,"storage":null,"line":56} +{"file_path":"torture.go","name":"Dur","kind":"decl","node_id":"type_alias:14815ab9ef5358d5c7df8b3c5c636ba0","target_spec":null,"target_name":null,"exported_as":"Dur","export_form":"public","scope_start":1,"scope_end":116,"storage":null,"line":46} +{"file_path":"torture.go","name":"Exact","kind":"decl","node_id":"interface:6d9879a05b65ba3dcb91e66ab1e8b323","target_spec":null,"target_name":null,"exported_as":"Exact","export_form":"public","scope_start":1,"scope_end":116,"storage":null,"line":44} +{"file_path":"torture.go","name":"FnAlias","kind":"decl","node_id":"type_alias:8e43bdc4a5230913e6bb71478f4d3ce9","target_spec":null,"target_name":null,"exported_as":"FnAlias","export_form":"public","scope_start":1,"scope_end":116,"storage":null,"line":54} +{"file_path":"torture.go","name":"IfaceAlias","kind":"decl","node_id":"interface:eb24239549242ce0007b1ae6f6844c48","target_spec":null,"target_name":null,"exported_as":"IfaceAlias","export_form":"public","scope_start":1,"scope_end":116,"storage":null,"line":64} +{"file_path":"torture.go","name":"LocalAlias","kind":"decl","node_id":"type_alias:e443846c686bab2d9b4f68123bff60b2","target_spec":null,"target_name":null,"exported_as":"LocalAlias","export_form":"public","scope_start":1,"scope_end":116,"storage":null,"line":52} +{"file_path":"torture.go","name":"MAX_ITEMS","kind":"decl","node_id":"constant:0558b57204d51fcce0a73629df252447","target_spec":null,"target_name":null,"exported_as":"MAX_ITEMS","export_form":"public","scope_start":1,"scope_end":116,"storage":null,"line":9} +{"file_path":"torture.go","name":"MAX_ITEMS","kind":"local","node_id":null,"target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":93,"scope_end":96,"storage":null,"line":94} +{"file_path":"torture.go","name":"MapAlias","kind":"decl","node_id":"type_alias:f009af79ecd90d8b4e99e21e99f148e8","target_spec":null,"target_name":null,"exported_as":"MapAlias","export_form":"public","scope_start":1,"scope_end":116,"storage":null,"line":55} {"file_path":"torture.go","name":"Marshal","kind":"local","node_id":"method:c9c8044a5ff012815e20baac8f5920fe","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":31,"scope_end":38,"storage":null,"line":36} -{"file_path":"torture.go","name":"NewRegistry","kind":"decl","node_id":"function:b7ab4c33e93fae58f533755e245826ea","target_spec":null,"target_name":null,"exported_as":"NewRegistry","export_form":"public","scope_start":1,"scope_end":90,"storage":null,"line":48} -{"file_path":"torture.go","name":"Number","kind":"decl","node_id":"interface:b3282ee81a952dfabf759c45dabb86de","target_spec":null,"target_name":null,"exported_as":"Number","export_form":"public","scope_start":1,"scope_end":90,"storage":null,"line":40} -{"file_path":"torture.go","name":"Push","kind":"decl","node_id":"method:7d8ded560e29b4ffe08aa4244f7ad020","target_spec":null,"target_name":null,"exported_as":"Push","export_form":"public","scope_start":1,"scope_end":90,"storage":null,"line":57} -{"file_path":"torture.go","name":"Render","kind":"decl","node_id":"method:e1f80971e6bc3a4e161d867b6a28844d","target_spec":null,"target_name":null,"exported_as":"Render","export_form":"public","scope_start":1,"scope_end":90,"storage":null,"line":61} -{"file_path":"torture.go","name":"Stack","kind":"decl","node_id":"struct:1a02b04e584e146642ddb911b81ce419","target_spec":null,"target_name":null,"exported_as":"Stack","export_form":"public","scope_start":1,"scope_end":90,"storage":null,"line":27} -{"file_path":"torture.go","name":"TargetCb","kind":"decl","node_id":"function:013bc08b4895f4830b19ced189245d75","target_spec":null,"target_name":null,"exported_as":"TargetCb","export_form":"public","scope_start":1,"scope_end":90,"storage":null,"line":65} +{"file_path":"torture.go","name":"NewRegistry","kind":"decl","node_id":"function:f0f9e8fdbfce4addf8bfac6085255496","target_spec":null,"target_name":null,"exported_as":"NewRegistry","export_form":"public","scope_start":1,"scope_end":116,"storage":null,"line":74} +{"file_path":"torture.go","name":"Number","kind":"decl","node_id":"interface:b3282ee81a952dfabf759c45dabb86de","target_spec":null,"target_name":null,"exported_as":"Number","export_form":"public","scope_start":1,"scope_end":116,"storage":null,"line":40} +{"file_path":"torture.go","name":"PtrAlias","kind":"decl","node_id":"type_alias:2eadd35ab3a35039204ee7e485c9d3f4","target_spec":null,"target_name":null,"exported_as":"PtrAlias","export_form":"public","scope_start":1,"scope_end":116,"storage":null,"line":53} +{"file_path":"torture.go","name":"Push","kind":"decl","node_id":"method:f04601d0017a75d44928a1341b1d2aa3","target_spec":null,"target_name":null,"exported_as":"Push","export_form":"public","scope_start":1,"scope_end":116,"storage":null,"line":83} +{"file_path":"torture.go","name":"Render","kind":"decl","node_id":"method:f8796162f1bd1fa332ec680173c6c80a","target_spec":null,"target_name":null,"exported_as":"Render","export_form":"public","scope_start":1,"scope_end":116,"storage":null,"line":87} +{"file_path":"torture.go","name":"Render","kind":"local","node_id":"method:1516d9f02e3a409a1257f9f857fa65ee","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":64,"scope_end":66,"storage":null,"line":65} +{"file_path":"torture.go","name":"Stack","kind":"decl","node_id":"struct:1a02b04e584e146642ddb911b81ce419","target_spec":null,"target_name":null,"exported_as":"Stack","export_form":"public","scope_start":1,"scope_end":116,"storage":null,"line":27} +{"file_path":"torture.go","name":"TargetCb","kind":"decl","node_id":"function:e2ce1de9e98005e0d5b6bb7119c7978a","target_spec":null,"target_name":null,"exported_as":"TargetCb","export_form":"public","scope_start":1,"scope_end":116,"storage":null,"line":91} {"file_path":"torture.go","name":"Unmarshal","kind":"local","node_id":"method:9310925a4f25f6bc4ce75ba243f7ede5","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":31,"scope_end":38,"storage":null,"line":37} -{"file_path":"torture.go","name":"Widget","kind":"decl","node_id":"struct:4c4abd243b1894739d7060ec73d2c7a0","target_spec":null,"target_name":null,"exported_as":"Widget","export_form":"public","scope_start":1,"scope_end":90,"storage":null,"line":17} -{"file_path":"torture.go","name":"cfg","kind":"local","node_id":null,"target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":48,"scope_end":55,"storage":null,"line":52} +{"file_path":"torture.go","name":"Widget","kind":"decl","node_id":"struct:4c4abd243b1894739d7060ec73d2c7a0","target_spec":null,"target_name":null,"exported_as":"Widget","export_form":"public","scope_start":1,"scope_end":116,"storage":null,"line":17} +{"file_path":"torture.go","name":"WordAlias","kind":"decl","node_id":"type_alias:5703c449ce310e218eeb262059f2b2cd","target_spec":null,"target_name":null,"exported_as":"WordAlias","export_form":"public","scope_start":1,"scope_end":116,"storage":null,"line":68} +{"file_path":"torture.go","name":"a","kind":"param","node_id":null,"target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":70,"scope_end":72,"storage":null,"line":70} +{"file_path":"torture.go","name":"cfg","kind":"local","node_id":null,"target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":74,"scope_end":81,"storage":null,"line":78} {"file_path":"torture.go","name":"data","kind":"param","node_id":null,"target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":37,"scope_end":37,"storage":null,"line":37} -{"file_path":"torture.go","name":"fmt","kind":"import","node_id":null,"target_spec":"fmt","target_name":"*","exported_as":null,"export_form":null,"scope_start":1,"scope_end":90,"storage":null,"line":5} -{"file_path":"torture.go","name":"handlerTable","kind":"decl","node_id":"variable:6b7262d2883839c6f601126f4dd59e9b","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":1,"scope_end":90,"storage":"package","line":13} -{"file_path":"torture.go","name":"item","kind":"param","node_id":null,"target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":57,"scope_end":59,"storage":null,"line":57} -{"file_path":"torture.go","name":"mdBare","kind":"decl","node_id":"variable:3731c4860c0eac76475f173e3f52e850","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":1,"scope_end":90,"storage":"package","line":80} -{"file_path":"torture.go","name":"mdEscapes","kind":"decl","node_id":"variable:96073374f298db55700de2091389c955","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":1,"scope_end":90,"storage":"package","line":82} -{"file_path":"torture.go","name":"mdGuide","kind":"decl","node_id":"variable:9567fda1765028e68645e39a624d61c7","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":1,"scope_end":90,"storage":"package","line":79} -{"file_path":"torture.go","name":"mdLoad","kind":"decl","node_id":"function:081d63a8e9c39d959398cdaa85b2188d","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":1,"scope_end":90,"storage":"package","line":87} -{"file_path":"torture.go","name":"mdQueried","kind":"decl","node_id":"variable:b0a4dbba6b6298349ca2c95d0d1a8136","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":1,"scope_end":90,"storage":"package","line":84} -{"file_path":"torture.go","name":"mdRemote","kind":"decl","node_id":"variable:bb83e0760d206687873ef97791edf0b3","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":1,"scope_end":90,"storage":"package","line":83} -{"file_path":"torture.go","name":"mdRooted","kind":"decl","node_id":"variable:193a260abddbacd4b8812616870236ab","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":1,"scope_end":90,"storage":"package","line":81} -{"file_path":"torture.go","name":"mdTwoInOne","kind":"decl","node_id":"variable:86594ca8f3b6dae35209d56363bcd5a7","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":1,"scope_end":90,"storage":"package","line":85} -{"file_path":"torture.go","name":"n","kind":"param","node_id":null,"target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":65,"scope_end":65,"storage":null,"line":65} -{"file_path":"torture.go","name":"pkga","kind":"import","node_id":null,"target_spec":"example.com/other/pkga","target_name":"*","exported_as":null,"export_form":null,"scope_start":1,"scope_end":90,"storage":null,"line":6} -{"file_path":"torture.go","name":"q","kind":"local","node_id":null,"target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":48,"scope_end":55,"storage":null,"line":50} -{"file_path":"torture.go","name":"reads","kind":"decl","node_id":"function:468f41958dcbb0ea1e0fd73b3eafab10","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":1,"scope_end":90,"storage":"package","line":72} -{"file_path":"torture.go","name":"s","kind":"param","node_id":null,"target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":57,"scope_end":59,"storage":null,"line":57} -{"file_path":"torture.go","name":"shadowed","kind":"decl","node_id":"function:a1ea154d9e5e5d67a05afd07d2206001","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":1,"scope_end":90,"storage":"package","line":67} +{"file_path":"torture.go","name":"fmt","kind":"import","node_id":null,"target_spec":"fmt","target_name":"*","exported_as":null,"export_form":null,"scope_start":1,"scope_end":116,"storage":null,"line":5} +{"file_path":"torture.go","name":"handlerTable","kind":"decl","node_id":"variable:6b7262d2883839c6f601126f4dd59e9b","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":1,"scope_end":116,"storage":"package","line":13} +{"file_path":"torture.go","name":"item","kind":"param","node_id":null,"target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":83,"scope_end":85,"storage":null,"line":83} +{"file_path":"torture.go","name":"mdBare","kind":"decl","node_id":"variable:338a23f1f938ffd8fa2b896287ab699f","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":1,"scope_end":116,"storage":"package","line":106} +{"file_path":"torture.go","name":"mdEscapes","kind":"decl","node_id":"variable:a862a8826882bccf9eeee7d2218227f7","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":1,"scope_end":116,"storage":"package","line":108} +{"file_path":"torture.go","name":"mdGuide","kind":"decl","node_id":"variable:9efcdcca3961f25f73cb6ef8b0912799","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":1,"scope_end":116,"storage":"package","line":105} +{"file_path":"torture.go","name":"mdLoad","kind":"decl","node_id":"function:b2a0ca120e76741ae6bb879a2f0aea6f","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":1,"scope_end":116,"storage":"package","line":113} +{"file_path":"torture.go","name":"mdQueried","kind":"decl","node_id":"variable:86bbaa5a32491e5a70aeb77a6373bdc7","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":1,"scope_end":116,"storage":"package","line":110} +{"file_path":"torture.go","name":"mdRemote","kind":"decl","node_id":"variable:5491cc9401b25cc313dfe1dfeb28d23c","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":1,"scope_end":116,"storage":"package","line":109} +{"file_path":"torture.go","name":"mdRooted","kind":"decl","node_id":"variable:84b3675fd4032c00792c256c0ba41496","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":1,"scope_end":116,"storage":"package","line":107} +{"file_path":"torture.go","name":"mdTwoInOne","kind":"decl","node_id":"variable:53163f1d6bc66b7bc2c6979841ca1abc","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":1,"scope_end":116,"storage":"package","line":111} +{"file_path":"torture.go","name":"n","kind":"param","node_id":null,"target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":91,"scope_end":91,"storage":null,"line":91} +{"file_path":"torture.go","name":"pkga","kind":"import","node_id":null,"target_spec":"example.com/other/pkga","target_name":"*","exported_as":null,"export_form":null,"scope_start":1,"scope_end":116,"storage":null,"line":6} +{"file_path":"torture.go","name":"q","kind":"local","node_id":null,"target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":74,"scope_end":81,"storage":null,"line":76} +{"file_path":"torture.go","name":"reads","kind":"decl","node_id":"function:9f483e5d42c66f7b48795991a0de7f31","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":1,"scope_end":116,"storage":"package","line":98} +{"file_path":"torture.go","name":"s","kind":"param","node_id":null,"target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":83,"scope_end":85,"storage":null,"line":83} +{"file_path":"torture.go","name":"shadowed","kind":"decl","node_id":"function:d58344b89f92bcdaf87bfd139418e73d","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":1,"scope_end":116,"storage":"package","line":93} +{"file_path":"torture.go","name":"useAlias","kind":"decl","node_id":"function:38bd0a279764bd368f0732f5abecc4f8","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":1,"scope_end":116,"storage":"package","line":70} {"file_path":"torture.go","name":"v","kind":"param","node_id":null,"target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":36,"scope_end":36,"storage":null,"line":36} -{"file_path":"torture.go","name":"w","kind":"local","node_id":null,"target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":48,"scope_end":55,"storage":null,"line":49} -{"file_path":"torture.go","name":"w","kind":"param","node_id":null,"target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":61,"scope_end":63,"storage":null,"line":61} +{"file_path":"torture.go","name":"w","kind":"local","node_id":null,"target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":74,"scope_end":81,"storage":null,"line":75} +{"file_path":"torture.go","name":"w","kind":"param","node_id":null,"target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":87,"scope_end":89,"storage":null,"line":87} {"file_path":"torture.hpp","name":"Calibrate","kind":"decl","node_id":"method:ca160b191e7fb257769f213a8450ab4c","target_spec":null,"target_name":null,"exported_as":"Calibrate","export_form":"public","scope_start":1,"scope_end":32,"storage":null,"line":29} {"file_path":"torture.hpp","name":"Meter","kind":"decl","node_id":"class:da855a76803320806ba08457a72b59fa","target_spec":null,"target_name":null,"exported_as":"Meter","export_form":"public","scope_start":1,"scope_end":32,"storage":null,"line":20} {"file_path":"torture.hpp","name":"Peek","kind":"local","node_id":"method:736a454a1f159019a862854710311b0c","target_spec":null,"target_name":null,"exported_as":null,"export_form":null,"scope_start":20,"scope_end":27,"storage":null,"line":24} @@ -6707,7 +6752,7 @@ {"path":"torture.c","language":"c","node_count":54} {"path":"torture.cpp","language":"cpp","node_count":40} {"path":"torture.dart","language":"dart","node_count":94} -{"path":"torture.go","language":"go","node_count":28} +{"path":"torture.go","language":"go","node_count":39} {"path":"torture.hpp","language":"cpp","node_count":5} {"path":"torture.js","language":"javascript","node_count":32} {"path":"torture.kt","language":"kotlin","node_count":117} diff --git a/__tests__/fixtures/kernel-parity/torture.go b/__tests__/fixtures/kernel-parity/torture.go index 1bcc9c8095..c050692830 100644 --- a/__tests__/fixtures/kernel-parity/torture.go +++ b/__tests__/fixtures/kernel-parity/torture.go @@ -45,6 +45,32 @@ type Exact interface{ int64 } type Dur int +// Aliases (`=`): the types they name are references; a literal makes a struct or interface. +type Alias = pkga.Widget + +type ( + LocalAlias = Widget + PtrAlias = *Stack[int] + FnAlias = func(w Widget) error + MapAlias = map[string][]pkga.Item + Defined Widget +) + +type AnonAlias = struct { + *Base + n int +} + +type IfaceAlias = interface { + Render() string +} + +type WordAlias = uint + +func useAlias(a *Alias) LocalAlias { + return LocalAlias{} +} + func NewRegistry() *Registry { w := Widget{name: "w"} q := pkga.Widget{} diff --git a/__tests__/framework-resolve-language.test.ts b/__tests__/framework-resolve-language.test.ts new file mode 100644 index 0000000000..7d5b6f168c --- /dev/null +++ b/__tests__/framework-resolve-language.test.ts @@ -0,0 +1,204 @@ +/** + * A framework resolves only the references written in its own languages. + * Express is detected in etcd by a file-content scan, and its `logger` + * middleware rule (a same-file declaration named `logger`) took a Go method's + * `*zap.Logger` result type for the method itself. A resolver that reads more + * languages than it extracts from lists them: a SvelteKit `+page.server.ts` + * still resolves its `$lib/…` imports, and a Razor page's `@model` its + * PageModel. + */ +import { describe, it, expect, afterAll, beforeAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; +import { getResolvingFrameworks } from '../src/resolution/frameworks'; +import { reactResolver } from '../src/resolution/frameworks/react'; +import type { ResolutionContext, UnresolvedRef } from '../src/resolution/types'; +import type { FrameworkResolver } from '../src/resolution/types'; + +function writeProject(prefix: string, files: Record): string { + const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + 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 root; +} + +describe('getResolvingFrameworks', () => { + const go: FrameworkResolver = { name: 'go', languages: ['go'], detect: () => true, resolve: () => null }; + const svelte: FrameworkResolver = { + name: 'svelte', languages: ['svelte'], resolveLanguages: ['svelte', 'typescript'], detect: () => true, resolve: () => null, + }; + const any: FrameworkResolver = { name: 'any', detect: () => true, resolve: () => null }; + + it('keeps the frameworks that list the language, and those that list none', () => { + expect(getResolvingFrameworks([go, svelte, any], 'go').map((f) => f.name)).toEqual(['go', 'any']); + expect(getResolvingFrameworks([go, svelte, any], 'python').map((f) => f.name)).toEqual(['any']); + }); + + it('reads resolveLanguages before languages', () => { + expect(getResolvingFrameworks([go, svelte, any], 'typescript').map((f) => f.name)).toEqual(['svelte', 'any']); + expect(getResolvingFrameworks([go, svelte, any], 'svelte').map((f) => f.name)).toEqual(['svelte', 'any']); + }); +}); + +describe('Express beside Go code (etcd)', () => { + let root = ''; + let cg: CodeGraph; + + beforeAll(async () => { + root = writeProject('cg-fw-lang-go-', { + 'package.json': JSON.stringify({ name: 'dashboard', dependencies: { express: '^4.19.0' } }), + 'web/server.js': `const express = require('express'); +const app = express(); + +function logger(req, res, next) { + next(); +} + +app.use(logger); +app.get('/health', (req, res) => res.json({ ok: true })); +`, + 'go.mod': 'module example.com/kv\n\ngo 1.22\n', + 'server/read/read_test.go': `package read + +import "go.uber.org/zap" + +type mockServer struct { + lg *zap.Logger +} + +func (s *mockServer) Logger() *zap.Logger { return s.lg } +`, + }); + cg = await CodeGraph.init(root, { index: true }); + }); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + it('resolves no Go reference', () => { + const ids = cg.getNodesInFile('server/read/read_test.go').map((n) => n.id); + const byExpress = cg.getOutgoingEdgesFrom(ids).filter((e) => (e.metadata as { framework?: string } | undefined)?.framework === 'express'); + expect(byExpress).toEqual([]); + }); + + it('does not take a method’s `*zap.Logger` result for the method itself', () => { + const logger = cg.getNodesInFile('server/read/read_test.go').find((n) => n.kind === 'method' && n.name === 'Logger')!; + expect(logger).toBeDefined(); + expect(cg.getOutgoingEdges(logger.id).map((e) => e.target)).not.toContain(logger.id); + }); +}); + +describe('Svelte in a SvelteKit app', () => { + let root = ''; + let cg: CodeGraph; + + beforeAll(async () => { + root = writeProject('cg-fw-lang-svelte-', { + 'package.json': JSON.stringify({ name: 'web', devDependencies: { '@sveltejs/kit': '^2.0.0', svelte: '^5.0.0' } }), + 'src/lib/api.ts': `export async function get(path: string) { + return fetch(path); +} +`, + 'src/routes/+page.server.ts': `import * as api from '$lib/api'; + +export async function load() { + return { articles: await api.get('/articles') }; +} +`, + 'src/routes/+page.svelte': ` + +

{data.articles}

+`, + }); + cg = await CodeGraph.init(root, { index: true }); + }); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + it('still resolves a `$lib/…` import written in a .ts module', () => { + const ids = cg.getNodesInFile('src/routes/+page.server.ts').map((n) => n.id); + const imported = cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'imports').map((e) => cg.getNode(e.target)!); + expect(imported.map((n) => n.filePath)).toContain('src/lib/api.ts'); + }); +}); + +describe('ASP.NET in a Razor Pages app (eShopOnWeb)', () => { + let root = ''; + let cg: CodeGraph; + + beforeAll(async () => { + root = writeProject('cg-fw-lang-razor-', { + 'Program.cs': `var builder = WebApplication.CreateBuilder(args); +builder.Services.AddRazorPages(); +var app = builder.Build(); +app.MapRazorPages(); +app.Run(); +`, + 'Pages/_ViewImports.cshtml': `@namespace Shop.Web.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +`, + 'Pages/Index.cshtml': `@page +@model IndexModel +

Catalog

+`, + 'Pages/Index.cshtml.cs': `using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Shop.Web.Pages; + +public class IndexModel : PageModel +{ +} +`, + 'Pages/Basket/Index.cshtml': `@page +@model IndexModel +

Basket

+`, + 'Pages/Basket/Index.cshtml.cs': `using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Shop.Web.Pages.Basket; + +public class IndexModel : PageModel +{ +} +`, + }); + cg = await CodeGraph.init(root, { index: true }); + }); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + it('still links each page’s `@model` to the PageModel beside it', () => { + const modelOf = (page: string) => { + const ids = cg.getNodesInFile(page).map((n) => n.id); + return cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'references').map((e) => cg.getNode(e.target)!.filePath); + }; + expect(modelOf('Pages/Index.cshtml')).toEqual(['Pages/Index.cshtml.cs']); + expect(modelOf('Pages/Basket/Index.cshtml')).toEqual(['Pages/Basket/Index.cshtml.cs']); + }); +}); + +it('React layout resolution leaves Vue and Angular route ownership intact', () => { + const context = { + getImportMappings: () => [{ localName: 'Shell', source: './Shell', exportedName: 'Shell' }], + resolveImport: () => ({ targetNodeId: 'shell' }), + } as unknown as ResolutionContext; + const ref: UnresolvedRef = { fromNodeId: 'route:app:/', referenceName: 'layout:Shell', referenceKind: 'calls', filePath: 'app.tsx', language: 'tsx', line: 1, column: 0 }; + expect(reactResolver.resolve(ref, context)?.targetNodeId).toBe('shell'); + for (const id of ['route:app:/:vue', 'route:app:/:angular', 'app:function']) { + expect(reactResolver.resolve({ ...ref, fromNodeId: id }, context)).toBeNull(); + } +}); diff --git a/__tests__/go-framework-own-package.test.ts b/__tests__/go-framework-own-package.test.ts new file mode 100644 index 0000000000..0423e03800 --- /dev/null +++ b/__tests__/go-framework-own-package.test.ts @@ -0,0 +1,249 @@ +/** + * The Go framework resolver's name heuristics ("a `…Handler` is a handler + * function", "a PascalCase name is a model struct") outrank name matching on a + * tie, so where they guess wrong nothing corrects them. Go reads a name from + * one package only — one written bare from the reference's own, `pkg.Name` + * from that import's, `x[i].Name()` from whatever type `x[i]` has — so they + * guess only for a bare name, and only in its own package. Before, they took + * a struct of that name from any package: prometheus's promql/parser `Node` + * parameters went to discovery/kubernetes's struct `Node` instead of the + * package's own `Node` interface, and every `.String()` called through an + * expression became an instantiation of the struct `promql.String`. + */ +import { describe, it, expect, afterAll, beforeAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; + +let root = ''; +let cg: CodeGraph; + +beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-own-pkg-')); + const files: Record = { + 'go.mod': 'module example.com/app\n\ngo 1.22\n', + 'promql/parser/ast.go': `package parser + +type Node interface { + String() string +} + +type Visitor interface { + Visit(node Node, path []Node) (Visitor, error) +} + +func Walk(v Visitor, node Node, path []Node) error { + return nil +} +`, + 'discovery/kubernetes/node.go': `package kubernetes + +import apiv1 "k8s.io/api/core/v1" + +type Node struct { + name string +} + +func nodeSource(n *apiv1.Node) string { + return n.Name +} +`, + 'storage/interface.go': `package storage + +type Appender interface { + Append(ref uint64) error +} + +type Filter interface { + Accept(value string) bool +} +`, + 'storage/fanout.go': `package storage + +type fanout struct{} + +func (f *fanout) Appender() Appender { + return nil +} +`, + 'discovery/aws/aws.go': `package aws + +type Filter struct { + Name string +} +`, + 'web/api/v1/search.go': `package v1 + +import "example.com/app/storage" + +type ChainFilter struct{} + +func NewChainFilter(filters ...storage.Filter) *ChainFilter { + return &ChainFilter{} +} +`, + 'errors.go': `package gin + +type Error struct { + Err error +} + +func (msg *Error) Error() string { + return msg.Err.Error() +} + +var _ error = (*Error)(nil) + +func newError(err error) *Error { + return &Error{Err: err} +} +`, + 'binding/validator.go': `package binding + +import "strings" + +type SliceValidationError []error + +func (err SliceValidationError) Error() string { + var b strings.Builder + for i := range err { + b.WriteString(err[i].Error()) + } + return b.String() +} +`, + 'handler/handler.go': `package handler + +type Handler struct{} + +func (h *Handler) Follow(c Context) error { + return nil +} + +type Context interface{} +`, + 'handler/routes.go': `package handler + +type Group interface { + POST(path string, h func(Context) error) +} + +func (h *Handler) Register(g Group) { + g.POST("/:username/follow", h.Follow) +} +`, + 'binding/time.go': `package binding + +import "time" + +type FooBarStructForTimeType struct { TimeFoo time.Time; TimeBar time.Time } + +func identity[T any](value T, _ bool) T { + return value +} + +func genericCall() int { + return identity[int](1, true) +} + +func formatTimes(obj FooBarStructForTimeType) { + _ = obj.TimeFoo.Location().String() + _ = obj.TimeBar.Location().String() + _ = obj.TimeFoo.Round(0).Round(0) +} +`, + 'protoexample/message.go': `package protoexample + +type FOO struct{} +func (m *FOO) String() string { return "foo" } +func (m *FOO) Round(value int) *FOO { return m } +func directString(m *FOO) string { return m.String() } +func (m *FOO) Next() *FOO { return m } +func chainedString(m *FOO) string { return m.Next().Next().String() } +func makeFOO() (*FOO, int) { return &FOO{}, 0 } +func factoryString() string { + m, _ := makeFOO() + return m.Next().String() +} +`, + 'model/user.go': `package model + +type Follow struct { + FollowerID uint +} +`, + }; + 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 }); +}); + +afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); +}); + +/** + * `kind file:qualifiedName` of each edge out of `file`'s nodes (of one kind, + * or named `from`) whose target is named `name`, deduplicated and sorted. + */ +const linksFrom = (file: string, name: string, from?: { kind?: string; name?: string }) => { + const sources = cg.getNodesInFile(file).filter((n) => (!from?.kind || n.kind === from.kind) && (!from?.name || n.name === from.name)); + const links = cg.getOutgoingEdgesFrom(sources.map((n) => n.id)) + .filter((e) => e.kind !== 'contains') + .map((e) => ({ kind: e.kind, target: cg.getNode(e.target)! })) + .filter(({ target }) => target.name === name) + .map(({ kind, target }) => `${kind} ${target.filePath}:${target.qualifiedName}`); + return [...new Set(links)].sort(); +}; + +describe('Go framework heuristics pick only in the package Go reads the name from', () => { + it('a bare type is its own package’s interface, not another package’s struct', () => { + expect(linksFrom('promql/parser/ast.go', 'Node')).toEqual(['references promql/parser/ast.go:Node']); + }); + + it('a type written through an outside import is not the same-named struct of its own file', () => { + expect(linksFrom('discovery/kubernetes/node.go', 'Node')).toEqual([]); + }); + + it('a method called through an expression is not an instantiation of a same-named struct', () => { + const links = linksFrom('binding/validator.go', 'Error'); + expect(links.filter((l) => l.startsWith('instantiates'))).toEqual([]); + expect(links).not.toContain('calls errors.go:Error'); + }); + + it('a chained external time receiver cannot borrow an unrelated method through a similar field name', () => { + expect(linksFrom('binding/time.go', 'String', { name: 'formatTimes' })).toEqual([]); + expect(linksFrom('binding/time.go', 'Round', { name: 'formatTimes' })).toEqual([]); + const genericLinks = linksFrom('binding/time.go', 'identity', { name: 'genericCall' }); + expect(genericLinks).toEqual(['calls binding/time.go:identity']); + expect(linksFrom('protoexample/message.go', 'String', { name: 'factoryString' })).toEqual(['calls protoexample/message.go:FOO::String']); + expect(linksFrom('protoexample/message.go', 'String', { name: 'chainedString' })).toEqual(['calls protoexample/message.go:FOO::String']); + expect(linksFrom('protoexample/message.go', 'String', { name: 'directString' })).toEqual([ + 'calls protoexample/message.go:FOO::String', + ]); + }); + + it('a result type is its own package’s type, not the method the line declares', () => { + expect(linksFrom('storage/fanout.go', 'Appender')).toEqual(['references storage/interface.go:Appender']); + }); + + it('a variadic `...pkg.T` is that import’s type', () => { + expect(linksFrom('web/api/v1/search.go', 'Filter')).toEqual(['references storage/interface.go:Filter']); + }); + + it('a conversion and a composite literal still reach their own package’s struct', () => { + expect(linksFrom('errors.go', 'Error', { name: '_' })).toEqual(['instantiates errors.go:Error']); + expect(linksFrom('errors.go', 'Error', { name: 'newError' })).toEqual([ + 'instantiates errors.go:Error', + 'references errors.go:Error', + ]); + }); + + it('a route’s handler written through a value is that value’s method, not a model struct', () => { + expect(linksFrom('handler/routes.go', 'Follow', { kind: 'route' })).toEqual(['references handler/handler.go:Handler::Follow']); + }); +}); diff --git a/__tests__/go-implements-defined-types.test.ts b/__tests__/go-implements-defined-types.test.ts new file mode 100644 index 0000000000..66a2f30b28 --- /dev/null +++ b/__tests__/go-implements-defined-types.test.ts @@ -0,0 +1,315 @@ +/** + * Go's implicit interface satisfaction (#584) is not only for structs. Any + * defined type can declare methods, and plenty of real implementers are not + * structs at all: + * + * type staticDiscoverer []*targetgroup.Group // prometheus discovery + * func (c staticDiscoverer) Run(ctx context.Context, up chan<- []*targetgroup.Group) + * + * type formSource map[string][]string // gin binding + * func (form formSource) TrySet(…) (bool, error) + * + * type Int64Comparable int64 // etcd pkg/adt + * func (v Int64Comparable) Compare(c Comparable) int + * + * Such a type is extracted as a `type_alias` that owns its methods through + * `contains` edges, but goImplementsEdges only ever offered structs as + * implementers, and the interface-dispatch bridge only walked class, struct and + * union. So `Discoverer` never listed the static discoverer, and a call + * through `Discoverer.Run` could not reach its `Run`. + */ +import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import CodeGraph from '../src/index'; +import { buildTypeHierarchy } from '../src/graph/type-hierarchy'; +import type { Edge, Node } from '../src/types'; + +const RING_STRUCTS = 40; + +const FILES: Record = { + 'go.mod': 'module example.com/app\n\ngo 1.22\n', + // prometheus's discovery package. + 'discovery/discovery.go': `package discovery + +import "context" + +type Group struct{ Source string } + +type Discoverer interface { + Run(ctx context.Context, up chan<- []*Group) +} + +type Config interface { + Name() string + NewDiscoverer() (Discoverer, error) +} + +// A list of groups that is a Config, and hands out a Discoverer over itself. +type StaticConfig []*Group + +func (StaticConfig) Name() string { return "static" } + +func (c StaticConfig) NewDiscoverer() (Discoverer, error) { return staticDiscoverer(c), nil } + +type staticDiscoverer []*Group + +func (c staticDiscoverer) Run(ctx context.Context, up chan<- []*Group) { + select { + case <-ctx.Done(): + case up <- c: + } +} + +// A true alias: the same type as staticDiscoverer, not a second one. +type StaticDiscoverer = staticDiscoverer + +// A name but no NewDiscoverer: no Config. +type namedGroups []*Group + +func (namedGroups) Name() string { return "named" } +`, + // gin's binding package: two setters over a map and over a struct type, the + // second with its method in another file. + 'binding/form_mapping.go': `package binding + +type setOptions struct{ isDefaultExists bool } + +type setter interface { + TrySet(key string, opt setOptions) (bool, error) +} + +type formSource map[string][]string + +func (form formSource) TrySet(key string, opt setOptions) (bool, error) { return false, nil } + +// No method at all. +type keys []string +`, + 'binding/multipart_form_mapping.go': `package binding + +import "net/http" + +type multipartRequest http.Request +`, + 'binding/multipart_set.go': `package binding + +func (r *multipartRequest) TrySet(key string, opt setOptions) (bool, error) { return false, nil } +`, + // etcd's pkg/adt. + 'adt/interval_tree.go': `package adt + +type Comparable interface { + Compare(c Comparable) int +} + +type StringComparable string + +func (s StringComparable) Compare(c Comparable) int { return 0 } + +type Int64Comparable int64 + +func (v Int64Comparable) Compare(c Comparable) int { return 0 } +`, + // An adapter over a function type, a generic defined type, and a defined + // type whose underlying type is an interface. + 'web/handler.go': `package web + +type Request struct{} + +type ResponseWriter interface { + Write(b []byte) (int, error) +} + +type Handler interface { + ServeHTTP(w ResponseWriter, r *Request) +} + +type HandlerFunc func(ResponseWriter, *Request) + +func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) } + +type Lener interface { + Len() int +} + +type Set[T comparable] map[T]struct{} + +func (s Set[T]) Len() int { return len(s) } + +// An interface type by another name: never an implementer itself. +type Serving Handler +`, + // More implementers that declare Tick than the cap: a defined type in the + // file listed first, then the structs. + 'ring/a.go': 'package ring\n\ntype Ticker interface{ Tick() }\n\ntype alarm int\n\nfunc (alarm) Tick() {}\n', + 'ring/b.go': [ + 'package ring', + '', + ...Array.from({ length: RING_STRUCTS }, (_, i) => { + const name = `tick${String(i).padStart(2, '0')}`; + return `type ${name} struct{}\n\nfunc (${name}) Tick() {}\n`; + }), + ].join('\n'), +}; + +let root = ''; +let cg: CodeGraph; + +beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-defined-types-')); + 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 }); +}, 120_000); + +afterAll(() => { + cg?.destroy(); + if (root) fs.rmSync(root, { recursive: true, force: true }); +}); + +const meta = (e: Edge): Record => (e.metadata ?? {}) as Record; +const pkg = (n: Node | null): string => `${path.posix.dirname(n?.filePath ?? '?')}.${n?.name}`; + +/** The one non-import node with this name in this file. */ +function one(name: string, file: string): Node { + const found = cg.getNodesByName(name).filter((n) => n.filePath === file && n.kind !== 'import'); + expect(found, `${name} in ${file}`).toHaveLength(1); + return found[0]!; +} + +/** Every Go type in the graph that could implement an interface. */ +function goTypes(g: CodeGraph): Node[] { + return [...g.getNodesByKind('struct'), ...g.getNodesByKind('type_alias')].filter((n) => n.language === 'go'); +} + +/** Every synthesized type → interface edge, as `pkg.Type -> pkg.Interface`. */ +function goImplements(): string[] { + return cg + .getOutgoingEdgesFrom(goTypes(cg).map((n) => n.id), ['implements']) + .filter((e) => meta(e).synthesizedBy === 'go-implements') + .map((e) => `${pkg(cg.getNode(e.source))} -> ${pkg(cg.getNode(e.target))}`) + .filter((e) => !e.startsWith('ring.')) + .sort(); +} + +/** Where a call through the interface's method is bridged to. */ +function dispatchTargets(iface: string, file: string, method: string): string[] { + const m = cg + .getOutgoingEdgesFrom([one(iface, file).id], ['contains']) + .map((e) => cg.getNode(e.target)) + .find((n) => n?.name === method)!; + return cg + .getOutgoingEdgesFrom([m.id], ['calls']) + .filter((e) => meta(e).synthesizedBy === 'interface-impl') + .map((e) => cg.getNode(e.target)?.qualifiedName ?? '?') + .sort(); +} + +describe('Go implicit satisfaction counts defined types as implementers', () => { + it('links every defined type whose methods cover the interface', () => { + expect(goImplements()).toEqual([ + 'adt.Int64Comparable -> adt.Comparable', + 'adt.StringComparable -> adt.Comparable', + 'binding.formSource -> binding.setter', + 'binding.multipartRequest -> binding.setter', + 'discovery.StaticConfig -> discovery.Config', + 'discovery.staticDiscoverer -> discovery.Discoverer', + 'web.HandlerFunc -> web.Handler', + 'web.Set -> web.Lener', + ]); + }); + + it('bridges a call through the interface to the defined type\'s method', () => { + expect(dispatchTargets('Discoverer', 'discovery/discovery.go', 'Run')).toEqual(['staticDiscoverer::Run']); + expect(dispatchTargets('Config', 'discovery/discovery.go', 'NewDiscoverer')).toEqual([ + 'StaticConfig::NewDiscoverer', + ]); + expect(dispatchTargets('setter', 'binding/form_mapping.go', 'TrySet')).toEqual([ + 'formSource::TrySet', + 'multipartRequest::TrySet', + ]); + expect(dispatchTargets('Comparable', 'adt/interval_tree.go', 'Compare')).toEqual([ + 'Int64Comparable::Compare', + 'StringComparable::Compare', + ]); + expect(dispatchTargets('Handler', 'web/handler.go', 'ServeHTTP')).toEqual(['HandlerFunc::ServeHTTP']); + }); + + it('shows them in the type hierarchy, both ways', () => { + const below = buildTypeHierarchy(cg, one('Comparable', 'adt/interval_tree.go'))?.descendants ?? []; + expect(below.map((d) => `${d.relation} ${pkg(d.node)}${d.synthesized ? ' (synthesized)' : ''}`).sort()).toEqual([ + 'implements adt.Int64Comparable (synthesized)', + 'implements adt.StringComparable (synthesized)', + ]); + const above = buildTypeHierarchy(cg, one('staticDiscoverer', 'discovery/discovery.go'))?.ancestors ?? []; + expect(above.map((a) => `${a.relation} ${pkg(a.node)}`)).toEqual(['implements discovery.Discoverer']); + }); + + it('keeps a true alias and an interface by another name from implementing anything', () => { + const named = (name: string) => + cg + .getNodesByName(name) + .filter((n) => n.language === 'go' && n.kind !== 'import') + .flatMap((n) => cg.getOutgoingEdgesFrom([n.id], ['implements'])); + expect(named('StaticDiscoverer')).toEqual([]); + expect(named('Serving')).toEqual([]); + // The type the alias names is linked once. + expect(goImplements().filter((e) => e.includes('staticDiscoverer'))).toHaveLength(1); + }); + + it('caps an interface at 40 implementers, taking defined types in file order with the declaring structs', () => { + const linked = cg + .getIncomingEdgesTo([one('Ticker', 'ring/a.go').id], ['implements']) + .filter((e) => meta(e).synthesizedBy === 'go-implements') + .map((e) => cg.getNode(e.source)!.name); + // alarm and 40 structs declare Tick. alarm comes first in file order, so + // the last struct is the one the cap leaves out. + expect(linked).toHaveLength(40); + expect(linked).toContain('alarm'); + expect(linked).not.toContain(`tick${RING_STRUCTS - 1}`); + expect(dispatchTargets('Ticker', 'ring/a.go', 'Tick')).toContain('alarm::Tick'); + }); +}); + +describe('a sync that edits a defined type\'s method', () => { + let dir = ''; + let graph: CodeGraph | undefined; + + afterEach(() => { + graph?.destroy(); + graph = undefined; + if (dir) fs.rmSync(dir, { recursive: true, force: true }); + dir = ''; + }); + + it('re-derives what the defined type satisfies', async () => { + dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-defined-types-sync-')); + const write = (rel: string, content: string) => { + fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true }); + fs.writeFileSync(path.join(dir, rel), content); + }; + const int64 = 'package adt\n\ntype Int64Comparable int64\n\nfunc (v Int64Comparable) Compare(c Comparable) int { return 0 }\n'; + write('go.mod', 'module example.com/app\n\ngo 1.22\n'); + write('adt/comparable.go', 'package adt\n\ntype Comparable interface {\n\tCompare(c Comparable) int\n}\n'); + write('adt/int64.go', int64); + graph = await CodeGraph.init(dir, { index: true }); + const g = graph; + const linked = () => + g + .getOutgoingEdgesFrom(goTypes(g).map((n) => n.id), ['implements']) + .filter((e) => meta(e).synthesizedBy === 'go-implements') + .map((e) => `${g.getNode(e.source)?.name} -> ${g.getNode(e.target)?.name}`); + + expect(linked()).toEqual(['Int64Comparable -> Comparable']); + write('adt/int64.go', int64.replace('Compare(', 'Cmp(')); + await g.sync({ paths: ['adt/int64.go'] }); + expect(linked()).toEqual([]); + write('adt/int64.go', int64); + await g.sync({ paths: ['adt/int64.go'] }); + expect(linked()).toEqual(['Int64Comparable -> Comparable']); + }, 60_000); +}); diff --git a/__tests__/go-implements-embedding.test.ts b/__tests__/go-implements-embedding.test.ts new file mode 100644 index 0000000000..702a52f634 --- /dev/null +++ b/__tests__/go-implements-embedding.test.ts @@ -0,0 +1,407 @@ +/** + * Go's implicit interface satisfaction (#584) compares method sets, and in Go + * both sides include what embedding brings in: + * + * type AuthReadTx interface { // etcd's server/auth/store.go + * RLock() + * RUnlock() + * UnsafeAuthReader // UnsafeReadAuthEnabled, UnsafeGetUser, … + * } + * + * type blockQuerier struct { // Select of its own; LabelValues, LabelNames + * *storage.BaseQuerier // and Close promoted from BaseQuerier + * } + * + * goImplementsEdges read each type's own methods only. AuthReadTx asked for + * RLock and RUnlock, so etcd's RWMutex "implemented" it, and an interface + * that only embeds others (`ChunkQuerier { LabelQuerier }`) asked for nothing + * and was skipped. Embedded interfaces are `extends` edges now, embedded + * structs `extends` and embedded interfaces of a struct `implements`, so both + * method sets follow those declared edges, however deep and around cycles. + * Reading only the interface side would have dropped every struct that + * satisfies an interface through promoted methods, like blockQuerier. + */ +import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import CodeGraph from '../src/index'; +import { buildTypeHierarchy } from '../src/graph/type-hierarchy'; +import type { Edge, Node } from '../src/types'; + +const FLEET_UNITS = 45; + +const FILES: Record = { + 'go.mod': 'module example.com/app\n\ngo 1.22\n', + // prometheus's storage interfaces. + 'storage/storage.go': `package storage + +type LabelQuerier interface { + LabelValues(name string) []string + LabelNames() []string + Close() error +} + +type Querier interface { + LabelQuerier + Select(sorted bool) int +} + +// No method of its own. +type ChunkQuerier interface { + LabelQuerier +} + +// Promotes its methods into every struct that embeds it. +type BaseQuerier struct{} + +func (b *BaseQuerier) LabelValues(name string) []string { return nil } +func (b *BaseQuerier) LabelNames() []string { return nil } +func (b *BaseQuerier) Close() error { return nil } + +type Appender interface { + Append(ref uint64, v float64) (uint64, error) + Commit() error + Rollback() error +} + +type Committer interface { + Commit() error + Rollback() error +} +`, + 'tsdb/querier.go': `package tsdb + +import "example.com/app/storage" + +// A storage.Querier through BaseQuerier's promoted methods and its own Select. +type blockQuerier struct { + *storage.BaseQuerier + mint, maxt int64 +} + +func (q *blockQuerier) Select(sorted bool) int { return 0 } + +// Select and nothing else: no Querier. +type exemplarQuerier struct{} + +func (e *exemplarQuerier) Select(sorted bool) int { return 0 } + +// Embeds the interface it wraps, so it already declares that it is one. +type limitAppender struct { + storage.Appender + limit int +} + +func (a *limitAppender) Append(ref uint64, v float64) (uint64, error) { + return a.Appender.Append(ref, v) +} +`, + // etcd's server/auth/store.go. + 'auth/store.go': `package auth + +type UnsafeAuthReader interface { + UnsafeReadAuthEnabled() bool + UnsafeGetUser(name string) string +} + +type AuthReadTx interface { + RLock() + RUnlock() + UnsafeAuthReader +} + +// A lock: no auth reader. +type RWMutex struct{} + +func (m *RWMutex) Lock() {} +func (m *RWMutex) Unlock() {} +func (m *RWMutex) RLock() {} +func (m *RWMutex) RUnlock() {} + +type authReadTx struct{} + +func (t *authReadTx) RLock() {} +func (t *authReadTx) RUnlock() {} +func (t *authReadTx) UnsafeReadAuthEnabled() bool { return false } +func (t *authReadTx) UnsafeGetUser(name string) string { return name } +`, + // gin's router: Engine overrides Use and gets the rest from RouterGroup. + 'gin/gin.go': `package gin + +type HandlerFunc func() + +type HandlersChain []HandlerFunc + +func (c HandlersChain) Last() HandlerFunc { return nil } + +type IRoutes interface { + Use(...HandlerFunc) IRoutes + GET(string, ...HandlerFunc) IRoutes +} + +type IRouter interface { + IRoutes + Group(string, ...HandlerFunc) *RouterGroup +} + +type RouterGroup struct{ Handlers HandlersChain } + +func (g *RouterGroup) Use(...HandlerFunc) IRoutes { return g } +func (g *RouterGroup) GET(string, ...HandlerFunc) IRoutes { return g } +func (g *RouterGroup) Group(string, ...HandlerFunc) *RouterGroup { return g } + +type Engine struct { + RouterGroup + trees []string +} + +func (e *Engine) Use(middleware ...HandlerFunc) IRoutes { + e.RouterGroup.Use(middleware...) + return e +} + +type Chain interface { + Last() HandlerFunc + Reset() +} + +// Last is promoted from the defined type it embeds. +type routeInfo struct { + HandlersChain + path string +} + +func (r *routeInfo) Reset() {} +`, + 'cycles/cycles.go': `package cycles + +type Walker interface{ Walk() } + +// A struct may embed a pointer to itself. +type Node struct { + *Node + next *Node +} + +func (n *Node) Walk() {} + +type Peer interface { + Send() + Reply() +} + +// Each embeds the other through a pointer, so each has both methods. +type Client struct{ *Server } + +type Server struct{ *Client } + +func (c *Client) Send() {} +func (s *Server) Reply() {} + +// Not valid Go, but a graph can hold it: each embeds the other. +type Left interface { + Right + L() +} + +type Right interface { + Left + R() +} + +type both struct{} + +func (both) L() {} +func (both) R() {} +`, + 'empty/empty.go': `package empty + +import "io" + +type Any interface{} + +// Embeds only what the project does not declare: nothing to match. +type Closer interface{ io.Closer } +`, + // More implementers than the cap: every unit through the base they all + // embed, and heart, declared after them, with a Beat of its own. + 'fleet/fleet.go': [ + 'package fleet', + '', + 'type Beater interface{ Beat() }', + '', + 'type Base struct{}', + '', + 'func (Base) Beat() {}', + '', + ...Array.from({ length: FLEET_UNITS }, (_, i) => `type unit${String(i).padStart(2, '0')} struct{ Base }`), + '', + ].join('\n'), + 'fleet/heart.go': 'package fleet\n\ntype heart struct{}\n\nfunc (heart) Beat() {}\n', +}; + +let root = ''; +let cg: CodeGraph; + +beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-implements-')); + 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 }); +}, 120_000); + +afterAll(() => { + cg?.destroy(); + if (root) fs.rmSync(root, { recursive: true, force: true }); +}); + +const meta = (e: Edge): Record => (e.metadata ?? {}) as Record; +const pkg = (n: Node | null): string => `${path.posix.dirname(n?.filePath ?? '?')}.${n?.name}`; + +/** The one non-import node with this name in this file. */ +function one(name: string, file: string): Node { + const found = cg.getNodesByName(name).filter((n) => n.filePath === file && n.kind !== 'import'); + expect(found, `${name} in ${file}`).toHaveLength(1); + return found[0]!; +} + +/** Every synthesized struct → interface edge, as `pkg.Struct -> pkg.Interface`. */ +function goImplements(): string[] { + const structs = cg.getNodesByKind('struct').filter((n) => n.language === 'go'); + return cg + .getOutgoingEdgesFrom(structs.map((n) => n.id), ['implements']) + .filter((e) => meta(e).synthesizedBy === 'go-implements') + .map((e) => `${pkg(cg.getNode(e.source))} -> ${pkg(cg.getNode(e.target))}`) + .filter((e) => !e.startsWith('fleet.')) + .sort(); +} + +describe('Go implicit satisfaction counts embedded methods on both sides', () => { + it('links exactly the structs whose method set, embeddings included, covers the interface', () => { + expect(goImplements()).toEqual([ + 'auth.authReadTx -> auth.AuthReadTx', + 'auth.authReadTx -> auth.UnsafeAuthReader', + 'cycles.Client -> cycles.Peer', + 'cycles.Node -> cycles.Walker', + 'cycles.Server -> cycles.Peer', + 'cycles.both -> cycles.Left', + 'cycles.both -> cycles.Right', + 'gin.Engine -> gin.IRouter', + 'gin.Engine -> gin.IRoutes', + 'gin.RouterGroup -> gin.IRouter', + 'gin.RouterGroup -> gin.IRoutes', + 'gin.routeInfo -> gin.Chain', + 'storage.BaseQuerier -> storage.ChunkQuerier', + 'storage.BaseQuerier -> storage.LabelQuerier', + 'tsdb.blockQuerier -> storage.ChunkQuerier', + 'tsdb.blockQuerier -> storage.LabelQuerier', + 'tsdb.blockQuerier -> storage.Querier', + 'tsdb.limitAppender -> storage.Committer', + ]); + }); + + it('asks for an embedded interface\'s methods too, so a lock is no AuthReadTx', () => { + const implementers = (name: string, file: string) => + (buildTypeHierarchy(cg, one(name, file))?.descendants ?? []).map((d) => `${d.relation} ${pkg(d.node)}`).sort(); + expect(implementers('AuthReadTx', 'auth/store.go')).toEqual(['implements auth.authReadTx']); + expect(implementers('Querier', 'storage/storage.go')).toEqual(['implements tsdb.blockQuerier']); + }); + + it('keeps a struct that embeds the interface to its declared edge', () => { + const edges = cg + .getOutgoingEdgesFrom([one('limitAppender', 'tsdb/querier.go').id], ['implements', 'extends']) + .map((e) => `${e.kind} ${pkg(cg.getNode(e.target))}${e.provenance === 'heuristic' ? ' (synthesized)' : ''}`) + .sort(); + expect(edges).toEqual(['implements storage.Appender', 'implements storage.Committer (synthesized)']); + }); + + it('bridges a call through the interface to an override on the embedding struct', () => { + const use = cg + .getOutgoingEdgesFrom([one('IRoutes', 'gin/gin.go').id], ['contains']) + .map((e) => cg.getNode(e.target)) + .find((n) => n?.name === 'Use')!; + const targets = cg + .getOutgoingEdgesFrom([use.id], ['calls']) + .filter((e) => meta(e).synthesizedBy === 'interface-impl') + .map((e) => cg.getNode(e.target)?.qualifiedName) + .sort(); + // RouterGroup was linked before. Engine satisfies IRoutes only with the + // GET that RouterGroup promotes into it. + expect(targets).toEqual(['Engine::Use', 'RouterGroup::Use']); + }); + + it('skips an interface it knows no method of', () => { + const into = (name: string, file: string) => + cg.getIncomingEdgesTo([one(name, file).id], ['implements']).filter((e) => meta(e).synthesizedBy === 'go-implements'); + expect(into('Any', 'empty/empty.go')).toEqual([]); + expect(into('Closer', 'empty/empty.go')).toEqual([]); + }); + + it('caps an interface at 40 implementers, keeping those that declare its methods', () => { + const beater = one('Beater', 'fleet/fleet.go'); + const linked = cg + .getIncomingEdgesTo([beater.id], ['implements']) + .filter((e) => meta(e).synthesizedBy === 'go-implements') + .map((e) => cg.getNode(e.source)!.name); + // Base, heart and 45 units satisfy Beater. heart is declared after the + // units, but a call through Beater reaches its Beat, not theirs. + expect(linked).toHaveLength(40); + expect(linked).toEqual(expect.arrayContaining(['Base', 'heart'])); + expect(linked.filter((n) => n.startsWith('unit'))).toHaveLength(38); + const beat = cg + .getOutgoingEdgesFrom([beater.id], ['contains']) + .map((e) => cg.getNode(e.target)) + .find((n) => n?.name === 'Beat')!; + expect( + cg + .getOutgoingEdgesFrom([beat.id], ['calls']) + .filter((e) => meta(e).synthesizedBy === 'interface-impl') + .map((e) => cg.getNode(e.target)!.qualifiedName) + .sort() + ).toEqual(['Base::Beat', 'heart::Beat']); + }); +}); + +describe('a sync that edits only the embedded type', () => { + let dir = ''; + let graph: CodeGraph | undefined; + + afterEach(() => { + graph?.destroy(); + graph = undefined; + if (dir) fs.rmSync(dir, { recursive: true, force: true }); + dir = ''; + }); + + it('re-derives what the struct embedding it satisfies', async () => { + dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-implements-sync-')); + const write = (rel: string, content: string) => { + fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true }); + fs.writeFileSync(path.join(dir, rel), content); + }; + const base = 'package base\n\ntype Base struct{}\n\nfunc (Base) Close() error { return nil }\n'; + write('go.mod', 'module example.com/app\n\ngo 1.22\n'); + write('api/api.go', 'package api\n\ntype NamedCloser interface {\n\tClose() error\n\tName() string\n}\n'); + write('base/base.go', base); + write('wrap/wrap.go', 'package wrap\n\nimport "example.com/app/base"\n\ntype Wrapper struct{ base.Base }\n\nfunc (Wrapper) Name() string { return "" }\n'); + graph = await CodeGraph.init(dir, { index: true }); + const g = graph; + const linked = () => + g + .getOutgoingEdgesFrom(g.getNodesByKind('struct').map((n) => n.id), ['implements']) + .filter((e) => meta(e).synthesizedBy === 'go-implements') + .map((e) => `${g.getNode(e.source)?.name} -> ${g.getNode(e.target)?.name}`); + + // Close comes from Base, in a file neither Wrapper's nor the interface's. + expect(linked()).toEqual(['Wrapper -> NamedCloser']); + write('base/base.go', base.replace('Close()', 'Shutdown()')); + await g.sync({ paths: ['base/base.go'] }); + expect(linked()).toEqual([]); + write('base/base.go', base); + await g.sync({ paths: ['base/base.go'] }); + expect(linked()).toEqual(['Wrapper -> NamedCloser']); + }, 60_000); +}); diff --git a/__tests__/go-import-package-names.test.ts b/__tests__/go-import-package-names.test.ts new file mode 100644 index 0000000000..2617cd8726 --- /dev/null +++ b/__tests__/go-import-package-names.test.ts @@ -0,0 +1,265 @@ +/** + * A Go import without a name written before its path is bound to the name of + * the package it imports, and the resolver took that to be the path's last + * element. For a versioned path it isn't: `go.yaml.in/yaml/v3` and + * `gopkg.in/yaml.v3` are package `yaml`, `github.com/mattn/go-sqlite3` is + * `sqlite3`. So `yaml.Node`, `klog.V(2)` or `semver.Version` matched none of + * the file's imports, and resolved by the bare name to whatever project symbol + * shared it: kubernetes' 1,561 `klog.V(…)` calls went to an etcd3 logger + * wrapper's `V` method, etcd's `*semver.Version` parameters to client/v3's + * `Version` function. An unaliased import now also takes the name goimports + * assumes for its path, while keeping the last element: + * `k8s.io/api/core/v1` really is package `v1`. + * + * And a word in a comment is no import name: `"fmt" // for printing` used to + * name the next import `printing`. + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; +import { importMappingsFromBindings } from '../src/resolution/import-resolver'; +import { tryKernelExtract } from '../src/extraction/kernel'; + +/** The names a Go file's imports are bound to, as `name <- path`. */ +const names = (source: string): string[] => + (importMappingsFromBindings(tryKernelExtract('x.go', source, 'go')!.bindings ?? []) ?? []).map((m) => `${m.localName} <- ${m.source}`).sort(); + +describe('Go import names', () => { + it('bind an unaliased versioned or go- import to its package name as well as its last element', () => { + expect(names(`package p + +import ( + "go.yaml.in/yaml/v3" + "github.com/mattn/go-sqlite3" + "k8s.io/api/core/v1" + "github.com/gin-gonic/gin" + "gopkg.in/natefinch/lumberjack.v2" +) +`)).toEqual([ + 'core <- k8s.io/api/core/v1', + 'gin <- github.com/gin-gonic/gin', + 'go-sqlite3 <- github.com/mattn/go-sqlite3', + 'lumberjack <- gopkg.in/natefinch/lumberjack.v2', + 'lumberjack.v2 <- gopkg.in/natefinch/lumberjack.v2', + 'sqlite3 <- github.com/mattn/go-sqlite3', + 'v1 <- k8s.io/api/core/v1', + 'v3 <- go.yaml.in/yaml/v3', + 'yaml <- go.yaml.in/yaml/v3', + ]); + expect(names('package p\n\nimport "gopkg.in/yaml.v3"\n')).toEqual(['yaml <- gopkg.in/yaml.v3', 'yaml.v3 <- gopkg.in/yaml.v3']); + }); + + it('take no assumed name another import holds, or two imports assume', () => { + // `core` is pkg/apis/core's own name, not the one assumed for api/core/v1. + expect(names(`package p + +import ( + "k8s.io/api/core/v1" + "k8s.io/kubernetes/pkg/apis/core" + yaml "sigs.k8s.io/yaml" + "go.yaml.in/yaml/v3" + "github.com/a/semver/v3" + "github.com/b/go-semver" +) +`)).toEqual([ + 'core <- k8s.io/kubernetes/pkg/apis/core', + 'go-semver <- github.com/b/go-semver', + 'v1 <- k8s.io/api/core/v1', + 'v3 <- github.com/a/semver/v3', + 'v3 <- go.yaml.in/yaml/v3', + 'yaml <- sigs.k8s.io/yaml', + ]); + }); + + it('keep a written name, assume none for a dot import, and read none from a comment', () => { + expect(names(`package p + +import ( + "fmt" // for printing + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + _ "embed" + . "github.com/onsi/ginkgo/v2" + + // yaml decoding + "gopkg.in/yaml.v3" + /* the store */ "example.com/app/store" +) + +func run() {} +`)).toEqual([ + '. <- github.com/onsi/ginkgo/v2', + '_ <- embed', + 'fmt <- fmt', + 'metav1 <- k8s.io/apimachinery/pkg/apis/meta/v1', + 'store <- example.com/app/store', + 'yaml <- gopkg.in/yaml.v3', + 'yaml.v3 <- gopkg.in/yaml.v3', + ]); + }); + + it('read only the import declarations, wherever a comment or a later string spells one', () => { + expect(names(`/* +Package p is a code generator. + +func Example() {} +*/ +package p + +// import "example.com/commented" +import ( + "text/template" // (for the body below) + "strings" +) + +func run() string { + return strings.TrimSpace(\` +import "example.com/generated" +\`) +} +`)).toEqual(['strings <- strings', 'template <- text/template']); + }); +}); + +describe('Go references through a versioned or go- import', () => { + let root = ''; + let cg: CodeGraph; + + beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-import-names-')); + const files: Record = { + 'go.mod': 'module example.com/app\n\ngo 1.22\n', + // Project symbols named like what yaml, klog, semver and sqlite3 export. + 'other/other.go': [ + 'package other', + '', + 'type Node struct{}', + '', + 'type Version struct{}', + '', + 'type Error struct{}', + '', + 'func Unmarshal(b []byte, v any) error { return nil }', + '', + 'type klogWrapper struct{}', + '', + 'func (k *klogWrapper) V(level int) bool { return true }', + '', + 'func (k *klogWrapper) Infof(format string, args ...any) {}', + '', + ].join('\n'), + 'store/store.go': [ + 'package store', + '', + 'import (', + '\t"github.com/Masterminds/semver/v3"', + '\t"github.com/mattn/go-sqlite3"', + '\t"go.yaml.in/yaml/v3"', + '\t"k8s.io/klog/v2"', + ')', + '', + 'func Load(b []byte, root *yaml.Node, min *semver.Version) error {', + '\tif klog.V(2) {', + '\t\tklog.Infof("loading %d bytes", len(b))', + '\t}', + '\tvar e sqlite3.Error', + '\t_ = e', + '\treturn yaml.Unmarshal(b, root)', + '}', + '', + ].join('\n'), + // A project module whose path ends in a major version: package kit. + 'kit/go.mod': 'module example.com/kit/v2\n\ngo 1.22\n', + 'kit/kit.go': [ + 'package kit', + '', + 'type Kit struct{}', + '', + 'func New() *Kit { return &Kit{} }', + '', + ].join('\n'), + 'decoy/decoy.go': [ + 'package decoy', + '', + 'type Kit struct{}', + '', + 'func New() *Kit { return &Kit{} }', + '', + 'func Find(id string) string { return id }', + '', + ].join('\n'), + // k8s.io/api/core/v1 is package v1, beside a package named core. + 'api/core/v1/types.go': 'package v1\n\ntype Pod struct{}\n', + 'pkg/apis/core/types.go': 'package core\n\ntype Pod struct{}\n', + 'repo/repo.go': 'package repo\n\nfunc Find(id string) string { return id }\n', + 'app/app.go': [ + 'package app', + '', + 'import (', + '\t"fmt" // for printing', + '\t"example.com/app/repo"', + '\t"example.com/app/api/core/v1"', + '\t"example.com/app/pkg/apis/core"', + '\t"example.com/kit/v2"', + ')', + '', + 'func Run(p *v1.Pod, q *core.Pod) *kit.Kit {', + '\tfmt.Println(repo.Find("x"))', + '\treturn kit.New()', + '}', + '', + ].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 }); + }, 60_000); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + const targetsFrom = (file: string) => { + const ids = cg.getNodesInFile(file).map((n) => n.id); + return cg.getOutgoingEdgesFrom(ids) + .filter((e) => e.kind !== 'contains') + .map((e) => { + const t = cg.getNode(e.target)!; + return `${e.kind} ${t.qualifiedName}@${t.filePath}`; + }) + .sort(); + }; + + it('never reach a project symbol that only shares the name', () => { + expect(targetsFrom('store/store.go').filter((t) => t.includes('@other/'))).toEqual([]); + }); + + it('reach the project package the import names', () => { + expect(targetsFrom('app/app.go')).toEqual([ + 'calls Find@repo/repo.go', + 'calls New@kit/kit.go', + 'references Kit@kit/kit.go', + 'references Pod@api/core/v1/types.go', + 'references Pod@pkg/apis/core/types.go', + ]); + }); +}); + +it('emits assumed imports in source order and encodes their factory chains', () => { + const result = tryKernelExtract('x.go', `package p +import ( + "go.yaml.in/yaml/v3" + "github.com/mattn/go-sqlite3" + "k8s.io/api/core/v1" +) +func run(r any, v any) { yaml.NewDecoder(r).Decode(v) } +`, 'go')!; + const ordered = (importMappingsFromBindings(result.bindings ?? []) ?? []) + .map((m) => m.localName).filter((n) => ['yaml', 'sqlite3', 'core'].includes(n)); + expect(ordered).toEqual(['yaml', 'sqlite3', 'core']); + expect(result.unresolvedReferences.map((ref) => ref.referenceName)).toContain('yaml.NewDecoder().Decode'); +}); diff --git a/__tests__/go-interface-embedding.test.ts b/__tests__/go-interface-embedding.test.ts index d3f9b07d0a..c6c491bf75 100644 --- a/__tests__/go-interface-embedding.test.ts +++ b/__tests__/go-interface-embedding.test.ts @@ -25,8 +25,9 @@ * Resolution keeps an embedded type in its package. A bare name is its own * package's type, not another package's struct of that name, which the Go * framework heuristics preferred (promql/parser's `Node` interface, etcd's - * `Lease`). A package that is none of the file's imports as indexed (`yaml` - * under an unaliased `go.yaml.in/yaml/v3`) leaves the type unresolved. + * `Lease`). A package that is none of the file's imports as indexed + * (`clientv3` under an unaliased `go.etcd.io/etcd/client/v3`, which goimports + * would call `client`) leaves the type unresolved. */ import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'; import * as fs from 'fs'; @@ -310,7 +311,7 @@ describe('the type hierarchy of an indexed Go module', () => { 'func (b *Base) Close() error { return nil }', '', ].join('\n'), - // Named like io.Closer, io.Reader, sync.Mutex and yaml.Node: never what those name. + // Named like io.Closer, io.Reader, sync.Mutex, yaml.Node and clientv3.KV: never what those name. 'other/other.go': [ 'package other', '', @@ -322,6 +323,8 @@ describe('the type hierarchy of an indexed Go module', () => { '', 'type Node struct{}', '', + 'type KV interface{ Get(key string) string }', + '', ].join('\n'), 'store/store.go': [ 'package store', @@ -331,15 +334,21 @@ describe('the type hierarchy of an indexed Go module', () => { '\t"sync"', '', '\t"example.com/app/storage"', + '\t"go.etcd.io/etcd/client/v3"', '\t"go.yaml.in/yaml/v3"', ')', '', - '// The package is yaml, though the index knows its import as v3.', + '// The package is yaml, though its import path ends in v3.', 'type RuleGroupNode struct {', '\tyaml.Node', '\tName string', '}', '', + '// The package is clientv3: not v3, nor the client goimports would assume.', + 'type kvPrefix struct {', + '\tclientv3.KV', + '}', + '', 'type Queryable interface{ Q() }', '', 'type Querier interface {', @@ -417,19 +426,24 @@ describe('the type hierarchy of an indexed Go module', () => { one('Number', 'store/store.go'), one('Store', 'store/store.go'), one('RuleGroupNode', 'store/store.go'), + one('kvPrefix', 'store/store.go'), one('Reader', 'wrap/wrap.go'), one('IRouter', 'gin/routergroup.go'), one('Expr', 'parser/ast.go'), one('exprWrapper', 'parser/ast.go'), ]; + // Declared edges only: Store also satisfies other.Closer with the Close + // that storage.Base promotes, which go-implements synthesizes from names. const supertypes = graph .getOutgoingEdgesFrom(sources.map((n) => n.id), ['extends', 'implements']) + .filter((e) => e.provenance !== 'heuristic') .map((e) => `${graph.getNode(e.source)?.name} ${e.kind} ${where(graph.getNode(e.target))}`) .sort(); // An embedded interface of a struct resolves as `implements`; one of an // interface stays `extends`. io.Closer, sync.Mutex, io.Reader and yaml.Node - // are outside the project, so nothing named like them is linked. A bare - // name is its own package's type, whatever kind another package's is. + // are outside the project, and clientv3 is no import the index knows, so + // nothing named like them is linked. A bare name is its own package's + // type, whatever kind another package's is. expect(supertypes).toEqual([ 'Expr extends Node@parser/ast.go', 'IRouter extends IRoutes@gin/routergroup.go', @@ -452,6 +466,7 @@ describe('the type hierarchy of an indexed Go module', () => { expect(unresolved(one('Querier', 'store/store.go'))).toEqual(['Closer']); expect(unresolved(one('Store', 'store/store.go'))).toEqual(['Mutex']); expect(unresolved(one('RuleGroupNode', 'store/store.go'))).toEqual(['Node']); + expect(unresolved(one('kvPrefix', 'store/store.go'))).toEqual(['KV']); expect(unresolved(one('Reader', 'wrap/wrap.go'))).toEqual(['Reader']); // What the viewer's Type hierarchy and codegraph_explore read. Declared @@ -465,7 +480,7 @@ describe('the type hierarchy of an indexed Go module', () => { expect(subtypes(one('LabelQuerier', 'storage/storage.go'))).toEqual(['extends Querier@store/store.go']); expect(subtypes(one('Base', 'storage/storage.go'))).toEqual(['extends Store@store/store.go']); const declaredInto = graph - .getIncomingEdgesTo(['Closer', 'Mutex', 'Reader', 'Node'].map((name) => one(name, 'other/other.go').id), ['extends', 'implements']) + .getIncomingEdgesTo(['Closer', 'Mutex', 'Reader', 'Node', 'KV'].map((name) => one(name, 'other/other.go').id), ['extends', 'implements']) .filter((e) => e.provenance !== 'heuristic'); expect(declaredInto).toEqual([]); expect( diff --git a/__tests__/go-promoted-dispatch.test.ts b/__tests__/go-promoted-dispatch.test.ts new file mode 100644 index 0000000000..76b5d6243f --- /dev/null +++ b/__tests__/go-promoted-dispatch.test.ts @@ -0,0 +1,379 @@ +/** + * A Go struct can satisfy an interface with methods it gets by embedding: + * + * type blockBaseSeriesSet struct{ … } // prometheus's tsdb/querier.go + * func (b *blockBaseSeriesSet) Next() bool // Next, Err, Warnings — no At + * + * type blockSeriesSet struct{ blockBaseSeriesSet } + * func (b *blockSeriesSet) At() storage.Series + * + * go-implements links blockSeriesSet to storage.SeriesSet, and a call through + * `SeriesSet.Next` then runs blockBaseSeriesSet's Next. The interface-dispatch + * bridge linked an interface's methods only to the methods the implementing + * struct declares, so that call reached nothing: blockBaseSeriesSet has no At + * and implements nothing itself. The bridge now follows the embedding to the + * method Go's selector picks — the shallowest one, none when two tie — and + * stops at an embedded interface, whose method is a dynamic call again. + */ +import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import CodeGraph from '../src/index'; +import { ToolHandler } from '../src/mcp/tools'; +import type { Edge, Node } from '../src/types'; + +const BIG_METHODS = 45; +const big = (i: number) => `M${String(i).padStart(2, '0')}`; + +const FILES: Record = { + 'go.mod': 'module example.com/app\n\ngo 1.22\n', + 'storage/interface.go': `package storage + +type Series interface{ Labels() string } + +type ChunkSeries interface{ Labels() string } + +type SeriesSet interface { + Next() bool + At() Series + Err() error + Warnings() []string +} + +type ChunkSeriesSet interface { + Next() bool + At() ChunkSeries + Err() error + Warnings() []string +} +`, + 'tsdb/querier.go': `package tsdb + +import "example.com/app/storage" + +// Next, Err and Warnings for the two series sets below. No At: it is +// neither series set itself. +type blockBaseSeriesSet struct { + err error +} + +func (b *blockBaseSeriesSet) Next() bool { return false } +func (b *blockBaseSeriesSet) Err() error { return b.err } +func (b *blockBaseSeriesSet) Warnings() []string { return nil } + +type blockSeriesSet struct { + blockBaseSeriesSet +} + +func (b *blockSeriesSet) At() storage.Series { return nil } + +type blockChunkSeriesSet struct { + blockBaseSeriesSet +} + +func (b *blockChunkSeriesSet) At() storage.ChunkSeries { return nil } + +func newBlockSeriesSet() storage.SeriesSet { return &blockSeriesSet{} } +`, + 'promql/engine.go': `package promql + +import "example.com/app/storage" + +func expandSeriesSet(it storage.SeriesSet) int { + n := 0 + for it.Next() { + n++ + } + return n +} +`, + 'selector/selector.go': `package selector + +type Doer interface { + Do() + Name() string +} + +type Runner interface{ Do() } + +// Do from two levels down. +type inner struct{} + +func (inner) Do() {} + +type middle struct{ inner } + +type deep struct { + middle +} + +func (deep) Name() string { return "" } + +// near's Do at depth 1 hides far's at depth 2. +type near struct{} + +func (near) Do() {} + +type far struct{} + +func (far) Do() {} + +type farWrap struct{ far } + +type shadowed struct { + farWrap + near +} + +func (shadowed) Name() string { return "" } + +// Two Dos at depth 1: Go rejects t.Do(), no method runs. +type left struct{} + +func (left) Do() {} + +type right struct{} + +func (right) Do() {} + +type tie struct { + left + right +} + +func (tie) Name() string { return "" } + +// One type's Do at depth 2 along two paths: as ambiguous. +type core struct{} + +func (core) Do() {} + +type viaA struct{ core } + +type viaB struct{ core } + +type diamond struct { + viaA + viaB +} + +func (diamond) Name() string { return "" } + +// Do from the interface it embeds: whatever that holds runs. +type delegate struct{ Runner } + +func (delegate) Name() string { return "" } + +// The embedded interface's Do at depth 1 hides buried's at depth 2. +type buried struct{} + +func (buried) Do() {} + +type cover struct{ buried } + +type hidden struct { + cover + Runner +} + +func (hidden) Name() string { return "" } + +// Do from the defined type it embeds. +type Chain []func() + +func (c Chain) Do() {} + +type chained struct{ Chain } + +func (chained) Name() string { return "" } + +// full is a Doer itself: its own edge, whoever embeds it. +type fullWrap struct{ full } + +type full struct{} + +func (full) Do() {} +func (full) Name() string { return "" } +`, + // More of the interface than the cap: one method of its own, the rest from + // a base that lacks the last. + 'big/big.go': [ + 'package big', + '', + 'type Big interface {', + ...Array.from({ length: BIG_METHODS }, (_, i) => `\t${big(i)}()`), + '}', + '', + 'type bigBase struct{}', + '', + ...Array.from({ length: BIG_METHODS - 1 }, (_, i) => `func (bigBase) ${big(i)}() {}`), + '', + 'type bigOne struct{ bigBase }', + '', + `func (bigOne) ${big(BIG_METHODS - 1)}() {}`, + '', + ].join('\n'), +}; + +let root = ''; +let cg: CodeGraph; + +beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-promoted-')); + 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 }); +}, 120_000); + +afterAll(() => { + cg?.destroy(); + if (root) fs.rmSync(root, { recursive: true, force: true }); +}); + +const meta = (e: Edge): Record => (e.metadata ?? {}) as Record; + +/** `file:line` of the first fixture line containing `text`. */ +function at(file: string, text: string): string { + const line = FILES[file]!.split('\n').findIndex((l) => l.includes(text)); + expect(line, `${text} in ${file}`).toBeGreaterThanOrEqual(0); + return `${file}:${line + 1}`; +} + +/** The one non-import node with this name in this file. */ +function one(name: string, file: string): Node { + const found = cg.getNodesByName(name).filter((n) => n.filePath === file && n.kind !== 'import'); + expect(found, `${name} in ${file}`).toHaveLength(1); + return found[0]!; +} + +/** An interface's method. */ +function methodOf(graph: CodeGraph, iface: Node, name: string): Node { + const m = graph + .getOutgoingEdgesFrom([iface.id], ['contains']) + .map((e) => graph.getNode(e.target)) + .find((n) => n?.name === name); + expect(m, `${iface.name}.${name}`).toBeTruthy(); + return m!; +} + +/** Where a call through the method dispatches, as `Target::m` or `Target::m <- promotedInto @registeredAt`. */ +function dispatch(graph: CodeGraph, method: Node): string[] { + return graph + .getOutgoingEdgesFrom([method.id], ['calls']) + .filter((e) => meta(e).synthesizedBy === 'interface-impl') + .map((e) => { + const target = graph.getNode(e.target)?.qualifiedName; + const into = meta(e).promotedInto; + return into === undefined ? `${target}` : `${target} <- ${into} @${meta(e).registeredAt}`; + }) + .sort(); +} + +describe('a call through a Go interface reaches the method embedding promotes', () => { + it('links the interface method to the embedded type that declares it', () => { + const seriesSet = one('SeriesSet', 'storage/interface.go'); + const chunkSet = one('ChunkSeriesSet', 'storage/interface.go'); + // blockSeriesSet's embedding, the first of the two. + const embedding = at('tsdb/querier.go', '\tblockBaseSeriesSet'); + // Both structs satisfy both interfaces by name, and both get Next from + // the same base: one edge, through the first of them. + for (const iface of [seriesSet, chunkSet]) { + expect(dispatch(cg, methodOf(cg, iface, 'Next'))).toEqual([`blockBaseSeriesSet::Next <- blockSeriesSet @${embedding}`]); + expect(dispatch(cg, methodOf(cg, iface, 'Err'))).toEqual([`blockBaseSeriesSet::Err <- blockSeriesSet @${embedding}`]); + expect(dispatch(cg, methodOf(cg, iface, 'At'))).toEqual(['blockChunkSeriesSet::At', 'blockSeriesSet::At']); + } + }); + + it('picks the provider the way Go\'s selector does', () => { + const doer = one('Doer', 'selector/selector.go'); + const file = 'selector/selector.go'; + expect(dispatch(cg, methodOf(cg, doer, 'Do'))).toEqual( + [ + // The defined type's method. + `Chain::Do <- chained @${at(file, 'type chained struct')}`, + // full's own edge, though fullWrap comes first. + 'full::Do', + // Two levels down, through deep's own embedding of middle. + `inner::Do <- deep @${at(file, '\tmiddle')}`, + // near at depth 1, not far at depth 2. + `near::Do <- shadowed @${at(file, '\tnear')}`, + ].sort() + ); + // Not linked: left and right (tie), core (two paths), the Runner that + // delegate and hidden embed, and buried behind it. + }); + + it('keeps the provider\'s own edge as it was', () => { + const doer = one('Doer', 'selector/selector.go'); + const doEdge = cg + .getOutgoingEdgesFrom([methodOf(cg, doer, 'Do').id], ['calls']) + .find((e) => cg.getNode(e.target)?.qualifiedName === 'full::Do'); + expect(doEdge).toMatchObject({ + provenance: 'heuristic', + metadata: { synthesizedBy: 'interface-impl', via: 'Do', registeredAt: at('selector/selector.go', 'func (full) Do()') }, + }); + expect(meta(doEdge!)).not.toHaveProperty('promotedInto'); + }); + + it('caps a struct\'s links into one interface, keeping its own method', () => { + const bigIface = one('Big', 'big/big.go'); + const targets = Array.from({ length: BIG_METHODS }, (_, i) => dispatch(cg, methodOf(cg, bigIface, big(i)))).flat(); + const embedding = at('big/big.go', 'type bigOne struct'); + expect(targets).toHaveLength(40); + expect(targets).toContain(`bigOne::${big(BIG_METHODS - 1)}`); + // The 39 slots left go to methods the base promotes. + const promoted = targets.filter((t) => t.startsWith('bigBase::')); + expect(promoted).toHaveLength(39); + for (const t of promoted) expect(t).toMatch(new RegExp(`^bigBase::M\\d\\d <- bigOne @${embedding}$`)); + }); + + it('shows the hop in codegraph_explore as a promoted method with its embedding', async () => { + const result = await new ToolHandler(cg).execute('codegraph_explore', { + query: 'expandSeriesSet SeriesSet.Next blockBaseSeriesSet.Next', + }); + const text = result.content?.[0]?.text ?? ''; + expect(text).toContain( + `dynamic: interface → method promoted into blockSeriesSet @${at('tsdb/querier.go', '\tblockBaseSeriesSet')}` + ); + }); +}); + +describe('a sync that changes what the struct declares', () => { + let dir = ''; + let graph: CodeGraph | undefined; + + afterEach(() => { + graph?.destroy(); + graph = undefined; + if (dir) fs.rmSync(dir, { recursive: true, force: true }); + dir = ''; + }); + + it('moves the link between the promoted method and the struct\'s own', async () => { + dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-promoted-sync-')); + const write = (rel: string, content: string) => { + fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true }); + fs.writeFileSync(path.join(dir, rel), content); + }; + const wrap = 'package wrap\n\nimport "example.com/app/base"\n\ntype Wrapper struct {\n\tbase.Base\n}\n\nfunc (Wrapper) Name() string { return "" }\n'; + write('go.mod', 'module example.com/app\n\ngo 1.22\n'); + write('api/api.go', 'package api\n\ntype NamedCloser interface {\n\tClose() error\n\tName() string\n}\n'); + write('base/base.go', 'package base\n\ntype Base struct{}\n\nfunc (Base) Close() error { return nil }\n'); + write('wrap/wrap.go', wrap); + graph = await CodeGraph.init(dir, { index: true }); + const g = graph; + const close = () => dispatch(g, methodOf(g, g.getNodesByName('NamedCloser')[0]!, 'Close')); + + // Close comes from base.Base, embedded on line 6. + expect(close()).toEqual(['Base::Close <- Wrapper @wrap/wrap.go:6']); + write('wrap/wrap.go', `${wrap}\nfunc (Wrapper) Close() error { return nil }\n`); + await g.sync({ paths: ['wrap/wrap.go'] }); + expect(close()).toEqual(['Wrapper::Close']); + write('wrap/wrap.go', wrap); + await g.sync({ paths: ['wrap/wrap.go'] }); + expect(close()).toEqual(['Base::Close <- Wrapper @wrap/wrap.go:6']); + }, 60_000); +}); diff --git a/__tests__/go-type-alias.test.ts b/__tests__/go-type-alias.test.ts new file mode 100644 index 0000000000..d02dce9830 --- /dev/null +++ b/__tests__/go-type-alias.test.ts @@ -0,0 +1,371 @@ +/** + * A Go alias declaration (`type Event = mvccpb.Event`, `type ( A = B )`) was + * in neither extractor's graph. tree-sitter-go parses it as a `type_alias` + * beside the `type_spec` of a defined type (`type WatchChan <-chan + * WatchResponse`), and both extractors only read `type_spec`, so etcd's + * client/v3 `Event` had no node while the line below it did. + * + * An alias is now a node of the kind a declaration of its type gets: a + * `type_alias` that references the types it names, a `struct` or an + * `interface` when it names a struct or interface literal. Each reference sits + * on the type's name, where resolution reads a package qualifier back. A + * generic alias (`type Set[T any] = …`, Go 1.24) has no rule in this grammar: + * it parses as a `type_spec` around an error. Native recovery reads the + * alias while excluding its type parameters from references. + * + * Resolution treats the alias as the type it names. A bare `Event{}` or + * `*Event` in its package links to the alias, a method called on an `*Event` + * is the aliased type's method, methods written with the alias as the + * receiver make it implement what they satisfy, and an alias whose target is + * written through a package the index doesn't know (`clientv3` under an + * unaliased `go.etcd.io/etcd/client/v3`, known as `v3` or `client`) never + * links to itself or to a namesake. + */ +import { describe, it, expect, beforeAll, beforeEach, afterEach, afterAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import CodeGraph from '../src/index'; +import { extractFromSource } from '../src/extraction'; +import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars'; +import { tryKernelExtract, resetKernelForTests } from '../src/extraction/kernel'; +import type { ExtractionResult } from '../src/types'; + +/** Every alias shape the grammar has a rule for, beside defined types. */ +const ALIAS_SOURCE = `package clientv3 + +import ( + "context" + + "go.etcd.io/etcd/api/v3/mvccpb" +) + +// Event is the event a watcher reports. +type Event = mvccpb.Event + +type WatchChan <-chan WatchResponse + +type WatchResponse struct { + Events []*Event +} + +type ( + // Local names a type of this package. + Local = WatchResponse + Ptr = *WatchResponse + Slice = []WatchResponse + Handler = func(ctx context.Context, r WatchResponse) error + Index = map[string]*mvccpb.KeyValue + Defined WatchResponse +) + +type Plain = List[WatchResponse] + +type Anon = struct { + Key string +} + +type Closer = interface { + Close() error +} + +type Type = string + +type Word = uint +`; + +/** Generic aliases: no grammar rule, so a parse error the kernel defers. */ +const GENERIC_SOURCE = `package clientv3 + +type List[T any] struct { + items []T +} + +// Set is a set of comparable values. +type Set[T comparable] = map[T]struct{} + +type Items[T any] = List[T] + +type Pairs[K comparable, V any] = map[K]List[V] +`; + +const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS'] as const; + +/** ` ` of every node but the file and its imports, in source order. */ +function declarations(result: ExtractionResult): string[] { + return result.nodes.filter((n) => n.kind !== 'file' && n.kind !== 'import').map((n) => `${n.kind} ${n.name}`); +} + +/** ` ` for every `references` ref out of a type declaration, sorted. */ +function typeRefs(result: ExtractionResult): string[] { + const byId = new Map(result.nodes.map((n) => [n.id, n])); + return result.unresolvedReferences + .filter((r) => r.referenceKind === 'references' && ['type_alias', 'struct', 'interface'].includes(byId.get(r.fromNodeId)?.kind ?? '')) + .map((r) => `${byId.get(r.fromNodeId)!.name} ${r.referenceName}`) + .sort(); +} + +describe('Go alias declarations are extracted', () => { + let savedEnv: Record = {}; + + beforeAll(async () => { + await initGrammars(); + await loadGrammarsForLanguages(['go']); + }); + + 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): ExtractionResult { + const result = tryKernelExtract(file, source, 'go'); + expect(result, `native extraction of ${file}`).not.toBeNull(); + return result!; + } + + /** Each type ref sits on its name, where resolution reads a package qualifier back. */ + function expectRefsOnTheirNames(result: ExtractionResult, source: string): void { + const lines = source.split(/\r?\n/); + const refs = result.unresolvedReferences.filter((r) => r.referenceKind === 'references'); + expect(refs.length).toBeGreaterThan(0); + for (const r of refs) { + expect(lines[r.line - 1]!.startsWith(r.referenceName, r.column), `${r.referenceName} at ${r.line}:${r.column}`).toBe(true); + } + } + + 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)(`an alias is a node of its type's kind: %s${label}`, (backend) => { + const result = extract(backend, 'client/v3/watch.go', eol(ALIAS_SOURCE)); + expect(declarations(result)).toEqual([ + 'type_alias Event', + 'type_alias WatchChan', + 'struct WatchResponse', + 'type_alias Local', + 'type_alias Ptr', + 'type_alias Slice', + 'type_alias Handler', + 'type_alias Index', + 'type_alias Defined', + 'type_alias Plain', + 'struct Anon', + 'interface Closer', + 'method Close', + 'type_alias Type', + 'type_alias Word', + ]); + const event = result.nodes.find((n) => n.name === 'Event')!; + expect(event.isExported).toBe(true); + expect(event.startLine).toBe(10); + // A doc comment is read as for any type declaration: inside a group. + expect(result.nodes.find((n) => n.name === 'Local')!.docstring).toBe('Local names a type of this package.'); + // Nested under the file, as a defined type is. + const file = result.nodes.find((n) => n.kind === 'file')!; + expect(result.edges.some((e) => e.kind === 'contains' && e.source === file.id && e.target === event.id)).toBe(true); + }); + + it.each(backends)(`an alias references the types it names: %s${label}`, (backend) => { + const source = eol(ALIAS_SOURCE); + const result = extract(backend, 'client/v3/watch.go', source); + // The package of mvccpb.Event / context.Context stays in the source; + // predeclared types (string, uint, error) are no references. + expect(typeRefs(result)).toEqual([ + 'Event Event', + 'Handler Context', + 'Handler WatchResponse', + 'Index KeyValue', + 'Local WatchResponse', + 'Plain List', + 'Plain WatchResponse', + 'Ptr WatchResponse', + 'Slice WatchResponse', + ]); + expectRefsOnTheirNames(result, source); + }); + } + + it.each([false, true])('a generic alias is an alias without its type parameters (native; CRLF: %s)', (crlf) => { + const source = crlf ? GENERIC_SOURCE.replace(/\n/g, '\r\n') : GENERIC_SOURCE; + const result = extract('kernel', 'client/v3/set.go', source); + expect(declarations(result)).toEqual(['struct List', 'type_alias Set', 'type_alias Items', 'type_alias Pairs']); + expect(result.nodes.find((n) => n.name === 'Set')!.isExported).toBe(true); + expect(typeRefs(result)).toEqual(['Items List', 'Pairs List']); + expectRefsOnTheirNames(result, source); + }); +}); + +describe('an indexed Go module resolves through its aliases', () => { + let root = ''; + let cg: CodeGraph; + + beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-alias-')); + const files: Record = { + 'go.mod': 'module go.etcd.io/etcd\n\ngo 1.24\n', + 'api/v3/mvccpb/kv.go': [ + 'package mvccpb', + '', + 'type Event struct {', + '\tType int', + '}', + '', + 'func (e *Event) IsCreate() bool { return e.Type == 0 }', + '', + ].join('\n'), + // A namesake package that sorts first, so a lookup by name meets its + // `Event` and `Event::IsCreate` before mvccpb's. + 'alpha/event.go': [ + 'package alpha', + '', + 'type Event struct{}', + '', + 'func (e *Event) IsCreate() bool { return false }', + '', + ].join('\n'), + 'client/v3/watch.go': [ + 'package clientv3', + '', + 'import "go.etcd.io/etcd/api/v3/mvccpb"', + '', + 'type Event = mvccpb.Event', + '', + 'type WatchResponse struct {', + '\tEvents []*Event', + '}', + '', + 'func (wr *WatchResponse) Err() error { return nil }', + '', + 'type (', + '\tLocal = WatchResponse', + '\tPtr = *Local', + ')', + '', + 'func newEvent() Event { return Event{} }', + '', + 'func isCreate(e *Event) bool { return e.IsCreate() }', + '', + 'func check(p Ptr) error { return p.Err() }', + '', + ].join('\n'), + // A generic alias exercises native parse-error recovery. + 'client/v3/list.go': [ + 'package clientv3', + '', + 'type List[T any] struct{ items []T }', + '', + 'func (l *List[T]) Len() int { return len(l.items) }', + '', + 'type Items[T any] = List[T]', + '', + 'func count(xs *Items[int]) int { return xs.Len() }', + '', + ].join('\n'), + // prometheus's discovery/xds: methods written on the alias are SDConfig's. + 'discovery/kuma.go': [ + 'package discovery', + '', + 'type Config interface {', + '\tName() string', + '}', + '', + 'type SDConfig struct{}', + '', + 'type KumaSDConfig = SDConfig', + '', + 'func (*KumaSDConfig) Name() string { return "kuma" }', + '', + ].join('\n'), + 'watcher/watcher.go': [ + 'package watcher', + '', + 'import "go.etcd.io/etcd/client/v3"', + '', + '// The package is clientv3, though the index knows the import as v3 or client.', + 'type Event = clientv3.Event', + '', + ].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 }); + }, 60_000); + + afterAll(() => { + cg?.destroy(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + /** `kind file:qualifiedName` of each non-contains edge out of `name` in `file`, sorted. */ + const linksFrom = (file: string, name: string) => { + const sources = cg.getNodesInFile(file).filter((n) => n.name === name && n.kind !== 'import'); + expect(sources, `${name} in ${file}`).toHaveLength(1); + const links = cg.getOutgoingEdgesFrom(sources.map((n) => n.id)) + .filter((e) => e.kind !== 'contains') + .map((e) => ({ kind: e.kind, target: cg.getNode(e.target)! })) + .map(({ kind, target }) => `${kind} ${target.filePath}:${target.qualifiedName}`); + return [...new Set(links)].sort(); + }; + + it('an alias references the type it names, in the package it is written through', () => { + expect(linksFrom('client/v3/watch.go', 'Event')).toEqual(['references api/v3/mvccpb/kv.go:Event']); + expect(linksFrom('client/v3/watch.go', 'Local')).toEqual(['references client/v3/watch.go:WatchResponse']); + expect(linksFrom('client/v3/watch.go', 'Ptr')).toEqual(['references client/v3/watch.go:Local']); + expect(linksFrom('client/v3/list.go', 'Items')).toEqual(['references client/v3/list.go:List']); + }); + + it('a bare name in the alias package is the alias', () => { + expect(linksFrom('client/v3/watch.go', 'newEvent')).toEqual([ + 'instantiates client/v3/watch.go:Event', + 'references client/v3/watch.go:Event', + ]); + }); + + it('a method called on an alias is the aliased type’s', () => { + expect(linksFrom('client/v3/watch.go', 'isCreate')).toEqual([ + 'calls api/v3/mvccpb/kv.go:Event::IsCreate', + 'references client/v3/watch.go:Event', + ]); + // Through two aliases, one of a pointer. + expect(linksFrom('client/v3/watch.go', 'check')).toEqual([ + 'calls client/v3/watch.go:WatchResponse::Err', + 'references client/v3/watch.go:Ptr', + ]); + expect(linksFrom('client/v3/list.go', 'count')).toEqual([ + 'calls client/v3/list.go:List::Len', + 'references client/v3/list.go:Items', + ]); + }); + + it('an alias owning methods written on it implements what they satisfy', () => { + expect(linksFrom('discovery/kuma.go', 'KumaSDConfig')).toEqual([ + 'implements discovery/kuma.go:Config', + 'references discovery/kuma.go:SDConfig', + ]); + // A call through Config.Name reaches the method written on the alias. + const config = cg.getNodesInFile('discovery/kuma.go').find((n) => n.name === 'Config')!; + const name = cg.getOutgoingEdgesFrom([config.id], ['contains']).map((e) => cg.getNode(e.target)!).find((n) => n.name === 'Name')!; + expect(cg.getOutgoingEdgesFrom([name.id], ['calls']).map((e) => cg.getNode(e.target)?.qualifiedName)).toEqual(['KumaSDConfig::Name']); + }); + + it('an alias written through a package the index does not know links to nothing', () => { + // Not to itself, nor to client/v3's, mvccpb's or alpha's `Event`. + expect(linksFrom('watcher/watcher.go', 'Event')).toEqual([]); + const event = cg.getNodesInFile('watcher/watcher.go').find((n) => n.name === 'Event')!; + expect(cg.getUnresolvedReferencesFrom(event.id).map((r) => `${r.referenceKind} ${r.referenceName}`)).toEqual(['references Event']); + }); +}); diff --git a/__tests__/go-type-position-kinds.test.ts b/__tests__/go-type-position-kinds.test.ts new file mode 100644 index 0000000000..9e788d1da5 --- /dev/null +++ b/__tests__/go-type-position-kinds.test.ts @@ -0,0 +1,260 @@ +/** + * A Go type position — a parameter or result type, a composite literal's + * type — names a type: a struct, an interface or a defined type, of the + * package Go reads the name from. Never a method or a function, which Go + * reaches only through a value or a package. Name matching took whatever + * declaration shared the name: etcd's + * `func (ti *treeIndex) KeyIndex(keyi *keyIndex) *keyIndex` linked both + * `keyIndex` types to the method `treeIndex.keyIndex` beside it, prometheus's + * `(ec2Client, error)` result to the method `EC2Discovery.ec2Client` it + * declares, every `samples{…}` literal to `sampleRing.samples`, and + * `&config_util.URL{…}` (an outside package) to `Target.URL`. A generic + * receiver's `[T]` declares `T` too: `func (p *Pool[T]) Get() T` returns the + * pool's own element type, not the method `Sample.T` of another package. + */ +import { describe, it, expect, afterAll, beforeAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; +import { goTypePositionTarget } from '../src/resolution/go-type-visibility'; +import type { ResolutionContext, UnresolvedRef } from '../src/resolution/types'; + +let root = ''; +let cg: CodeGraph; + +beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-type-pos-')); + const files: Record = { + 'go.mod': 'module example.com/app\n\ngo 1.22\n', + 'mvcc/key_index.go': `package mvcc + +type keyIndex struct { + key []byte +} + +type revisions []int64 +`, + 'mvcc/index.go': `package mvcc + +type treeIndex struct { + tree map[string]*keyIndex +} + +func (ti *treeIndex) KeyIndex(keyi *keyIndex) *keyIndex { + return ti.keyIndex(keyi) +} + +func (ti *treeIndex) keyIndex(keyi *keyIndex) *keyIndex { + return &keyIndex{key: keyi.key} +} + +func (ti *treeIndex) revisions() revisions { + return revisions{1, 2} +} +`, + 'mvcc/watchable_store.go': `package mvcc + +type contains interface { + contains(rev int64) bool +} + +func kvsToEvents(c contains, revs [][]byte) {} +`, + 'discovery/aws/ec2.go': `package aws + +import "context" + +type ec2Client interface { + DescribeInstances(ctx context.Context) error +} + +type EC2Discovery struct { + client ec2Client +} + +func (d *EC2Discovery) ec2Client(ctx context.Context) (ec2Client, error) { + return d.client, nil +} +`, + 'util/zeropool/pool.go': `package zeropool + +type Pool[T any] struct { + items []T +} + +func (p *Pool[T]) Get() T { + var zero T + return zero +} + +func (p *Pool[T]) Put(item T) {} + +type LazyLoader[K comparable, V any] struct{} + +func (l *LazyLoader[K, V]) Load(key K) (V, error) { + var v V + return v, nil +} +`, + 'prompb/types.pb.go': `package prompb + +type Histogram struct { + Count isHistogram_Count +} + +type isHistogram_Count interface { + isHistogram_Count() +} + +type Histogram_CountInt struct { + CountInt uint64 +} + +func (*Histogram_CountInt) isHistogram_Count() {} + +func (m *Histogram) GetCount() isHistogram_Count { + return m.Count +} +`, + 'prompb/codec.go': `package prompb + +func FromIntHistogram(count uint64) Histogram { + return Histogram{ + Count: &Histogram_CountInt{CountInt: count}, + } +} +`, + 'prompb/io/prometheus/write/v2/types.pb.go': `package writev2 + +type Histogram_CountInt struct { + CountInt uint64 +} + +type Sample struct { + ts int64 +} + +func (s Sample) T() int64 { return s.ts } +`, + 'model/value/value.go': `package value + +type T struct{} + +type K struct{} +`, + 'storage/remote/client_test.go': `package remote + +import ( + "testing" + + config_util "github.com/prometheus/common/config" +) + +type ClientConfig struct { + URL *config_util.URL +} + +func TestStoreHTTPErrorHandling(t *testing.T) { + conf := &ClientConfig{ + URL: &config_util.URL{URL: nil}, + } + _ = conf +} +`, + 'scrape/target.go': `package scrape + +type Target struct{} + +func (t *Target) URL() string { return "" } +`, + }; + 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 }); +}); + +afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); +}); + +/** The nodes of `file`, or the one named `from`. */ +const sourcesIn = (file: string, from?: string) => cg.getNodesInFile(file).filter((n) => !from || n.name === from); + +/** + * `kind file:qualifiedName` of each type-position edge (a `references` or an + * `instantiates`) out of `file`'s nodes whose target is named `name`, + * deduplicated and sorted. + */ +const typeLinksFrom = (file: string, name: string, from?: string) => { + const links = cg.getOutgoingEdgesFrom(sourcesIn(file, from).map((n) => n.id)) + .filter((e) => e.kind === 'references' || e.kind === 'instantiates') + .map((e) => ({ kind: e.kind, target: cg.getNode(e.target)! })) + .filter(({ target }) => target.name === name) + .map(({ kind, target }) => `${kind} ${target.filePath}:${target.qualifiedName}`); + return [...new Set(links)].sort(); +}; + +describe('A Go type position resolves to a type', () => { + it('an unexported parameter and result type is its package’s struct, not a method of that name', () => { + expect(typeLinksFrom('mvcc/index.go', 'keyIndex', 'KeyIndex')).toEqual(['references mvcc/key_index.go:keyIndex']); + expect(typeLinksFrom('mvcc/index.go', 'keyIndex', 'keyIndex')).toEqual([ + 'instantiates mvcc/key_index.go:keyIndex', + 'references mvcc/key_index.go:keyIndex', + ]); + }); + + it('the method a call names stays a method', () => { + const calls = cg.getOutgoingEdgesFrom(sourcesIn('mvcc/index.go', 'KeyIndex').map((n) => n.id)) + .filter((e) => e.kind === 'calls') + .map((e) => cg.getNode(e.target)!.qualifiedName); + expect(calls).toEqual(['treeIndex::keyIndex']); + }); + + it('a defined type in a result and a composite literal is that type', () => { + expect(typeLinksFrom('mvcc/index.go', 'revisions', 'revisions')).toEqual([ + 'instantiates mvcc/key_index.go:revisions', + 'references mvcc/key_index.go:revisions', + ]); + }); + + it('an interface whose method shares its name is the interface', () => { + expect(typeLinksFrom('mvcc/watchable_store.go', 'contains', 'kvsToEvents')).toEqual(['references mvcc/watchable_store.go:contains']); + expect(typeLinksFrom('prompb/types.pb.go', 'isHistogram_Count', 'GetCount')).toEqual(['references prompb/types.pb.go:isHistogram_Count']); + }); + + it('a result type is the interface, not the method the line declares', () => { + expect(typeLinksFrom('discovery/aws/ec2.go', 'ec2Client', 'ec2Client')).toEqual(['references discovery/aws/ec2.go:ec2Client']); + }); + + it('a bare composite literal is its own package’s struct, not a namesake in another package', () => { + expect(typeLinksFrom('prompb/codec.go', 'Histogram_CountInt')).toEqual(['instantiates prompb/types.pb.go:Histogram_CountInt']); + }); + + it('a generic receiver’s type parameters are type parameters', () => { + expect(typeLinksFrom('util/zeropool/pool.go', 'T', 'Get')).toEqual([]); + expect(typeLinksFrom('util/zeropool/pool.go', 'T', 'Put')).toEqual([]); + expect(typeLinksFrom('util/zeropool/pool.go', 'K', 'Load')).toEqual([]); + expect(typeLinksFrom('util/zeropool/pool.go', 'V', 'Load')).toEqual([]); + }); + + it('a composite literal of an outside package’s type is no project method of that name', () => { + expect(typeLinksFrom('storage/remote/client_test.go', 'URL')).toEqual([]); + }); +}); + +it('an imported module-root type replaces a namesake function', () => { + const wanted = { id: 'root-type', name: 'Widget', kind: 'struct', language: 'go', filePath: 'widget.go' }; + const functionNode = { ...wanted, id: 'function', kind: 'function', filePath: 'decoy/func.go' }; + const context = { + getNodeById: () => functionNode, + getNodesByName: () => [functionNode, wanted], + getImportMappings: () => [{ localName: 'kit', source: 'example.com/kit' }], + getGoPackageDir: () => '', + } as unknown as ResolutionContext; + const ref: UnresolvedRef = { fromNodeId: 'caller', referenceName: 'kit.Widget', referenceKind: 'references', filePath: 'app/main.go', language: 'go', line: 1, column: 0 }; + expect(goTypePositionTarget({ original: ref, targetNodeId: 'function', confidence: 0.7, resolvedBy: 'framework' }, ref, context)?.targetNodeId).toBe('root-type'); +}); diff --git a/__tests__/import-default-as.test.ts b/__tests__/import-default-as.test.ts new file mode 100644 index 0000000000..2193568b71 --- /dev/null +++ b/__tests__/import-default-as.test.ts @@ -0,0 +1,255 @@ +/** + * `import { default as X } from './x'` is the module's default import, spelled + * as a named one. The import mapping recorded it as a named import of an + * export called `default`, which no module declares, so a call, a route or a + * JSX attribute naming `X` never resolved through the import. It fell through + * to matching the name alone, which can bind another file's `X`, and binds + * nothing (or a stranger) when the default export has a name of its own. + * bulletproof-react's router imports its app shell this way: + * `import { default as AppRoot, ErrorBoundary as AppRootErrorBoundary } from + * './routes/app/root'`. + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; +import { importMappingsFromBindings } from '../src/resolution/import-resolver'; +import { extractFromSource } from '../src/extraction'; +import type { Language } from '../src/types'; +import type { Edge } from '../src/types'; + +const mappings = (source: string, language: Language = 'tsx') => + (importMappingsFromBindings(extractFromSource(`src/a.${language === 'svelte' || language === 'vue' ? language : 'tsx'}`, source, language).bindings ?? []) ?? []).map( + (m) => `${m.localName}<${m.exportedName}${m.isDefault ? ' default' : ''}${m.isNamespace ? ' ns' : ''}` + ); + +describe('`{ default as X }` maps to the default import', () => { + it('beside a named import, across lines, with a trailing comma', () => { + expect(mappings(`import {\n default as AppRoot,\n ErrorBoundary as AppRootErrorBoundary,\n} from './routes/app/root';\n`)).toEqual([ + 'AppRoot { + expect(mappings(`import { default as Settings } from './settings';\n`)).toEqual(['Settings { + expect(mappings(`import { defaults as d, defaultTheme as theme } from './theme';\n`)).toEqual([ + 'd { + const sfc = `\n\n\n`; + expect(mappings(sfc, 'svelte')).toEqual(['Card { + let root = ''; + let cg: CodeGraph; + + beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-default-as-')); + const files: Record = { + 'package.json': JSON.stringify({ name: 'web', dependencies: { react: '^18', 'react-router': '^7' } }), + // bulletproof-react's shape: the default export is a const declared + // above it, beside a named ErrorBoundary. + 'src/app/routes/app/root.tsx': `import { Outlet } from 'react-router'; + +export const ErrorBoundary = () => { + return
Something went wrong!
; +}; + +const AppRoot = () => { + return ( +
+ +
+ ); +}; + +export default AppRoot; +`, + // A default export imported under a name that is not its own. + 'src/app/routes/app/settings.tsx': `export default function SettingsRoute() { + return
settings
; +} +`, + 'src/components/card.tsx': `import { memo } from 'react'; + +const Card = memo(() =>
); + +export default Card; +`, + 'src/components/layout.tsx': `export function Layout({ sidebar, children }: { sidebar: unknown; children: unknown }) { + return
{children}
; +} +`, + // Other symbols with the importing file's local names. Matching by name + // alone can only choose between these and the real targets. + 'src/app/legacy.tsx': `export function AppRoot() { + return
legacy shell
; +} + +export function Settings() { + return
legacy settings
; +} +`, + 'src/app/router.tsx': `import { createBrowserRouter } from 'react-router'; + +import { + default as AppRoot, + ErrorBoundary as AppRootErrorBoundary, +} from './routes/app/root'; +import { default as Settings } from './routes/app/settings'; + +export const createAppRouter = () => + createBrowserRouter([ + { + path: '/app', + element: , + ErrorBoundary: AppRootErrorBoundary, + children: [{ path: 'settings', Component: Settings }], + }, + ]); + +export function renderShell() { + return AppRoot(); +} +`, + 'src/app/AppRoutes.tsx': `import { Routes, Route } from 'react-router'; + +import { Layout } from '../components/layout'; +import { default as Settings } from './routes/app/settings'; +import { default as Tile } from '../components/card'; + +export function AppRoutes() { + return ( + + } /> + + ); +} + +export function Gallery() { + return ( + + + + ); +} +`, + }; + 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 }); + }); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + const node = (file: string, name: string) => { + const found = cg.getNodesInFile(file).find((n) => n.name === name && n.kind !== 'import'); + expect(found, `${file}: ${name}`).toBeDefined(); + return found!; + }; + /** The edges into `target`, as `kind source-name resolvedBy`. */ + const into = (target: { id: string }, kinds: Edge['kind'][]) => + cg + .getIncomingEdges(target.id) + .filter((e) => kinds.includes(e.kind)) + .map((e) => { + const meta = (e.metadata ?? {}) as { resolvedBy?: string; synthesizedBy?: string }; + return `${e.kind} ${cg.getNode(e.source)!.name} ${meta.resolvedBy ?? meta.synthesizedBy}`; + }) + .sort(); + const route = (name: string) => { + const found = cg.getNodesByKind('route').find((r) => r.name === name); + expect(found, `route ${name}`).toBeDefined(); + return found!; + }; + const renders = (name: string) => + cg + .getOutgoingEdges(route(name).id) + .filter((e) => e.kind === 'references') + .map((e) => { + const target = cg.getNode(e.target)!; + return `${target.filePath}:${target.name} ${(e.metadata as { resolvedBy?: string }).resolvedBy}`; + }) + .sort(); + + it('a call reaches the module’s default export through the import', () => { + expect(into(node('src/app/routes/app/root.tsx', 'AppRoot'), ['calls'])).toEqual([ + 'calls createAppRouter jsx-render', + 'calls renderShell import', + ]); + expect(into(node('src/app/legacy.tsx', 'AppRoot'), ['calls', 'references'])).toEqual([]); + }); + + it('a data router’s `element` and `Component` render the default exports', () => { + expect(renders('/app')).toEqual(['src/app/routes/app/root.tsx:AppRoot import']); + // A child route renders inside its parent's element, its layout. + expect(renders('/app/settings')).toEqual([ + 'src/app/routes/app/root.tsx:AppRoot framework', + 'src/app/routes/app/settings.tsx:SettingsRoute import', + ]); + }); + + it('a JSX route and a JSX attribute name the default export', () => { + expect(renders('/preferences')).toEqual(['src/app/routes/app/settings.tsx:SettingsRoute import']); + // The router's `Component: Settings` is a function reference as well. + expect(into(node('src/app/routes/app/settings.tsx', 'SettingsRoute'), ['references'])).toEqual([ + 'references /app/settings import', + 'references /preferences import', + 'references Gallery import', + 'references createAppRouter import', + ]); + expect(into(node('src/app/legacy.tsx', 'Settings'), ['references'])).toEqual([]); + }); + + it('a JSX tag renders the default-exported component it was imported as', () => { + expect(into(node('src/components/card.tsx', 'Card'), ['calls'])).toEqual(['calls Gallery jsx-render']); + }); + + it('the named import beside it is unchanged', () => { + expect(into(node('src/app/routes/app/root.tsx', 'ErrorBoundary'), ['references'])).toEqual([ + 'references createAppRouter import', + ]); + }); + + it('the import binding links the module, as a default import’s does', () => { + const file = cg.getNodesInFile('src/app/router.tsx').find((n) => n.kind === 'file')!; + const bindings = cg + .getOutgoingEdges(file.id) + .filter((e) => e.kind === 'imports') + .map((e) => { + const meta = e.metadata as { refName?: string; resolvedBy?: string }; + const target = cg.getNode(e.target)!; + return `${meta.refName} → ${target.kind} ${target.filePath} ${meta.resolvedBy}`; + }) + .filter((s) => !s.startsWith('.')) + .sort(); + expect(bindings).toEqual([ + 'AppRoot → file src/app/routes/app/root.tsx import', + 'AppRootErrorBoundary → function src/app/routes/app/root.tsx import', + 'Settings → file src/app/routes/app/settings.tsx import', + ]); + }); +}); diff --git a/__tests__/js-local-binding-backtracking.test.ts b/__tests__/js-local-binding-backtracking.test.ts new file mode 100644 index 0000000000..5746cd3e85 --- /dev/null +++ b/__tests__/js-local-binding-backtracking.test.ts @@ -0,0 +1,73 @@ +/** + * Whether a JS function binds a name itself (jsFunctionLocalScope, #2226) is + * read off the function's comment-stripped code with a parameter-list regex. + * Its `(? { + let tmpDir: string | undefined; + let cg: CodeGraph | undefined; + + afterEach(() => { + cg?.close(); + cg = undefined; + if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5 }); + tmpDir = undefined; + }); + + const callees = (graph: CodeGraph, name: string): string[] => { + const from = graph.getNodesByName(name).find((n) => n.kind === 'function')!; + return graph.getOutgoingEdges(from.id).filter((e) => e.kind === 'calls') + .map((e) => graph.getNode(e.target)!.name); + }; + + it('stays linear when V8 stops optimizing regexes', async () => { + // Blanked by the comment stripper into one 60,000-character run of spaces. + const comment = `/* ${'word '.repeat(12_000)} */`; + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-js-lookbehind-')); + fs.writeFileSync(path.join(tmpDir, 'paint.js'), [ + 'function paintWidget() {\n return 1;\n}', + // Calls the file's own paintWidget: the check scans all of host's code and finds no binding. + `function host() {\n ${comment}\n return paintWidget();\n}`, + // Calls its parameter, which shadows the file's paintWidget. + `function shadow(paintWidget) {\n ${comment}\n return paintWidget();\n}`, + '', + ].join('\n')); + + v8.setFlagsFromString('--no-regexp-optimization'); + const started = Date.now(); + try { + cg = CodeGraph.initSync(tmpDir); + await cg.indexAll(); + } finally { + v8.setFlagsFromString('--regexp-optimization'); + } + const elapsed = Date.now() - started; + + expect(callees(cg, 'host')).toEqual(['paintWidget']); + expect(callees(cg, 'shadow')).toEqual([]); + // About a second with the fix; minutes when the lookbehind reads back through the run at every position. + expect(elapsed).toBeLessThan(20_000); + }, 300_000); +}); diff --git a/__tests__/jsx-render-work.test.ts b/__tests__/jsx-render-work.test.ts new file mode 100644 index 0000000000..68f5ae4b87 --- /dev/null +++ b/__tests__/jsx-render-work.test.ts @@ -0,0 +1,84 @@ +/** + * The jsx-render pass reads each function's own lines for the component tags + * it renders. Every function of a minified bundle spans the bundle's one line, + * so it split, sliced and scanned the whole file once per function: 2,234 + * times over go-ethereum's 980 KB `graphiql.min.js`, 25 of the 28 seconds its + * linking passes took on Node 22 (9 of 13 on Node 24). Functions that span + * the same lines render the same tags, so they share one scan. Counted, never + * timed. + */ +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'; + +/** The source of the pass's tag pattern, to count its scans by. */ +const JSX_TAG_SOURCE = String.raw`<([A-Z][A-Za-z0-9_]*)[\s/>]`; + +describe('jsx-render pass work', () => { + let tmpDir: string | undefined; + let cg: CodeGraph | undefined; + const originalExec = RegExp.prototype.exec; + + afterEach(() => { + RegExp.prototype.exec = originalExec; + cg?.close(); + cg = undefined; + if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5 }); + tmpDir = undefined; + }); + + async function index(files: Record): Promise { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-jsx-work-')); + fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"dependencies":{"react":"^18.0.0"}}'); + for (const [file, content] of Object.entries(files)) fs.writeFileSync(path.join(tmpDir, file), content); + cg = CodeGraph.initSync(tmpDir); + await cg.indexAll(); + return cg; + } + + /** What each named function renders, by jsx-render edge. */ + const renders = (graph: CodeGraph, name: string): string[] => { + const from = graph.getNodesByName(name).find((n) => n.kind === 'function' || n.kind === 'component')!; + return graph.getOutgoingEdges(from.id) + .filter((e) => (e.metadata as { synthesizedBy?: string } | undefined)?.synthesizedBy === 'jsx-render') + .map((e) => graph.getNode(e.target)!.name); + }; + + it('scans a line that many components share once, not once per component', async () => { + const COMPONENTS = 60; + const bundle = Array.from({ length: COMPONENTS }, (_, i) => `function Card${i}(){return }`).join(''); + // A scan of the bundle's line: the tag pattern run over it from the start. + let scans = 0; + RegExp.prototype.exec = function (this: RegExp, input: string) { + if (this.source === JSX_TAG_SOURCE && this.lastIndex === 0 && typeof input === 'string' && + input.includes(`Card${COMPONENTS - 1}(`)) scans++; + return originalExec.call(this, input); + }; + const graph = await index({ + 'bundle.jsx': `${bundle}\n`, + 'badge.jsx': 'export function Badge(){return null}\n', + }); + RegExp.prototype.exec = originalExec; + + for (const i of [0, 31, COMPONENTS - 1]) expect(renders(graph, `Card${i}`)).toEqual(['Badge']); + expect(scans).toBeGreaterThan(0); + expect(scans).toBeLessThan(COMPONENTS / 4); + }); + + it('keeps the tags of functions on different lines apart', async () => { + const graph = await index({ + 'page.jsx': [ + 'export function Page() { return
; }', + 'export function Footer() {', + ' return ;', + '}', + 'function Header(){return null} function Links(){return null}', + '', + ].join('\n'), + }); + expect(renders(graph, 'Page')).toEqual(['Header']); + expect(renders(graph, 'Footer')).toEqual(['Links']); + }); +}); diff --git a/__tests__/react-router-nested-lazy.test.ts b/__tests__/react-router-nested-lazy.test.ts index 545d409bf8..2a8fa44202 100644 --- a/__tests__/react-router-nested-lazy.test.ts +++ b/__tests__/react-router-nested-lazy.test.ts @@ -75,11 +75,14 @@ afterAll(() => { if (root) fs.rmSync(root, { recursive: true, force: true }); }); -const renders = (routeName: string): string[] => { +const bindings = (routeName: string, layout: boolean): string[] => { const route = cg.getNodesByKind('route').find((r) => r.name === routeName); if (!route) return []; - return cg.getOutgoingEdges(route.id).filter((e) => e.kind === 'references').map((e) => cg.getNode(e.target)!.name); + return cg.getOutgoingEdges(route.id) + .filter((e) => e.kind === 'references' && Boolean((e.metadata as Record | undefined)?.layout) === layout) + .map((e) => cg.getNode(e.target)!.name); }; +const renders = (routeName: string): string[] => bindings(routeName, false); describe('nested, lazy and constant React Router routes', () => { it('compose their paths and render their modules', () => { @@ -90,6 +93,11 @@ describe('nested, lazy and constant React Router routes', () => { expect(renders('/app/discussions')).toEqual(['DiscussionsRoute']); }); + it('a child route renders inside its parent’s component, the layout around it', () => { + expect(bindings('/app/discussions', true)).toEqual(['AppRoot']); + expect(bindings('/', true)).toEqual([]); + }); + it('resolves an app’s own tsconfig alias', () => { const router = cg.getNodesInFile('apps/web/src/app/router.tsx').map((n) => n.id); const imported = cg.getOutgoingEdgesFrom(router).filter((e) => e.kind === 'imports').map((e) => cg.getNode(e.target)!.filePath); diff --git a/__tests__/react-router.test.ts b/__tests__/react-router.test.ts index fa5cd56e16..be3a4a5801 100644 --- a/__tests__/react-router.test.ts +++ b/__tests__/react-router.test.ts @@ -10,7 +10,7 @@ * is what the app-root gate has to get right. Mirrors `nextjs.test.ts`. */ -import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest'; +import { describe, it, expect, beforeAll, afterAll, afterEach, vi } from 'vitest'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; @@ -20,6 +20,7 @@ import { buildScreens } from '../src/ui-server/api/screens'; import { buildSteps } from '../src/ui-server/api/steps'; import { reactRouterRoot, reactRouterNavVerb } from '../src/resolution/frameworks/react-router'; import type { Node } from '../src/types'; +import { ReferenceResolver } from '../src/resolution'; // ============================================================================= // The app root a route file owns @@ -516,9 +517,10 @@ describe('react-router: route declaration boundaries (#1348)', () => { const routes = cg.getNodesByKind('route'); return { paths: routes.map((route) => route.name).sort(), + // `->` what a route renders, `~>` a layout it renders inside. bindings: routes.flatMap((route) => cg!.getOutgoingEdges(route.id) .filter((edge) => edge.kind === 'references') - .map((edge) => `${route.name}->${cg!.getNode(edge.target)?.name}`)).sort(), + .map((edge) => `${route.name}${(edge.metadata as Record | undefined)?.layout ? '~>' : '->'}${cg!.getNode(edge.target)?.name}`)).sort(), }; } @@ -548,9 +550,11 @@ describe('react-router: route declaration boundaries (#1348)', () => { } `, extension); expect(result).toEqual({ - // A nested route's path is relative to its parent's (`settings` under `/dashboard`). + // A nested route's path is relative to its parent's (`settings` under + // `/dashboard`). The index route is the page at `/dashboard`; the `` around it only groups, so it is not a second, empty `/dashboard`. paths: ['/dashboard', '/dashboard/settings', '/empty', '/legacy', '/long', '/no-element', '/sibling'], - bindings: ['/dashboard/settings->Settings', '/legacy->Settings', '/long->Shell', '/sibling->Settings'], + bindings: ['/dashboard->DashboardHome', '/dashboard/settings->Settings', '/legacy->Settings', '/long->Shell', '/sibling->Settings'], }); }); @@ -574,8 +578,136 @@ describe('react-router: route declaration boundaries (#1348)', () => { ]); `, extension); expect(result).toEqual({ - paths: ['/', '/data/prefs', '/long', '/quoted', '/sibling'], - bindings: ['/->DataSettings', '/data/prefs->DataSettings', '/long->DataSettings', '/quoted->DataSettings', '/sibling->DataSettings'], + // `{ index: true, Component: DataIndex }` is the page at its parent's address. + paths: ['/', '/data', '/data/prefs', '/long', '/quoted', '/sibling'], + bindings: ['/->DataSettings', '/data->DataIndex', '/data/prefs->DataSettings', '/long->DataSettings', '/quoted->DataSettings', '/sibling->DataSettings'], + }); + }); + + it.each(['tsx', 'jsx', 'js'])('reads what a route renders past placeholders, guards and line breaks in %s', async (extension) => { + const result = await index(` + import { Suspense } from 'react'; + import { Routes, Route } from 'react-router-dom'; + function Loader() { return null; } + function AdminPanel() { return null; } + function RequireAuth({ children }) { return children; } + function ProtectedPage() { return null; } + function PrivateRoot({ component }) { return component; } + function HomePage() { return null; } + function AuthPage() { return null; } + function RememberMe() { return null; } + export function App() { + return + }>} /> + + + + } + /> + } />} /> + }>} /> + } />} /> + ; + } + `, extension); + expect(result.bindings).toEqual([ + // A `fallback` is shown while the page loads; it is not the page. + '/admin->AdminPanel', + // A page handed to a guard as a prop. + '/home->HomePage', + // `AuthPage` reads like a guard's name (`Auth…`); when every tag does, the innermost is the page. + '/login->AuthPage', + // Prettier writes a long element on lines of its own. + '/protected->ProtectedPage', + // Only a `component`, `element` or `page` prop hands over a page. + '/signin->AuthPage', + ]); + }); + + it('reads an index route only where its parent’s address is written down', async () => { + const result = await index(` + import { Routes, Route } from 'react-router-dom'; + import { paths } from './paths'; + function Navigation() { return null; } + function Home() { return null; } + function CategoriesPreview() { return null; } + function Category() { return null; } + function ListAgents() { return null; } + function ListWorkspaces() { return null; } + // Mounted at \`shop/*\` below: its index is the page at \`/shop\`, which its own does not say. + function Shop() { + return + } /> + } /> + ; + } + export function App() { + return + }> + } /> + } /> + + } /> + + + } /> + + + ; + } + `, 'jsx'); + expect(result.paths.filter((p) => p === '/')).toEqual(['/']); + expect(result.bindings).toContain('/->Home'); + // Neither a component's own nor a path the file does not spell out says where these are. + expect(result.bindings.filter((b) => /CategoriesPreview|ListAgents/.test(b))).toEqual([]); + // A path in braces is still written down. + expect(result.bindings).toContain('/workspaces->ListWorkspaces'); + }); + + it.each(['tsx', 'jsx'])('reads an index route at the top of the router itself as `/` in %s', async (extension) => { + const result = await index(` + import { BrowserRouter, createBrowserRouter, createRoutesFromElements, Routes, Route } from 'react-router-dom'; + function Home() { return null; } + function About() { return null; } + function Dashboard() { return null; } + export function App() { + return + + } /> + } /> + + ; + } + export const router = createBrowserRouter(createRoutesFromElements(} />)); + `, extension); + expect(result).toEqual({ paths: ['/', '/', '/about'], bindings: ['/->Dashboard', '/->Home', '/about->About'] }); + }); + + it('keeps a version 5 catch-all out, and reads a version 3 parent route as the layout around its children', async () => { + const result = await index(` + import { Router, Route, Switch } from 'react-router'; + function App() { return null; } + function About() { return null; } + function NotFound() { return null; } + export const routes = ( + + + + + + + + + + ); + `, 'jsx'); + expect(result).toEqual({ + // No child claims `/`, so App is the page there as well as the layout around `/about`. + paths: ['/', '/about'], + bindings: ['/->App', '/about->About', '/about~>App'], }); }); @@ -692,3 +824,996 @@ export default function FeaturePage() { return

Features

; } expect(navsFrom('PrivateRoute')).toEqual(['redirect /']); }); }); + +// ============================================================================= +// Route tables another file hands the router +// ============================================================================= + +function writeFiles(root: string, files: Record): void { + 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); + } +} + +async function indexProject(files: Record): Promise<{ root: string; cg: CodeGraph }> { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rr-tables-')); + writeFiles(root, files); + return { root, cg: await CodeGraph.init(root, { index: true }) }; +} + +/** `path -> Component` for what each route renders, `path ~> Layout` for each layout around it. */ +function routeBindings(cg: CodeGraph): string[] { + return cg.getNodesByKind('route').flatMap((r) => cg.getOutgoingEdges(r.id) + .filter((e) => e.kind === 'references') + .map((e) => `${r.name} ${(e.metadata as Record | undefined)?.layout ? '~>' : '->'} ${cg.getNode(e.target)?.name}`)) + .sort(); +} + +/** `Source -> /path` for every navigation into a route. */ +function navigations(cg: CodeGraph): string[] { + return cg.getNodesByKind('route').flatMap((r) => cg.getIncomingEdges(r.id) + .filter((e) => e.kind === 'navigates') + .map((e) => `${cg.getNode(e.source)?.name} -> ${r.name}`)) + .sort(); +} + +const routeNames = (cg: CodeGraph): string[] => cg.getNodesByKind('route').map((r) => r.name).sort(); + +const component = (name: string): string => `export function ${name}() {\n return
${name}
;\n}\n`; + +/** A screen file whose component is its default export. */ +const screen = (name: string): string => `export default function ${name}() {\n return
${name}
;\n}\n`; + +/** jasontaylordev/CleanArchitecture's ClientApp-React and the ASP.NET Core React template it comes from. */ +const ASPNET_TEMPLATE: Record = { + 'src/Web/ClientApp/package.json': JSON.stringify({ name: 'web', dependencies: { react: '^18', 'react-router-dom': '^6' } }), + 'src/Web/ClientApp/src/AppRoutes.js': `import ApiAuthorzationRoutes from './components/api-authorization/ApiAuthorizationRoutes'; +import { Counter } from "./components/Counter"; +import { FetchData } from "./components/FetchData"; +import { Home } from "./components/Home"; +import { ProtectedRoute } from "./components/ProtectedRoute"; +import AdminWrapper from "./components/AdminWrapper"; +import { Users } from "./components/Users"; + +const AppRoutes = [ + { + index: true, + element: + }, + { + path: '/counter', + element: + }, + { + path: '/fetch-data', + requireAuth: true, + element: + }, + { + path: 'admin/users', + element: + }, + ...ApiAuthorzationRoutes +]; + +export default AppRoutes; +`, + 'src/Web/ClientApp/src/components/api-authorization/ApiAuthorizationRoutes.js': `import { Profile } from './Profile'; +import { Login } from './Login'; +import { ApplicationPaths } from './ApiAuthorizationConstants'; + +const ApiAuthorizationRoutes = [ + { + path: '/authentication/profile', + element: + }, + { + path: ApplicationPaths.Login, + element: loginAction('login') + } +]; + +function loginAction(name) { + return ; +} + +export default ApiAuthorizationRoutes; +`, + 'src/Web/ClientApp/src/components/api-authorization/ApiAuthorizationConstants.js': + "const prefix = '/authentication';\nexport const ApplicationPaths = {\n Login: `${prefix}/login`,\n};\n", + 'src/Web/ClientApp/src/App.js': `import React, { Component } from 'react'; +import { Route, Routes } from 'react-router-dom'; +import AppRoutes from './AppRoutes'; +import AuthorizeRoute from './components/api-authorization/AuthorizeRoute'; +import { Layout } from './components/Layout'; + +export default class App extends Component { + static displayName = App.name; + + render() { + return ( + + + {AppRoutes.map((route, index) => { + const { element, requireAuth, ...rest } = route; + return : element} />; + })} + + + ); + } +} +`, + 'src/Web/ClientApp/src/components/NavMenu.js': `import { Link, NavLink, useNavigate } from 'react-router-dom'; +import { menu } from '../menu'; + +export function NavMenu() { + const navigate = useNavigate(); + const showProfile = () => { + navigate('/authentication/profile'); + }; + return ( + + ); +} +`, + // `{ path, element }` lists that are not route tables: a menu rendered as + // links, a list nothing renders, and a table mapped into a path it does not + // hold (`layout + path`). + 'src/Web/ClientApp/src/menu.js': `import { HelpIcon, SettingsIcon } from './icons'; +export const menu = [ + { path: '/help', element: }, + { path: '/settings', element: }, +]; +`, + 'src/Web/ClientApp/src/breadcrumbs.js': `import { Crumb } from './icons'; +export const crumbs = [{ path: '/history', element: }]; +`, + 'src/Web/ClientApp/src/dashboard.js': `import { Route, Routes } from 'react-router-dom'; +import { Stats } from './icons'; +const dashboardRoutes = [{ path: '/stats', layout: '/admin', element: }]; +export function Dashboard() { + return {dashboardRoutes.map((r) => )}; +} +`, + 'src/Web/ClientApp/src/icons.js': component('HelpIcon') + component('SettingsIcon') + component('Crumb') + component('Stats'), + 'src/Web/ClientApp/src/components/Home.js': component('Home'), + 'src/Web/ClientApp/src/components/Counter.js': component('Counter'), + 'src/Web/ClientApp/src/components/FetchData.js': component('FetchData'), + 'src/Web/ClientApp/src/components/ProtectedRoute.js': 'export function ProtectedRoute({ children }) {\n return children;\n}\n', + 'src/Web/ClientApp/src/components/AdminWrapper.js': 'export default function AdminWrapper({ children }) {\n return children;\n}\n', + 'src/Web/ClientApp/src/components/Users.js': component('Users'), + 'src/Web/ClientApp/src/components/Layout.js': + "import { NavMenu } from './NavMenu';\nexport function Layout({ children }) {\n return
{children}
;\n}\n", + 'src/Web/ClientApp/src/components/api-authorization/Profile.js': component('Profile'), + 'src/Web/ClientApp/src/components/api-authorization/Login.js': component('Login'), + 'src/Web/ClientApp/src/components/api-authorization/AuthorizeRoute.js': + 'export default function AuthorizeRoute({ element }) {\n return element;\n}\n', +}; + +describe('react-router: a route table another file maps into (the ASP.NET Core React template)', () => { + let root: string; + let cg: CodeGraph; + beforeAll(async () => { + ({ root, cg } = await indexProject(ASPNET_TEMPLATE)); + }); + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + it('reads the table App.js spreads into , and the table it spreads in turn', () => { + expect(routeBindings(cg)).toEqual([ + // `index: true` at the top of the table is the app's `/`. + '/ -> Home', + // `` and + // `` render what they wrap. + '/admin/users -> Users', + '/authentication/profile -> Profile', + '/counter -> Counter', + '/fetch-data -> FetchData', + ]); + // An entry that renders a call's result names no component, so it is no route. + expect(routeNames(cg)).toEqual(['/', '/admin/users', '/authentication/profile', '/counter', '/fetch-data']); + }); + + it('keeps each route at the line of its own object in the table', () => { + const counter = cg.getNodesByKind('route').find((r) => r.name === '/counter')!; + expect(counter.filePath).toBe('src/Web/ClientApp/src/AppRoutes.js'); + expect(counter.startLine).toBe(15); + }); + + it('makes no route of a menu, a list nothing hands the router, or a path the table does not hold', () => { + for (const p of ['/help', '/settings', '/history', '/stats', '/admin/stats']) expect(routeNames(cg)).not.toContain(p); + }); + + it('gives the navigation that names those routes somewhere to go', () => { + expect(navigations(cg)).toEqual([ + 'NavMenu -> /', + 'NavMenu -> /counter', + 'NavMenu -> /fetch-data', + 'showProfile -> /authentication/profile', + ]); + }); + + it('lands on the Screens tab', async () => { + const screens = await buildScreens(cg, root); + expect(screens.routed).toBe(true); + expect(screens.screens.map((s) => s.path).sort()).toEqual(['/', '/admin/users', '/authentication/profile', '/counter', '/fetch-data']); + }); +}); + +describe('react-router: a table handed to useRoutes from another file, an index route under a layout', () => { + let root: string; + let cg: CodeGraph; + beforeAll(async () => { + ({ root, cg } = await indexProject({ + 'package.json': JSON.stringify({ name: 'kit', dependencies: { react: '^18', 'react-router-dom': '^6' } }), + // A table written as a function of the login state, the way many admin kits write it. + 'src/routes.jsx': `import { Navigate } from 'react-router-dom'; +import DashboardLayout from './layouts/DashboardLayout'; +import MainLayout from './layouts/MainLayout'; +import { Account, Dashboard, Landing, Login } from './pages'; + +const routes = (isLoggedIn) => [ + { + path: 'app', + element: isLoggedIn ? : , + children: [ + { path: 'dashboard', element: }, + { path: 'account', element: }, + ], + }, + { + path: '/', + element: , + children: [ + { index: true, element: }, + { path: 'login', element: }, + ], + }, +]; + +export default routes; +`, + 'src/App.jsx': `import { useRoutes } from 'react-router-dom'; +import routes from './routes'; + +export default function App({ isLoggedIn }) { + return useRoutes(routes(isLoggedIn)); +} +`, + 'src/layouts/MainLayout.jsx': `import { Link, Outlet } from 'react-router-dom'; +export default function MainLayout() { + return
Dashboard
; +} +`, + 'src/layouts/DashboardLayout.jsx': `import { Outlet } from 'react-router-dom'; +export default function DashboardLayout() { + return ; +} +`, + 'src/pages.jsx': `import { useNavigate } from 'react-router-dom'; +export function Dashboard() { return
Dashboard
; } +export function Account() { return
Account
; } +export function Landing() { return
Landing
; } +export function Login() { + const navigate = useNavigate(); + const onSubmit = () => navigate('/app/account'); + return
; +} +`, + })); + }); + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + it('reads the table the function returns, nested paths composed', () => { + expect(routeBindings(cg)).toEqual([ + // The index route is the page at `/`; MainLayout is the layout around it, not a second `/`. + '/ -> Landing', + '/ ~> MainLayout', + // `app`'s element is a choice made at runtime: a path to sit under, no layout. + '/app/account -> Account', + '/app/dashboard -> Dashboard', + '/login -> Login', + '/login ~> MainLayout', + ]); + }); + + it("draws a layout's links on every screen inside it", async () => { + expect(navigations(cg)).toEqual([ + 'MainLayout -> /app/dashboard', + 'onSubmit -> /app/account', + // The table's own guard: `isLoggedIn ? : `. + 'routes -> /login', + ]); + const screens = await buildScreens(cg, root); + expect(screens.routed).toBe(true); + const at = (p: string) => screens.screens.find((s) => s.path === p)!.id; + for (const from of ['/', '/login']) { + expect(screens.links.find((l) => l.from === at(from) && l.to === at('/app/dashboard'))).toBeDefined(); + } + expect(screens.links.find((l) => l.from === at('/login') && l.to === at('/app/account'))).toBeDefined(); + }); +}); + +describe("react-router: route objects in their own files, handed to createBrowserRouter (codedthemes' admin templates)", () => { + let root: string; + let cg: CodeGraph; + beforeAll(async () => { + ({ root, cg } = await indexProject({ + 'package.json': JSON.stringify({ name: 'berry', dependencies: { react: '^18', 'react-router-dom': '^7' } }), + 'src/routes/index.jsx': `import { createBrowserRouter } from 'react-router-dom'; + +// routes +import AuthenticationRoutes from './AuthenticationRoutes'; +import MainRoutes from './MainRoutes'; + +const router = createBrowserRouter([MainRoutes, AuthenticationRoutes], { + basename: import.meta.env.VITE_APP_BASE_NAME +}); + +export default router; +`, + 'src/routes/MainRoutes.jsx': `import { lazy } from 'react'; +import MainLayout from '../layout/MainLayout'; +import Loadable from '../ui-component/Loadable'; + +const DashboardDefault = Loadable(lazy(() => import('../views/DashboardDefault'))); +// An import only the app's own build resolves (\`baseUrl\`). +const SamplePage = Loadable(lazy(() => import('views/sample-page'))); + +const MainRoutes = { + path: '/', + element: , + children: [ + { + path: '/', + element: + }, + { + path: 'dashboard', + children: [ + { + path: 'default', + element: + } + ] + }, + { + path: '/sample-page', + element: + } + ] +}; + +export default MainRoutes; +`, + 'src/routes/AuthenticationRoutes.jsx': `import MinimalLayout from '../layout/MinimalLayout'; +import LoginPage from '../views/LoginPage'; + +const AuthenticationRoutes = { + path: '/', + element: , + children: [ + { + path: '/pages/login', + element: + } + ] +}; + +export default AuthenticationRoutes; +`, + 'src/layout/MainLayout.jsx': 'export default function MainLayout() {\n return
;\n}\n', + 'src/layout/MinimalLayout.jsx': 'export default function MinimalLayout() {\n return
;\n}\n', + 'src/views/DashboardDefault.jsx': 'export default function DashboardDefault() {\n return
;\n}\n', + 'src/views/LoginPage.jsx': 'export default function LoginPage() {\n return
;\n}\n', + 'src/ui-component/Loadable.jsx': 'export default function Loadable(Component) {\n return (props) => ;\n}\n', + // The repository's other app, with pages of the same names. + 'next/package.json': JSON.stringify({ name: 'berry-next', dependencies: { next: '^15', react: '^18' } }), + 'next/src/views/sample-page.jsx': 'export default function SamplePage() {\n return
;\n}\n', + 'next/src/views/default.jsx': 'export default function DashboardDefault() {\n return
;\n}\n', + })); + }); + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + it('reads each route object the array names, its children under its path', () => { + expect(routeBindings(cg)).toEqual([ + // MainRoutes' `/` child claims its layout's address. + '/ -> DashboardDefault', + '/ ~> MainLayout', + // No child of the login layout claims `/`, so it is a page there of its + // own — the rule Angular and Vue routes follow. + '/ -> MinimalLayout', + '/dashboard/default -> DashboardDefault', + '/dashboard/default ~> MainLayout', + '/pages/login -> LoginPage', + '/pages/login ~> MinimalLayout', + '/sample-page -> SamplePage', + '/sample-page ~> MainLayout', + ].sort()); + }); + + it('binds a page the route file loads lazily to the module it loads, and never to a same-named page of another app', () => { + const rendered = (p: string): string[] => cg.getNodesByKind('route').filter((r) => r.name === p).flatMap((r) => cg.getOutgoingEdges(r.id) + .filter((e) => e.kind === 'references' && !(e.metadata as Record | undefined)?.layout) + .map((e) => { const n = cg.getNode(e.target)!; return `${n.kind} ${n.name} ${n.filePath}`; })); + expect(rendered('/dashboard/default')).toEqual(['function DashboardDefault src/views/DashboardDefault.jsx']); + // Its module is out of reach, so the page is the declaration itself. + expect(rendered('/sample-page')).toEqual(['constant SamplePage src/routes/MainRoutes.jsx']); + }); +}); + +describe('react-router: a table mapped inside in the same file', () => { + let root: string; + let cg: CodeGraph; + beforeAll(async () => { + ({ root, cg } = await indexProject({ + 'ClientApp/package.json': JSON.stringify({ name: 'client', dependencies: { react: '^18', 'react-router-dom': '^6' } }), + 'ClientApp/src/routes.js': `import { Routes, Route } from 'react-router-dom'; +import AdminLayout from './layouts/admin'; +import { adminRoutes } from './adminRoutes'; +import DashboardLayout from './layouts/dashboard'; +import DashboardApp from './pages/DashboardApp'; +import LandingPage from './pages/LandingPage'; +import TicketDetail from './pages/TicketDetail'; + +const DashboardRoutes = [ + { + path: 'app', + requireAuth: true, + element: + }, + { + path: 'tickets/:id', + requireAuth: true, + element: + } +]; + +export default function Router() { + return ( + + }/> + }> + {DashboardRoutes.map((route, index) => { + const {element, requireAuth, ...rest} = route; + return ; + })} + + }> + {adminRoutes.map((route) => )} + + + ); +} +`, + 'ClientApp/src/adminRoutes.js': `import Reports from './pages/Reports'; + +export const adminRoutes = [{ path: 'reports', element: }]; +`, + 'ClientApp/src/layouts/dashboard.js': 'export default function DashboardLayout() {\n return
;\n}\n', + 'ClientApp/src/layouts/admin.js': 'export default function AdminLayout() {\n return
;\n}\n', + 'ClientApp/src/pages/DashboardApp.js': 'export default function DashboardApp() {\n return
;\n}\n', + 'ClientApp/src/pages/LandingPage.js': 'export default function LandingPage() {\n return
;\n}\n', + 'ClientApp/src/pages/TicketDetail.js': 'export default function TicketDetail() {\n return
;\n}\n', + 'ClientApp/src/pages/Reports.js': 'export default function Reports() {\n return
;\n}\n', + })); + }); + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + it("composes the table's relative paths onto the it is mapped inside, and renders them inside its element", () => { + expect(routeBindings(cg)).toEqual([ + '/admin -> AdminLayout', + // A table written in another file keeps its routes there, where a layout + // named in this file would be looked up: they get no layout edge. + '/admin/reports -> Reports', + '/dashboard -> DashboardLayout', + '/dashboard/app -> DashboardApp', + '/dashboard/app ~> DashboardLayout', + '/dashboard/tickets/:id -> TicketDetail', + '/dashboard/tickets/:id ~> DashboardLayout', + '/landing -> LandingPage', + ]); + }); +}); + +describe('react-router: a route table as the files around it change', () => { + let root: string; + let cg: CodeGraph; + const app = (body: string) => `import { Route, Routes } from 'react-router-dom'; +import AppRoutes from './AppRoutes'; +import { Counter } from './Counter'; + +export default function App() { + return ( + + ${body} + + ); +} +`; + const MAPPED = app('{AppRoutes.map(({ element, ...rest }, index) => )}'); + const table = (extra: string) => `import { Counter } from './Counter'; +import { Home } from './Home'; +import { FetchData } from './FetchData'; + +function unused() { const AppRoutes = [{ path: '/decoy', element: }]; return AppRoutes; } + +const AppRoutes = [${extra} + { index: true, element: }, + { path: '/counter', element: }, +]; + +export default AppRoutes; +`; + beforeAll(async () => { + ({ root, cg } = await indexProject({ + 'package.json': JSON.stringify({ name: 'app', dependencies: { react: '^18', 'react-router-dom': '^6' } }), + 'src/AppRoutes.js': table(''), + 'src/App.js': MAPPED, + 'src/Home.js': component('Home'), + 'src/Counter.js': component('Counter'), + 'src/FetchData.js': component('FetchData'), + 'src/NavMenu.js': `import { Link, useNavigate } from 'react-router-dom'; +export function NavMenu() { + const navigate = useNavigate(); + const goHome = () => navigate('/'); + return ; +} +`, + })); + }); + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + it('starts with the table read', () => { + expect(routeNames(cg)).toEqual(['/', '/counter']); + expect(navigations(cg)).toEqual(['NavMenu -> /counter', 'goHome -> /']); + }); + + it('an edit to the table moves its routes, and what navigated to them still does', async () => { + writeFiles(root, { 'src/AppRoutes.js': table("\n { path: '/fetch-data', element: },") }); + await cg.sync(); + expect(routeBindings(cg)).toEqual(['/ -> Home', '/counter -> Counter', '/fetch-data -> FetchData']); + expect(navigations(cg)).toEqual(['NavMenu -> /counter', 'goHome -> /']); + }); + + it('a file that stops handing the table over takes its routes with it, and putting it back brings them back', async () => { + writeFiles(root, { 'src/App.js': app('} />') }); + await cg.sync(); + expect(routeNames(cg)).toEqual(['/static']); + writeFiles(root, { 'src/App.js': MAPPED }); + await cg.sync(); + expect(routeNames(cg)).toEqual(['/', '/counter', '/fetch-data']); + // `navigate('/')` failed while `/` was gone; the routes coming back retry it. + expect(navigations(cg)).toEqual(['NavMenu -> /counter', 'goHome -> /']); + }); + + it('deleting that file is a change too', async () => { + fs.rmSync(path.join(root, 'src/App.js')); + await cg.sync(); + expect(routeNames(cg)).toEqual([]); + }); +}); + +// ============================================================================= +// JSX index routes, and the around other routes +// ============================================================================= + +/** React Router 6's own examples: a layout route at `/`, an index route inside it. */ +const JSX_LAYOUTS: Record = { + 'package.json': JSON.stringify({ name: 'app', dependencies: { react: '^18', 'react-router-dom': '^6' } }), + 'src/App.jsx': `import { Routes, Route } from 'react-router-dom'; +import Layout from './Layout'; +import DashboardLayout from './DashboardLayout'; +import AdminLayout from './AdminLayout'; +import RequireAuth from './RequireAuth'; +import { Home, About, Login, DashboardHome, Stats, Account, Settings, NoMatch, Users } from './pages'; + +export default function App() { + return ( + + }> + } /> + } /> + } /> + }> + } /> + } /> + + }> + } /> + + + } /> + + } /> + + }> + } /> + + + ); +} +`, + 'src/Layout.jsx': `import { Link, Outlet } from 'react-router-dom'; +export default function Layout() { + return ( +
+ + +
+ ); +} +`, + 'src/DashboardLayout.jsx': `import { Link, Outlet } from 'react-router-dom'; +export default function DashboardLayout() { + return
Stats
; +} +`, + 'src/AdminLayout.jsx': `import { Link, Outlet } from 'react-router-dom'; +export default function AdminLayout() { + return
Users
; +} +`, + 'src/RequireAuth.jsx': `import { Navigate, Outlet } from 'react-router-dom'; +export default function RequireAuth({ user }) { + return user ? : ; +} +`, + 'src/pages.jsx': `import { useNavigate } from 'react-router-dom'; +export function Home() { return

Home

; } +export function About() { return

About

; } +export function Login() { return

Login

; } +export function DashboardHome() { return

Dashboard

; } +export function Stats() { return

Stats

; } +export function Account() { + const navigate = useNavigate(); + const signOut = () => navigate('/'); + return ; +} +export function Settings() { return

Settings

; } +export function NoMatch() { return

Nothing here

; } +export function Users() { return

Users

; } +`, +}; + +describe('react-router: a JSX index route is the page at its parent’s address, and the around it the layout', () => { + let root: string; + let cg: CodeGraph; + beforeAll(async () => { + ({ root, cg } = await indexProject(JSX_LAYOUTS)); + }); + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + it('binds each address to its page, inside every layout around it', () => { + expect(routeBindings(cg)).toEqual([ + '/ -> Home', + '/ ~> Layout', + '/* -> NoMatch', + '/* ~> Layout', + '/about -> About', + '/about ~> Layout', + // A `` with no path is a layout at no address of its own. + '/account -> Account', + '/account ~> Layout', + '/account ~> RequireAuth', + // No child claims `/admin`, so AdminLayout is the page there as well. + '/admin -> AdminLayout', + '/admin/users -> Users', + '/admin/users ~> AdminLayout', + '/dashboard -> DashboardHome', + '/dashboard ~> DashboardLayout', + '/dashboard ~> Layout', + '/dashboard/stats -> Stats', + '/dashboard/stats ~> DashboardLayout', + '/dashboard/stats ~> Layout', + '/login -> Login', + '/login ~> Layout', + // `` renders nothing: it only groups its index route. + '/settings -> Settings', + '/settings ~> Layout', + ].sort()); + }); + + it('puts one route at each address', () => { + expect(routeNames(cg)).toEqual(['/', '/*', '/about', '/account', '/admin', '/admin/users', '/dashboard', '/dashboard/stats', '/login', '/settings']); + const home = cg.getNodesByKind('route').find((r) => r.name === '/')!; + expect(home.startLine).toBe(12); + }); + + it('lands a navigation to a layout’s address on the page there', () => { + expect(navigations(cg)).toEqual([ + 'AdminLayout -> /admin/users', + 'DashboardLayout -> /dashboard/stats', + 'Layout -> /', + 'Layout -> /about', + 'Layout -> /dashboard', + 'RequireAuth -> /login', + 'signOut -> /', + ]); + }); + + it('draws a layout’s links on every screen inside it, and only there', async () => { + const screens = await buildScreens(cg, root); + expect(screens.routed).toBe(true); + const at = (p: string) => screens.screens.find((s) => s.path === p)!.id; + const linked = (from: string, to: string) => screens.links.some((l) => l.from === at(from) && l.to === at(to)); + // Layout's nav bar is on every page it wraps, the deepest included. + for (const from of ['/', '/about', '/login', '/dashboard', '/dashboard/stats', '/account', '/settings']) { + expect(linked(from, '/about')).toBe(true); + } + expect(linked('/dashboard/stats', '/dashboard')).toBe(true); + // The guard's redirect happens on the page it guards. + expect(linked('/account', '/login')).toBe(true); + expect(linked('/about', '/login')).toBe(false); + // The admin pages are not inside Layout. + expect(linked('/admin/users', '/about')).toBe(false); + expect(linked('/admin', '/admin/users')).toBe(true); + }); +}); + +describe('react-router: createRoutesFromElements with an index route and guards at path="" (proshop-v2)', () => { + let root: string; + let cg: CodeGraph; + beforeAll(async () => { + ({ root, cg } = await indexProject({ + 'package.json': JSON.stringify({ name: 'proshop', private: true }), + 'frontend/package.json': JSON.stringify({ name: 'frontend', dependencies: { react: '^18', 'react-router-dom': '^6' } }), + 'frontend/src/index.js': `import { createBrowserRouter, createRoutesFromElements, Route, RouterProvider } from 'react-router-dom'; +import App from './App'; +import PrivateRoute from './components/PrivateRoute'; +import AdminRoute from './components/AdminRoute'; +import HomeScreen from './screens/HomeScreen'; +import CartScreen from './screens/CartScreen'; +import LoginScreen from './screens/LoginScreen'; +import ShippingScreen from './screens/ShippingScreen'; +import OrderListScreen from './screens/admin/OrderListScreen'; + +const router = createBrowserRouter( + createRoutesFromElements( + }> + } /> + } /> + } /> + {/* Registered users */} + }> + } /> + + {/* Admin users */} + }> + } /> + + + ) +); + +export default function Root() { + return ; +} +`, + 'frontend/src/App.js': `import { Outlet } from 'react-router-dom'; +import Header from './components/Header'; +export default function App() { + return ( + <> +
+
+ + ); +} +`, + 'frontend/src/components/Header.js': `import { Link } from 'react-router-dom'; +export default function Header() { + return
Cart
; +} +`, + 'frontend/src/components/PrivateRoute.js': `import { Navigate, Outlet } from 'react-router-dom'; +export default function PrivateRoute({ userInfo }) { + return userInfo ? : ; +} +`, + 'frontend/src/components/AdminRoute.js': `import { Navigate, Outlet } from 'react-router-dom'; +export default function AdminRoute({ userInfo }) { + return userInfo && userInfo.isAdmin ? : ; +} +`, + 'frontend/src/screens/HomeScreen.js': screen('HomeScreen'), + 'frontend/src/screens/CartScreen.js': screen('CartScreen'), + 'frontend/src/screens/LoginScreen.js': screen('LoginScreen'), + 'frontend/src/screens/ShippingScreen.js': screen('ShippingScreen'), + 'frontend/src/screens/admin/OrderListScreen.js': screen('OrderListScreen'), + })); + }); + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + it('makes HomeScreen the page at `/` and App the layout around every screen, the guards around theirs', () => { + expect(routeBindings(cg)).toEqual([ + '/ -> HomeScreen', + '/ ~> App', + '/admin/orderlist -> OrderListScreen', + '/admin/orderlist ~> AdminRoute', + '/admin/orderlist ~> App', + '/cart -> CartScreen', + '/cart ~> App', + '/login -> LoginScreen', + '/login ~> App', + '/shipping -> ShippingScreen', + '/shipping ~> App', + '/shipping ~> PrivateRoute', + ]); + // Neither App nor a guard is a second page at `/`. + expect(routeNames(cg)).toEqual(['/', '/admin/orderlist', '/cart', '/login', '/shipping']); + }); + + it('draws the header’s link from every screen, and a guard’s redirect from the screens it guards', async () => { + const screens = await buildScreens(cg, root); + const at = (p: string) => screens.screens.find((s) => s.path === p)!.id; + const linked = (from: string, to: string) => screens.links.some((l) => l.from === at(from) && l.to === at(to)); + for (const from of ['/', '/login', '/shipping', '/admin/orderlist']) expect(linked(from, '/cart')).toBe(true); + expect(linked('/shipping', '/login')).toBe(true); + expect(linked('/admin/orderlist', '/login')).toBe(true); + expect(linked('/cart', '/login')).toBe(false); + }); +}); + +describe('react-router: JSX index routes as the route file changes', () => { + let root: string; + let cg: CodeGraph; + const app = (index: boolean) => `import { Routes, Route } from 'react-router-dom'; +import Layout from './Layout'; +import { Home, About } from './pages'; + +export default function App() { + return ( + + }>${index ? '\n } />' : ''} + } /> + + + ); +} +`; + const files = (index: boolean): Record => ({ + 'package.json': JSON.stringify({ name: 'app', dependencies: { react: '^18', 'react-router-dom': '^6' } }), + 'src/App.jsx': app(index), + 'src/Layout.jsx': `import { Link, Outlet } from 'react-router-dom'; +export default function Layout() { + return
About
; +} +`, + 'src/pages.jsx': `import { useNavigate } from 'react-router-dom'; +export function Home() { return

Home

; } +export function About() { + const navigate = useNavigate(); + const goHome = () => navigate('/'); + return ; +} +`, + }); + /** What a fresh index of the same files says. */ + const fresh = async (index: boolean): Promise<{ bindings: string[]; navigations: string[] }> => { + const project = await indexProject(files(index)); + try { + return { bindings: routeBindings(project.cg), navigations: navigations(project.cg) }; + } finally { + project.cg.close(); + fs.rmSync(project.root, { recursive: true, force: true }); + } + }; + beforeAll(async () => { + ({ root, cg } = await indexProject(files(false))); + }); + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + it('starts with the layout as the page at its own address', () => { + expect(routeBindings(cg)).toEqual(['/ -> Layout', '/about -> About', '/about ~> Layout']); + expect(navigations(cg)).toEqual(['Layout -> /about', 'goHome -> /']); + }); + + // Each also indexes the same files afresh, to compare with. + it('an index route added takes the address, and what navigated there follows it', async () => { + writeFiles(root, { 'src/App.jsx': app(true) }); + await cg.sync(); + expect(routeBindings(cg)).toEqual(['/ -> Home', '/ ~> Layout', '/about -> About', '/about ~> Layout']); + expect({ bindings: routeBindings(cg), navigations: navigations(cg) }).toEqual(await fresh(true)); + }, 60_000); + + it('taking it away gives the address back to the layout', async () => { + writeFiles(root, { 'src/App.jsx': app(false) }); + await cg.sync(); + expect({ bindings: routeBindings(cg), navigations: navigations(cg) }).toEqual(await fresh(false)); + }, 60_000); +}); + +describe('react-router: route tables re-exported through a barrel', () => { + it.each([ + ['named alias', "export { table as routes } from './table';", 'export const table =', ['/home -> Home']], + ['wildcard', "export * from './table';", 'export const routes =', ['/home -> Home']], + ['default alias', "export { default as routes } from './table';", 'export default', ['/home -> Home']], + ['namespace control', "export * as routes from './table';", 'export const routes =', []], + ] as const)('%s uses the exported binding', async (_label, barrel, declaration, expected) => { + const { root, cg } = await indexProject({ + 'package.json': JSON.stringify({ name: 'web', dependencies: { react: '^18', 'react-router-dom': '^6' } }), + 'src/Home.tsx': component('Home'), + 'src/table.tsx': `import { Home } from './Home';\n${declaration} [{ path: '/home', element: }];\n`, + 'src/barrel.ts': barrel, + 'src/App.tsx': "import { Routes, Route } from 'react-router-dom';\nimport { routes } from './barrel';\nexport function App() { return {routes.map(route => )}; }\n", + }); + try { + expect(routeBindings(cg)).toEqual(expected); + } finally { + cg.close(); + fs.rmSync(root, { recursive: true, force: true }); + } + }); +}); + + +describe('react-router: route-table reference persistence', () => { + it('rolls back a failed reference write so the next pass restores the complete route', async () => { + const { root, cg } = await indexProject({ + 'package.json': JSON.stringify({ name: 'web', dependencies: { react: '^18', 'react-router-dom': '^6' } }), + 'src/Home.tsx': component('Home'), + 'src/table.tsx': "import { Home } from './Home';\nexport const routes = [{ path: '/home', element: }];\n", + 'src/App.tsx': "import { Routes, Route } from 'react-router-dom';\nimport { routes } from './table';\nexport function App() { return {routes.map(route => )}; }\n", + }); + try { + expect(routeBindings(cg)).toEqual(['/home -> Home']); + const route = cg.getNodesByKind('route').find(n => n.name === '/home')!; + cg.queries.deleteNode(route.id); + const resolver = new ReferenceResolver(root, cg.queries); + resolver.initialize(); + const insert = vi.spyOn(cg.queries, 'insertUnresolvedRefsBatch').mockImplementationOnce(() => { + throw new Error('injected reference write failure'); + }); + try { + resolver.runPostExtract(); + } finally { + insert.mockRestore(); + } + expect(cg.getNode(route.id)).toBeNull(); + resolver.runPostExtract(); + expect(cg.getNode(route.id)?.name).toBe('/home'); + expect(cg.queries.getUnresolvedReferencesFrom(route.id)).toEqual(expect.arrayContaining([ + expect.objectContaining({ referenceName: 'Home', referenceKind: 'references' }), + ])); + } finally { + cg.close(); + fs.rmSync(root, { recursive: true, force: true }); + } + }); +}); diff --git a/__tests__/sync-import-retry.test.ts b/__tests__/sync-import-retry.test.ts index 52dcf0828b..04b093ca17 100644 --- a/__tests__/sync-import-retry.test.ts +++ b/__tests__/sync-import-retry.test.ts @@ -266,6 +266,12 @@ describe('sync links an import of a file that appears after the importer', () => expect(synced.ownStatements).toBe(0); expect(cg.getPendingReferenceCount()).toBe(0); + // A full re-index links the same. It has to start from an empty database, + // as `codegraph index` does: indexAll over this one skips the unchanged + // importer and keeps the edges the sync wrote. Close first: recreate + // unlinks the database file, which a held handle makes EBUSY on Windows. + cg.close(); + cg = await CodeGraph.recreate(root); await cg.indexAll(); expect(importsOf(c.from)).toEqual(synced); }, 60_000); diff --git a/__tests__/sync-navigation-retry.test.ts b/__tests__/sync-navigation-retry.test.ts new file mode 100644 index 0000000000..359089140d --- /dev/null +++ b/__tests__/sync-navigation-retry.test.ts @@ -0,0 +1,708 @@ +/** + * A sync binds a navigation call to a route that appears after the call was + * indexed, for every router, as a full index binds it. + * + * `history.push('/login')`, `navigate('/login')`, `router.push('/x')`, + * `goto('/x')`: a navigation call names its route by path and its reference + * by the router's method. While the route did not exist, the call was parked + * as failed, and sync's retry — a failed ref's name tail against the names + * the synced files define — never found it, because no file that adds + * `/login` defines anything named `push`. The synced index kept a failure a + * fresh index of the same files does not have (CG-33). + */ +import { describe, it, expect, afterEach, vi } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import CodeGraph from '../src/index'; +import { QueryBuilder } from '../src/db/queries'; +import { referenceNameTail } from '../src/db/reference-tail'; +import { getAllFrameworkResolvers } from '../src/resolution/frameworks'; +import { changedRoutes, type ReferenceResolver } from '../src/resolution'; +import type { Node } from '../src/types'; + +interface Case { + name: string; + /** The project as it ends up. */ + files: Record; + /** The file the route comes from, and what it holds while the route does not exist (null: no such file yet). */ + route: { file: string; before: string | null }; + /** The file whose navigation call waits for the route. */ + from: string; + /** Where that call goes once the route exists: ` → `. */ + expected: string; +} + +const angularComponent = (name: string, body = '', template = '

x

') => `import { Component } from '@angular/core'; +import { Router } from '@angular/router'; +@Component({ selector: 'app-x', template: '${template}' }) +export class ${name} { + constructor(private readonly router: Router) {} +${body} +} +`; + +const CASES: Case[] = [ + { + name: 'React Router (a in the markup)', + files: { + 'package.json': JSON.stringify({ name: 'shop', dependencies: { react: '18', 'react-router-dom': '5' } }), + 'src/App.js': + "import { BrowserRouter as Router, Route } from 'react-router-dom'\n" + + "import LoginScreen from './screens/LoginScreen'\n" + + "import CartScreen from './screens/CartScreen'\n" + + 'const App = () => (\n' + + ' \n' + + " \n" + + " \n" + + ' \n' + + ')\n' + + 'export default App\n', + 'src/screens/LoginScreen.js': 'const LoginScreen = () =>
Login
\nexport default LoginScreen\n', + 'src/screens/CartScreen.js': + 'const CartScreen = ({ history }) => {\n' + + ' const checkout = () => {\n' + + " history.push('/login?redirect=shipping')\n" + + ' }\n' + + ' return \n' + + '}\n' + + 'export default CartScreen\n', + }, + route: { + file: 'src/App.js', + before: + "import { BrowserRouter as Router, Route } from 'react-router-dom'\n" + + "import LoginScreen from './screens/LoginScreen'\n" + + "import CartScreen from './screens/CartScreen'\n" + + 'const App = () => (\n' + + ' \n' + + " \n" + + ' \n' + + ')\n' + + 'export default App\n', + }, + from: 'src/screens/CartScreen.js', + expected: '/login?redirect=shipping → /login', + }, + { + name: 'React Router (a path built from a constant another file holds)', + files: { + 'package.json': JSON.stringify({ name: 'web', dependencies: { react: '18', 'react-router': '7' } }), + 'src/config/paths.ts': "export const paths = {\n auth: { login: { path: '/auth/login' } },\n} as const;\n", + 'src/app/router.tsx': + "import { createBrowserRouter } from 'react-router';\n" + + "import { paths } from '../config/paths';\n" + + "import { LoginRoute } from './routes/login';\n" + + 'export const router = createBrowserRouter([\n' + + ' { path: paths.auth.login.path, element: },\n' + + ']);\n', + 'src/app/routes/login.tsx': 'export function LoginRoute() { return null; }\n', + 'src/lib/auth.tsx': + "import { useNavigate } from 'react-router';\n" + + 'export function Logout() {\n' + + ' const navigate = useNavigate();\n' + + " return ;\n" + + '}\n', + }, + // The route's own file never changes: the constant it reads does. + route: { file: 'src/config/paths.ts', before: "export const paths = {\n auth: { login: { path: '/auth/sign-in' } },\n} as const;\n" }, + from: 'src/lib/auth.tsx', + expected: '/auth/login → /auth/login', + }, + { + // The routes live in a file that never changes: they exist only while + // another file hands that table to the router. + name: 'React Router (a table another file hands the router)', + files: { + 'package.json': JSON.stringify({ name: 'app', dependencies: { react: '18', 'react-router-dom': '6' } }), + 'src/AppRoutes.js': + "import { Home } from './Home';\n" + + "import { Counter } from './Counter';\n" + + 'const AppRoutes = [\n' + + ' { index: true, element: },\n' + + " { path: '/counter', element: },\n" + + '];\n' + + 'export default AppRoutes;\n', + 'src/App.js': + "import { Route, Routes } from 'react-router-dom';\n" + + "import AppRoutes from './AppRoutes';\n" + + 'export default function App() {\n' + + ' return {AppRoutes.map(({ element, ...rest }, index) => )};\n' + + '}\n', + 'src/Home.js': 'export function Home() { return null; }\n', + 'src/Counter.js': 'export function Counter() { return null; }\n', + 'src/NavMenu.js': + "import { Link, useNavigate } from 'react-router-dom';\n" + + 'export function NavMenu() {\n' + + ' const navigate = useNavigate();\n' + + " const goHome = () => navigate('/');\n" + + ' return ;\n' + + '}\n', + }, + route: { + file: 'src/App.js', + before: + "import { Route, Routes } from 'react-router-dom';\n" + + "import { Counter } from './Counter';\n" + + 'export default function App() {\n' + + ' return } />;\n' + + '}\n', + }, + from: 'src/NavMenu.js', + expected: '/ → /', + }, + { + name: 'React Router (one arm of a conditional destination)', + files: { + 'package.json': JSON.stringify({ name: 'shop', dependencies: { react: '18', 'react-router-dom': '5' } }), + 'src/App.js': + "import { BrowserRouter as Router, Route } from 'react-router-dom'\n" + + "import LoginScreen from './screens/LoginScreen'\n" + + "import CartScreen from './screens/CartScreen'\n" + + 'const App = () => (\n' + + ' \n' + + " \n" + + " \n" + + ' \n' + + ')\n' + + 'export default App\n', + 'src/screens/LoginScreen.js': 'const LoginScreen = () =>
Login
\nexport default LoginScreen\n', + // While `/login` is missing the call still resolves, through its other + // arm: it is not parked as failed, it lost a destination. + 'src/screens/CartScreen.js': + 'const CartScreen = ({ history, user }) => {\n' + + ' const checkout = () => {\n' + + " history.push(user ? '/cart' : '/login')\n" + + ' }\n' + + ' return \n' + + '}\n' + + 'export default CartScreen\n', + }, + route: { + file: 'src/App.js', + before: + "import { BrowserRouter as Router, Route } from 'react-router-dom'\n" + + "import CartScreen from './screens/CartScreen'\n" + + 'const App = () => (\n' + + ' \n' + + " \n" + + ' \n' + + ')\n' + + 'export default App\n', + }, + from: 'src/screens/CartScreen.js', + expected: '/login → /login', + }, + { + name: 'React Router (a parameter route that took the call meanwhile)', + files: { + 'package.json': JSON.stringify({ name: 'admin', dependencies: { react: '18', 'react-router-dom': '6' } }), + 'src/App.jsx': + "import { Routes, Route } from 'react-router-dom'\n" + + "import { User, NewUser } from './User'\n" + + 'export const App = () => (\n' + + ' \n' + + ' } />\n' + + ' } />\n' + + ' \n' + + ')\n', + 'src/User.jsx': 'export const User = () =>
User
\nexport const NewUser = () =>
New
\n', + 'src/Admin.jsx': + "import { useNavigate } from 'react-router-dom'\n" + + 'export const Admin = () => {\n' + + ' const navigate = useNavigate()\n' + + " const add = () => navigate('/users/new')\n" + + ' return \n' + + '}\n', + }, + route: { + file: 'src/App.jsx', + before: + "import { Routes, Route } from 'react-router-dom'\n" + + "import { User } from './User'\n" + + 'export const App = () => (\n' + + ' \n' + + ' } />\n' + + ' \n' + + ')\n', + }, + from: 'src/Admin.jsx', + expected: '/users/new → /users/new', + }, + { + name: 'Next.js', + files: { + 'package.json': JSON.stringify({ name: 'site', dependencies: { next: '15', react: '19' } }), + 'app/page.tsx': 'export default function Home() {\n return null\n}\n', + 'app/login/page.tsx': 'export default function LoginPage() {\n return null\n}\n', + 'components/logout-button.tsx': + "'use client'\n" + + "import { useRouter } from 'next/navigation'\n" + + 'export function LogoutButton() {\n' + + ' const router = useRouter()\n' + + " const out = () => router.push('/login')\n" + + ' return \n' + + '}\n', + }, + route: { file: 'app/login/page.tsx', before: null }, + from: 'components/logout-button.tsx', + expected: '/login → /login', + }, + { + name: 'Expo Router', + files: { + 'package.json': JSON.stringify({ name: 'app', dependencies: { expo: '52', 'expo-router': '4', react: '18' } }), + 'src/app/_layout.tsx': 'export default function Layout() { return null }\n', + 'src/app/index.tsx': 'export default function Home() { return null }\n', + 'src/app/settings.tsx': 'export default function Settings() { return null }\n', + 'src/services/nav.ts': "import { router } from 'expo-router'\nexport function openSettings() {\n router.push('/settings')\n}\n", + }, + route: { file: 'src/app/settings.tsx', before: null }, + from: 'src/services/nav.ts', + expected: '/settings → /settings', + }, + { + name: 'Expo Router (a catch-all screen that took the call meanwhile)', + files: { + 'package.json': JSON.stringify({ name: 'app', dependencies: { expo: '52', 'expo-router': '4', react: '18' } }), + 'src/app/_layout.tsx': 'export default function Layout() { return null }\n', + 'src/app/index.tsx': 'export default function Home() { return null }\n', + 'src/app/[...missing].tsx': 'export default function NotFound() { return null }\n', + 'src/app/login.tsx': 'export default function Login() { return null }\n', + 'src/services/session.ts': "import { router } from 'expo-router'\nexport function signOut() {\n router.replace('/login')\n}\n", + }, + route: { file: 'src/app/login.tsx', before: null }, + from: 'src/services/session.ts', + expected: '/login → /login', + }, + { + name: 'Vue Router', + files: { + 'package.json': JSON.stringify({ name: 'conduit', dependencies: { vue: '3', 'vue-router': '4' } }), + 'src/router/index.js': + 'import { createRouter, createWebHistory } from "vue-router"\n' + + 'const router = createRouter({\n' + + ' history: createWebHistory(),\n' + + ' routes: [\n' + + ' { name: "home", path: "/", component: () => import("@/views/Home") },\n' + + ' { name: "login", path: "/login", component: () => import("@/views/Login") },\n' + + ' ]\n' + + '})\n' + + 'export default router\n', + 'src/views/Home.vue': + '\n' + + '\n', + 'src/views/Login.vue': '\n\n', + }, + route: { + file: 'src/router/index.js', + before: + 'import { createRouter, createWebHistory } from "vue-router"\n' + + 'const router = createRouter({\n' + + ' history: createWebHistory(),\n' + + ' routes: [\n' + + ' { name: "home", path: "/", component: () => import("@/views/Home") },\n' + + ' ]\n' + + '})\n' + + 'export default router\n', + }, + from: 'src/views/Home.vue', + expected: '/login → /login', + }, + { + name: 'Angular', + files: { + 'package.json': JSON.stringify({ dependencies: { '@angular/core': '19', '@angular/router': '19' } }), + 'src/app/app.routes.ts': + "import { Routes } from '@angular/router';\n" + + "import { HomeComponent } from './home.component';\n" + + "import { LoginComponent } from './login.component';\n" + + 'export const routes: Routes = [\n' + + " { path: 'home', component: HomeComponent },\n" + + " { path: 'login', component: LoginComponent },\n" + + '];\n', + 'src/app/home.component.ts': angularComponent('HomeComponent', " signIn() { this.router.navigate(['/login']); }"), + 'src/app/login.component.ts': angularComponent('LoginComponent'), + }, + route: { + file: 'src/app/app.routes.ts', + before: + "import { Routes } from '@angular/router';\n" + + "import { HomeComponent } from './home.component';\n" + + 'export const routes: Routes = [\n' + + " { path: 'home', component: HomeComponent },\n" + + '];\n', + }, + from: 'src/app/home.component.ts', + expected: '/login → /login', + }, + { + // A link in a template is a synthesizer's edge, and a routes file holds + // nothing that sends a sync to the synthesizers on its own. + name: 'Angular (a routerLink, the routes file added later)', + files: { + 'package.json': JSON.stringify({ dependencies: { '@angular/core': '19', '@angular/router': '19' } }), + 'src/app/app.routes.ts': + "import { Routes } from '@angular/router';\n" + + "import { HomeComponent } from './home.component';\n" + + "import { LoginComponent } from './login.component';\n" + + 'export const routes: Routes = [\n' + + " { path: 'home', component: HomeComponent },\n" + + " { path: 'login', component: LoginComponent },\n" + + '];\n', + 'src/app/home.component.ts': angularComponent('HomeComponent', '', 'Sign in'), + 'src/app/login.component.ts': angularComponent('LoginComponent'), + }, + route: { file: 'src/app/app.routes.ts', before: null }, + from: 'src/app/home.component.ts', + expected: '/login → /login', + }, + { + name: 'TanStack Router', + files: { + 'package.json': JSON.stringify({ name: 'app', dependencies: { react: '19', '@tanstack/react-router': '1' } }), + 'src/routes/index.tsx': + "import { createFileRoute, useNavigate } from '@tanstack/react-router'\n" + + "export const Route = createFileRoute('/')({ component: IndexComponent })\n" + + 'function IndexComponent() {\n' + + ' const navigate = useNavigate()\n' + + " const signIn = () => navigate({ to: '/login' })\n" + + ' return \n' + + '}\n', + 'src/routes/login.tsx': + "import { createFileRoute } from '@tanstack/react-router'\n" + + "export const Route = createFileRoute('/login')({ component: LoginComponent })\n" + + 'function LoginComponent() {\n return
Login
\n}\n', + }, + route: { file: 'src/routes/login.tsx', before: null }, + from: 'src/routes/index.tsx', + expected: '/login → /login', + }, + { + name: 'SvelteKit', + files: { + 'package.json': JSON.stringify({ name: 'conduit', devDependencies: { '@sveltejs/kit': '2', svelte: '5' } }), + 'src/routes/+page.svelte': '

Home

\n', + 'src/routes/settings/+page.svelte': '

Settings

\n', + 'src/routes/settings/+page.server.js': + "import { redirect } from '@sveltejs/kit'\n" + + 'export function load({ locals }) {\n' + + " if (!locals.user) redirect(302, '/login')\n" + + '}\n', + 'src/routes/login/+page.svelte': '

Sign in

\n', + }, + route: { file: 'src/routes/login/+page.svelte', before: null }, + from: 'src/routes/settings/+page.server.js', + expected: '/login → /login', + }, +]; + +const roots: string[] = []; +const graphs: CodeGraph[] = []; + +afterEach(() => { + for (const graph of graphs.splice(0)) graph.close(); + for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true }); + vi.restoreAllMocks(); +}); + +function tempRoot(): string { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-sync-nav-')); + roots.push(root); + return root; +} + +function write(root: string, files: Record): void { + 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); + } +} + +/** Set a file's content, or remove the file for null. */ +function put(root: string, rel: string, content: string | null): void { + if (content === null) fs.rmSync(path.join(root, rel)); + else write(root, { [rel]: content }); +} + +async function index(root: string): Promise { + const graph = await CodeGraph.init(root, { index: true }); + graphs.push(graph); + return graph; +} + +/** ` → ` for every navigation out of `file`. */ +function navigations(graph: CodeGraph, file: string): string[] { + const ids = graph.getNodesInFile(file).map((n) => n.id); + return graph + .getOutgoingEdgesFrom(ids, ['navigates']) + .map((edge) => `${String(edge.metadata?.href)} → ${graph.getNode(edge.target)?.name}`) + .sort(); +} + +/** The same files, indexed from scratch in a folder of their own. */ +async function freshNavigations(files: Record, file: string): Promise { + const root = tempRoot(); + write(root, files); + return navigations(await index(root), file); +} + +function internals(graph: CodeGraph): { queries: QueryBuilder; resolver: ReferenceResolver } { + return graph as unknown as { queries: QueryBuilder; resolver: ReferenceResolver }; +} + +const sharedTable = CASES.find((c) => c.name === 'React Router (a table another file hands the router)')!; +CASES.push({ + ...sharedTable, + name: 'React Router (navigation and an imported table share an unchanged file)', + from: 'src/AppRoutes.js', + files: { + ...sharedTable.files, + 'src/AppRoutes.js': sharedTable.files['src/AppRoutes.js']! + + "\nimport { useNavigate } from 'react-router-dom';\nexport function LocalNav() { const navigate = useNavigate(); return ; }\n", + }, +}); + +describe('sync binds a navigation call to a route that appears later', () => { + it.each(CASES)('$name', async (c) => { + const root = tempRoot(); + const without = { ...c.files }; + if (c.route.before === null) delete without[c.route.file]; + else without[c.route.file] = c.route.before; + write(root, without); + const graph = await index(root); + const missing = navigations(graph, c.from); + expect(missing).not.toContain(c.expected); + const fresh = await freshNavigations(c.files, c.from); + expect(fresh).toContain(c.expected); + + // The route appears; the file with the call does not change. + put(root, c.route.file, c.files[c.route.file]!); + await graph.sync(); + expect(navigations(graph, c.from)).toEqual(fresh); + expect(graph.getPendingReferenceCount()).toBe(0); + + // Gone again, and back: the call parks while the route is missing and + // binds again when it returns, as a full index of each state has it. + put(root, c.route.file, c.route.before); + await graph.sync(); + expect(navigations(graph, c.from)).toEqual(missing); + put(root, c.route.file, c.files[c.route.file]!); + await graph.sync(); + expect(navigations(graph, c.from)).toEqual(fresh); + expect(graph.getPendingReferenceCount()).toBe(0); + }, 120_000); +}); + +describe('a route that goes away', () => { + it('lets a call two routes tied for bind to the one left', async () => { + const files = { + 'package.json': JSON.stringify({ name: 'admin', dependencies: { react: '18', 'react-router-dom': '6' } }), + 'src/App.jsx': + "import { Routes, Route } from 'react-router-dom'\n" + + "import { User } from './User'\n" + + 'export const App = () => (\n' + + ' \n' + + ' } />\n' + + ' \n' + + ')\n', + 'src/User.jsx': 'export const User = () =>
User
\n', + 'src/Admin.jsx': + "import { useNavigate } from 'react-router-dom'\n" + + 'export const Admin = () => {\n' + + ' const navigate = useNavigate()\n' + + " const open = () => navigate('/users/5')\n" + + ' return \n' + + '}\n', + }; + const root = tempRoot(); + write(root, files); + // A second route of the same shape: `/users/5` matches both, and a tie names nothing. + put(root, 'src/App.jsx', files['src/App.jsx'].replace(' ', ' } />\n ')); + const graph = await index(root); + expect(navigations(graph, 'src/Admin.jsx')).toEqual([]); + + put(root, 'src/App.jsx', files['src/App.jsx']); + await graph.sync(); + expect(navigations(graph, 'src/Admin.jsx')).toEqual(['/users/5 → /users/:id']); + expect(navigations(graph, 'src/Admin.jsx')).toEqual(await freshNavigations(files, 'src/Admin.jsx')); + }, 60_000); +}); + +describe('reopenNavigationsFor', () => { + const app = (name: string) => ({ + [`apps/${name}/package.json`]: JSON.stringify({ name, dependencies: { react: '18', 'react-router-dom': '6' } }), + [`apps/${name}/src/App.jsx`]: + "import { Routes, Route } from 'react-router-dom'\n" + + "import { Home } from './Home'\n" + + 'export const App = () => (\n \n } />\n \n)\n', + // Two calls to routes the app does not have, and one to its `/`. + [`apps/${name}/src/Home.jsx`]: + "import { useNavigate } from 'react-router-dom'\n" + + 'export const Home = () => {\n' + + ' const navigate = useNavigate()\n' + + " const go = () => navigate('/login')\n" + + " const back = () => navigate('/back')\n" + + " const home = () => navigate('/')\n" + + ' return \n' + + '}\n', + }); + + async function monorepo(): Promise { + const root = tempRoot(); + write(root, { 'package.json': JSON.stringify({ name: 'mono', private: true, workspaces: ['apps/*'] }), ...app('web'), ...app('admin') }); + return index(root); + } + + /** ` failed` per failed `navigate` call, ` → ` per navigation. */ + const navigationState = (graph: CodeGraph): string[] => [ + ...internals(graph).queries.getFailedCallsByTail(['navigate']).map((ref) => `${ref.filePath} failed`), + ...internals(graph).queries.getResolvedNavigations().map((e) => `${e.sourceFilePath} → ${graph.getNode(e.target)?.name}`), + ].sort(); + + /** The app's `/` route, moved to `path` — a route of that app the sync added. */ + const routeAt = (route: Node, path: string): Node => ({ ...route, id: route.id.replace(/:\/$/, `:${path}`), name: path }); + + it('re-opens the calls in the app the route belongs to, failed and resolved, and no others', async () => { + const graph = await monorepo(); + const { queries, resolver } = internals(graph); + const admin = ['apps/admin/src/Home.jsx failed', 'apps/admin/src/Home.jsx failed', 'apps/admin/src/Home.jsx → /']; + expect(navigationState(graph)).toEqual([...admin, 'apps/web/src/Home.jsx failed', 'apps/web/src/Home.jsx failed', 'apps/web/src/Home.jsx → /']); + const web = queries.getNodesByKind('route').find((r) => r.filePath === 'apps/web/src/App.jsx')!; + + expect(resolver.reopenNavigationsFor([routeAt(web, '/login')])).toBe(3); + expect(navigationState(graph)).toEqual(admin); + expect(graph.getPendingReferenceCount()).toBe(3); + }, 60_000); + + it('leaves alone the calls in a file the sync just resolved', async () => { + const graph = await monorepo(); + const { queries, resolver } = internals(graph); + const web = queries.getNodesByKind('route').find((r) => r.filePath === 'apps/web/src/App.jsx')!; + expect(resolver.reopenNavigationsFor([routeAt(web, '/login')], ['apps/web/src/Home.jsx'])).toBe(0); + expect(graph.getPendingReferenceCount()).toBe(0); + }, 60_000); + + it('skips a name more calls share than the ceiling', async () => { + const graph = await monorepo(); + const { queries, resolver } = internals(graph); + // Both apps gain a route: six `navigate` calls between them. + const added = queries.getNodesByKind('route').map((r) => routeAt(r, '/login')); + expect(resolver.reopenNavigationsFor(added, [], 5)).toBe(0); + expect(resolver.reopenNavigationsFor(added, [], 6)).toBe(6); + }, 60_000); + + it('passes over a route no router navigates to', async () => { + const graph = await monorepo(); + const { queries, resolver } = internals(graph); + // A nested route's relative path is not a destination of its own. + const added = queries.getNodesByKind('route').map((r) => routeAt(r, 'login')); + const lookup = vi.spyOn(QueryBuilder.prototype, 'getFailedCallsByTail'); + expect(resolver.reopenNavigationsFor(added)).toBe(0); + expect(lookup).not.toHaveBeenCalled(); + }, 60_000); +}); + +describe('Angular shared-library navigation when app count changes', () => { + it.each(['add', 'remove'] as const)('%s a second app matches a fresh index', async (event) => { + const from = 'libs/ui/shared.component.ts'; + const second = 'apps/admin/src/app.routes.ts'; + const files: Record = { + 'package.json': JSON.stringify({ dependencies: { '@angular/core': '19', '@angular/router': '19' } }), + 'apps/web/src/app.routes.ts': "import { Routes } from '@angular/router'; export const routes: Routes = [{ path: 'login', component: LoginComponent }];", + 'apps/web/src/login.component.ts': angularComponent('LoginComponent'), + [from]: angularComponent('SharedComponent', " signIn() { this.router.navigate(['/login']); }"), + }; + const admin = "import { Routes } from '@angular/router'; export const routes: Routes = [{ path: 'admin', component: AdminComponent }];"; + if (event === 'remove') files[second] = admin; + const root = tempRoot(); + write(root, files); + const graph = await index(root); + expect(navigations(graph, from)).toEqual(event === 'add' ? ['/login → /login'] : []); + if (event === 'add') files[second] = admin; + else delete files[second]; + put(root, second, event === 'add' ? admin : null); + await graph.sync({ paths: [second] }); + expect(navigations(graph, from)).toEqual(event === 'add' ? [] : ['/login → /login']); + expect(navigations(graph, from)).toEqual(await freshNavigations(files, from)); + }, 60_000); +}); + +describe('changedRoutes', () => { + const route = (filePath: string, name: string, id = `route:${filePath}:1:${name}`): Node => ({ + id, kind: 'route', name, qualifiedName: `${filePath}::route:${name}`, filePath, language: 'jsx', + startLine: 1, endLine: 1, startColumn: 0, endColumn: 0, updatedAt: 0, + }); + + it('is the routes on one side only, by file and path', () => { + const kept = route('src/App.jsx', '/'); + const moved = route('src/App.jsx', '/cart'); + const gone = route('src/App.jsx', '/login'); + const added = route('src/Admin.jsx', '/admin'); + // A body edit moves a route's line, and with it its id: still the same route. + const after = [{ ...kept, id: 'route:src/App.jsx:2:/', startLine: 2 }, moved, added]; + expect(changedRoutes([kept, moved, gone], after)).toEqual([gone, added]); + }); + + it('counts a route renamed in place as its old path and its new one', () => { + const old = route('src/router.tsx', '/auth/sign-in', 'route:src/router.tsx:5:paths.auth.login.path'); + const renamed = { ...old, name: '/auth/login' }; + expect(changedRoutes([old], [renamed])).toEqual([old, renamed]); + }); +}); + +describe('a sync that changes no route', () => { + it('looks up no failed navigation', async () => { + const c = CASES[0]!; + const root = tempRoot(); + write(root, c.files); + const graph = await index(root); + const lookup = vi.spyOn(QueryBuilder.prototype, 'getFailedCallsByTail'); + // A body edit to the file the routes are declared in, and to an unrelated one. + put(root, c.route.file, c.files[c.route.file]!.replace('export default App', 'export default App // routes')); + put(root, 'src/screens/LoginScreen.js', 'const LoginScreen = () =>
Sign in
\nexport default LoginScreen\n'); + const result = await graph.sync(); + expect(result.filesModified).toBe(2); + expect(lookup).not.toHaveBeenCalled(); + }, 60_000); + + it('takes no route snapshot without a router that navigates', async () => { + const root = tempRoot(); + write(root, { 'package.json': JSON.stringify({ name: 'lib' }), 'src/a.ts': 'export function a() { return 1; }\n' }); + const graph = await index(root); + const reopen = vi.spyOn(internals(graph).resolver, 'reopenNavigationsFor'); + put(root, 'src/a.ts', 'export function a() { return 2; }\n'); + expect((await graph.sync()).filesModified).toBe(1); + expect(reopen).not.toHaveBeenCalled(); + }, 60_000); +}); + +describe('FrameworkResolver.navigation', () => { + // Navigation calls each router's own tests resolve. + const CALLS: Record = { + 'react-router': ['history.push', 'history.replace', 'navigate', 'navigate.push', 'router.navigate', 'redirect'], + nextjs: ['router.push', 'router.replace', 'router.prefetch', 'redirect', 'permanentRedirect', 'NextResponse.redirect'], + 'expo-router': ['router.push', 'router.replace', 'router.navigate', 'router.dismissTo', 'nav.push'], + 'vue-router': ['router.push', 'this.$router.push', '$router.replace', 'navigateTo'], + 'angular-router': ['this.router.navigate', 'router.navigateByUrl', 'this._router.createUrlTree', 'Router.parseUrl'], + 'tanstack-router': ['navigate', 'redirect', 'router.navigate', 'Route.navigate'], + 'sveltekit-router': ['goto', 'redirect'], + }; + + it('is set by every router that binds navigation calls, and finds each by its tail', () => { + const routers = getAllFrameworkResolvers().filter((r) => r.navigation); + expect(routers.map((r) => r.name).sort()).toEqual(Object.keys(CALLS).sort()); + for (const router of routers) { + for (const call of CALLS[router.name]!) { + expect(router.claimsReference?.(call), `${router.name} claims ${call}`).toBe(true); + expect(router.navigation!.tails, `${router.name} ${call}`).toContain(referenceNameTail(call, 'calls')); + } + } + }); +}); diff --git a/__tests__/sync-path-reference-retry.test.ts b/__tests__/sync-path-reference-retry.test.ts new file mode 100644 index 0000000000..6b935052c2 --- /dev/null +++ b/__tests__/sync-path-reference-retry.test.ts @@ -0,0 +1,139 @@ +/** + * A sync links a reference written as a path once the file it names appears. + * + * Sync retries parked failed refs by name tail (#1240): the tail is matched + * against the names of the nodes a sync adds, and a file's node is named for + * the file. A reference written as a path — Liquid's `{% render 'price' %}` is + * `snippets/price.liquid`, a JSON template's section `"type": "404"` is + * `sections/404.liquid` — was parked under its extension, `liquid`, which no + * node is named, so it linked only once the file naming it changed or on a + * full index. #2392 fixed the same gap for imports. + */ +import { describe, it, expect, afterEach } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import CodeGraph from '../src/index'; +import { DatabaseConnection, getDatabasePath } from '../src/db'; +import { CURRENT_SCHEMA_VERSION, getCurrentVersion, runMigrations } from '../src/db/migrations'; +import { QueryBuilder } from '../src/db/queries'; +import { referenceNameTail } from '../src/db/reference-tail'; + +let root: string | undefined; +let cg: CodeGraph | undefined; +let db: DatabaseConnection | undefined; + +afterEach(() => { + cg?.close(); + cg = undefined; + db?.close(); + db = undefined; + if (root) fs.rmSync(root, { recursive: true, force: true }); + root = undefined; +}); + +function write(files: Record): void { + 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); + } +} + +describe('referenceNameTail', () => { + it('parks a reference written as a path under the name of the file it names', () => { + expect(referenceNameTail('snippets/price.liquid', 'references')).toBe('price.liquid'); + expect(referenceNameTail('sections/404.liquid', 'references')).toBe('404.liquid'); + expect(referenceNameTail('snippets/icon.logo.liquid', 'references')).toBe('icon.logo.liquid'); + }); + + it('keeps the symbol tail of a reference that names no file', () => { + // No path, a path with no file name at its end, or an Erlang arity. (A + // route's module, `lazy-import:./routes/about.tsx` or + // `import:./home.component#HomeComponent`, is sync-route-module-retry's.) + expect(referenceNameTail('Foo.Bar', 'references')).toBe('Bar'); + expect(referenceNameTail('snippets/price', 'references')).toBe('snippets/price'); + expect(referenceNameTail('lists::map/2', 'references')).toBe('map'); + // A call's slashes sit in its arguments, a comment or a division. + expect(referenceNameTail('assert.logfile("logs/error.log").has.line', 'calls')).toBe('line'); + expect(referenceNameTail('(rss_kb / 1024.0).to_i', 'calls')).toBe('to_i'); + // An import keeps the stem of its path (#2392). + expect(referenceNameTail('package:app/b.dart', 'imports')).toBe('b'); + }); +}); + +describe('sync links a path reference whose file appears later', () => { + it('in an index whose references an older version parked', async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-sync-path-ref-')); + write({ + 'layout/theme.liquid': "{% render 'price' %}{{ content_for_layout }}\n", + 'templates/404.json': JSON.stringify({ sections: { main: { type: '404' } }, order: ['main'] }), + }); + cg = await CodeGraph.init(root, { index: true }); + cg.close(); + cg = undefined; + + // What a version before 19 left: the extension as the tail. + db = DatabaseConnection.open(getDatabasePath(root)); + db.getDb().exec(`UPDATE unresolved_refs SET name_tail = 'liquid' WHERE reference_kind = 'references'; + DELETE FROM schema_versions WHERE version >= 19; + INSERT OR IGNORE INTO schema_versions(version, applied_at, description) VALUES (18, 0, 'legacy fixture');`); + db.close(); + db = undefined; + + cg = await CodeGraph.open(root); + write({ 'sections/404.liquid': '

404

\n', 'snippets/price.liquid': '{{ product.price }}\n' }); + expect((await cg.sync()).filesAdded).toBe(2); + expect(cg.getFileDependencies('templates/404.json')).toEqual(['sections/404.liquid']); + expect(cg.getFileDependencies('layout/theme.liquid')).toEqual(['snippets/price.liquid']); + expect(cg.getPendingReferenceCount()).toBe(0); + }, 60_000); +}); + +describe('schema v19', () => { + function fixture(): QueryBuilder { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-sync-path-ref-')); + db = DatabaseConnection.initialize(path.join(root, 'test.db')); + const queries = new QueryBuilder(db.getDb()); + queries.insertNode({ id: 'a', kind: 'file', name: 'theme.liquid', qualifiedName: 'layout/theme.liquid', filePath: 'layout/theme.liquid', + language: 'liquid', startLine: 1, endLine: 1, startColumn: 0, endColumn: 0, updatedAt: 0 }); + for (const [referenceName, referenceKind, status, tail] of [ + ['snippets/price.liquid', 'references', 'failed', 'liquid'], + ['sections/icon.logo.liquid', 'references', 'failed', 'liquid'], + ['snippets/card.liquid', 'references', 'pending', ''], + ['package:app/b.dart', 'imports', 'failed', 'b'], + ['assert.logfile("logs/error.log").has.line', 'calls', 'failed', 'line'], + ['lists::map/2', 'calls', 'failed', 'map'], + ] as const) { + queries.insertUnresolvedRef({ fromNodeId: 'a', referenceName, referenceKind, line: 1, column: 0, filePath: 'layout/theme.liquid', language: 'liquid' }); + db.getDb().prepare('UPDATE unresolved_refs SET status = ?, name_tail = ? WHERE reference_name = ?').run(status, tail, referenceName); + } + return queries; + } + + const tails = () => db!.getDb().prepare('SELECT reference_name AS name, name_tail AS tail FROM unresolved_refs ORDER BY id').all(); + + it('rewrites the tail of a path reference parked by an older version, and replays cleanly', () => { + fixture(); + db!.getDb().exec(`DELETE FROM schema_versions WHERE version >= 19; + INSERT OR IGNORE INTO schema_versions(version, applied_at, description) VALUES (18, 0, 'legacy fixture');`); + db!.close(); + db = DatabaseConnection.open(path.join(root!, 'test.db')); + expect(getCurrentVersion(db.getDb())).toBe(CURRENT_SCHEMA_VERSION); + const migrated = tails(); + expect(migrated).toEqual([ + { name: 'snippets/price.liquid', tail: 'price.liquid' }, + { name: 'sections/icon.logo.liquid', tail: 'icon.logo.liquid' }, + // Only a failed row is looked up by its tail. + { name: 'snippets/card.liquid', tail: '' }, + { name: 'package:app/b.dart', tail: 'b' }, + { name: 'assert.logfile("logs/error.log").has.line', tail: 'line' }, + { name: 'lists::map/2', tail: 'map' }, + ]); + const queries = new QueryBuilder(db.getDb()); + expect(queries.getRetryableFailedReferences(['price.liquid', 'liquid']).map((ref) => ref.referenceName)).toEqual(['snippets/price.liquid']); + + db.getDb().exec('DELETE FROM schema_versions WHERE version >= 19'); + runMigrations(db.getDb(), 18); + expect(tails()).toEqual(migrated); + }); +}); diff --git a/__tests__/sync-route-module-retry.test.ts b/__tests__/sync-route-module-retry.test.ts new file mode 100644 index 0000000000..2f39a43076 --- /dev/null +++ b/__tests__/sync-route-module-retry.test.ts @@ -0,0 +1,454 @@ +/** + * A sync links a route to the module it lazily loads once the module appears. + * + * A router names a lazily loaded page or layout by the module's path: React + * Router's `lazy: () => import('./pages/Team')` is the reference + * `lazy-import:./pages/Team`, Vue Router's `component: () => + * import('@/views/Login')` is `import:@/views/Login#Login`, and Angular's + * `loadComponent: () => import('./home/home.component')` is + * `import:./home/home.component#default`. Sync retries a parked failed ref by + * its tail (#1240), and these were parked under a fragment of the path — + * `/pages/Team`, `@/views/Login#Login`, `component#default` — that no file's + * keys match. So a route whose module was added after the router was indexed + * stayed unlinked until the router file changed or the project was indexed + * again, and so did one whose module gained its component in a later edit. + * #2392 fixed the same gap for imports, #2403 for Liquid's path references. + */ +import { describe, it, expect, afterEach, vi } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import CodeGraph from '../src/index'; +import { DatabaseConnection, getDatabasePath } from '../src/db'; +import { CURRENT_SCHEMA_VERSION, getCurrentVersion, runMigrations } from '../src/db/migrations'; +import { QueryBuilder } from '../src/db/queries'; +import { moduleReferenceKeys, referenceNameTail } from '../src/db/reference-tail'; + +type Files = Record; +/** Each route's links: `[ layout] ::`. */ +type Links = Record; + +interface Scenario { + name: string; + initial: Files; + added: Files; + expected: Links; +} + +const pkg = (dependencies: Record) => JSON.stringify({ name: 'app', dependencies }); +const angularComponent = (name: string, exported = 'export') => + `import { Component } from '@angular/core';\n\n@Component({ selector: 'app-x', template: '' })\n${exported} class ${name} {}\n`; +const vueView = (text: string) => `\n\n`; + +const SCENARIOS: Scenario[] = [ + { + name: 'React Router', + initial: { + 'package.json': pkg({ react: '^18', 'react-router-dom': '^6' }), + 'tsconfig.json': JSON.stringify({ compilerOptions: { baseUrl: '.', paths: { '@/*': ['src/*'] } } }), + 'src/router.tsx': [ + "import { createBrowserRouter } from 'react-router-dom';", + 'export const router = createBrowserRouter([', + " { path: '/team', lazy: () => import('./pages/Team') },", + " { path: '/about', lazy: () => import('./pages/About.tsx') },", + " { path: '/docs', lazy: () => import('./pages/Docs') },", + " { path: '/settings', lazy: () => import('./pages/Settings') },", + " { path: '/billing', lazy: () => import('@/pages/Billing') },", + " { path: '/app', lazy: () => import('./layouts/Shell'), children: [", + " { path: 'inbox', lazy: () => import('./pages/Inbox') },", + ' ] },', + ']);', + '', + ].join('\n'), + }, + added: { + 'src/pages/Team.tsx': 'export default function Team() { return null; }\n', + 'src/pages/About.tsx': 'export default function About() { return null; }\n', + // A default export named unlike its file. + 'src/pages/Docs.tsx': 'export default function DocsPage() { return null; }\n', + // A folder's index file. + 'src/pages/Settings/index.tsx': 'export default function Settings() { return null; }\n', + // Through a tsconfig alias, rendering the module's `Component` export. + 'src/pages/Billing.tsx': 'export function Component() { return null; }\n', + 'src/layouts/Shell.tsx': 'export default function Shell() { return null; }\n', + 'src/pages/Inbox.tsx': 'export default function Inbox() { return null; }\n', + }, + expected: { + '/team': ['references src/pages/Team.tsx::Team'], + '/about': ['references src/pages/About.tsx::About'], + '/docs': ['references src/pages/Docs.tsx::DocsPage'], + '/settings': ['references src/pages/Settings/index.tsx::Settings'], + '/billing': ['references src/pages/Billing.tsx::Component'], + '/app': ['references src/layouts/Shell.tsx::Shell'], + '/app/inbox': ['references layout src/layouts/Shell.tsx::Shell', 'references src/pages/Inbox.tsx::Inbox'], + }, + }, + { + name: 'Vue Router', + initial: { + 'package.json': pkg({ vue: '^3', 'vue-router': '^4' }), + 'src/router/index.js': [ + "import { createRouter, createWebHistory } from 'vue-router'", + 'export default createRouter({', + ' history: createWebHistory(),', + ' routes: [', + " { path: '/', component: () => import('../views/Home.vue') },", + " { path: '/login', component: () => import('@/views/Login') },", + " { path: '/admin', component: () => import('@/layout/index.vue'), children: [", + " { path: 'dashboard', component: () => import('@/views/dashboard/index') },", + ' ] },', + ' ]', + '})', + '', + ].join('\n'), + }, + added: { + 'src/views/Home.vue': vueView('home'), + 'src/views/Login.vue': vueView('login'), + 'src/layout/index.vue': vueView(''), + 'src/views/dashboard/index.vue': vueView('dashboard'), + }, + expected: { + '/': ['calls src/views/Home.vue::Home'], + '/login': ['calls src/views/Login.vue::Login'], + '/admin': ['calls src/layout/index.vue::index'], + '/admin/dashboard': ['calls src/views/dashboard/index.vue::index', 'references layout src/layout/index.vue::index'], + }, + }, + { + name: 'Angular', + initial: { + 'package.json': pkg({ '@angular/core': '^17', '@angular/router': '^17' }), + 'src/app/app.routes.ts': [ + "import { Routes } from '@angular/router';", + 'export const routes: Routes = [', + " { path: '', loadComponent: () => import('./home/home.component').then((m) => m.HomeComponent) },", + " { path: 'about', loadComponent: () => import('./about/about.component') },", + " { path: 'admin', loadComponent: () => import('./admin/shell.component').then((m) => m.ShellComponent), children: [", + " { path: 'users', loadComponent: () => import('./admin/users.component').then((m) => m.UsersComponent) },", + ' ] },', + '];', + '', + ].join('\n'), + }, + added: { + 'src/app/home/home.component.ts': angularComponent('HomeComponent'), + 'src/app/about/about.component.ts': angularComponent('AboutComponent', 'export default'), + 'src/app/admin/shell.component.ts': angularComponent('ShellComponent'), + 'src/app/admin/users.component.ts': angularComponent('UsersComponent'), + }, + expected: { + '/': ['references src/app/home/home.component.ts::HomeComponent'], + '/about': ['references src/app/about/about.component.ts::AboutComponent'], + '/admin': ['references src/app/admin/shell.component.ts::ShellComponent'], + '/admin/users': ['references layout src/app/admin/shell.component.ts::ShellComponent', 'references src/app/admin/users.component.ts::UsersComponent'], + }, + }, + { + // Ghostfolio's markets page: the child loads the class its layout + // renders, so the child's route and layout references make one edge, and + // the reference resolved first names it — in a full index, the one + // written first. + name: 'Angular, a child loading its layout\'s class', + initial: { + 'package.json': pkg({ '@angular/core': '^17', '@angular/router': '^17' }), + 'src/app/app.routes.ts': [ + "import { Routes } from '@angular/router';", + "import { MarketsComponent } from './markets/markets.component';", + 'export const routes: Routes = [', + " { path: 'markets', component: MarketsComponent, children: [", + " { path: '', loadComponent: () => import('./markets/markets.component').then((m) => m.MarketsComponent) },", + ' ] },', + '];', + '', + ].join('\n'), + }, + added: { 'src/app/markets/markets.component.ts': angularComponent('MarketsComponent') }, + expected: { '/markets': ['references src/app/markets/markets.component.ts::MarketsComponent'] }, + }, +]; + +/** + * The module is there from the start, but only a later edit gives it the + * component the route renders: React's default export, a component a Vue + * route loads from a script, Angular's class. + */ +const EDITED: Scenario[] = [ + { + name: 'React Router', + initial: { + 'package.json': pkg({ react: '^18', 'react-router-dom': '^6' }), + 'src/router.tsx': "import { createBrowserRouter } from 'react-router-dom';\nexport const router = createBrowserRouter([{ path: '/team', lazy: () => import('./pages/Team') }]);\n", + 'src/pages/Team.tsx': 'export const placeholder = 1;\n', + }, + added: { 'src/pages/Team.tsx': 'export default function Team() { return null; }\n' }, + expected: { '/team': ['references src/pages/Team.tsx::Team'] }, + }, + { + name: 'Vue Router', + initial: { + 'package.json': pkg({ vue: '^3', 'vue-router': '^4' }), + 'src/router/index.js': "import { createRouter, createWebHistory } from 'vue-router'\nexport default createRouter({ history: createWebHistory(), routes: [{ path: '/', component: () => import('../views/Home.js') }] })\n", + 'src/views/Home.js': 'export const placeholder = 1\n', + }, + added: { 'src/views/Home.js': 'export default function Home() { return null }\n' }, + expected: { '/': ['calls src/views/Home.js::Home'] }, + }, + { + name: 'Angular', + initial: { + 'package.json': pkg({ '@angular/core': '^17', '@angular/router': '^17' }), + 'src/app/app.routes.ts': "import { Routes } from '@angular/router';\nexport const routes: Routes = [{ path: '', loadComponent: () => import('./home/home.component').then((m) => m.HomeComponent) }];\n", + 'src/app/home/home.component.ts': 'export const placeholder = 1;\n', + }, + added: { 'src/app/home/home.component.ts': angularComponent('HomeComponent') }, + expected: { '/': ['references src/app/home/home.component.ts::HomeComponent'] }, + }, +]; + +let root: string | undefined; +let cg: CodeGraph | undefined; +let db: DatabaseConnection | undefined; + +afterEach(() => { + cg?.close(); + cg = undefined; + db?.close(); + db = undefined; + if (root) fs.rmSync(root, { recursive: true, force: true }); + root = undefined; +}); + +function write(dir: string, files: Files): void { + for (const [file, text] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(dir, file)), { recursive: true }); + fs.writeFileSync(path.join(dir, file), text); + } +} + +function routeLinks(graph: CodeGraph): Links { + const links: Links = {}; + for (const route of graph.getNodesByKind('route')) { + links[route.name] = graph + .getOutgoingEdges(route.id) + .filter((edge) => edge.kind !== 'contains') + .map((edge) => { + const target = graph.getNode(edge.target); + return `${edge.kind}${edge.metadata?.layout ? ' layout' : ''} ${target?.filePath}::${target?.name}`; + }) + .sort(); + } + return links; +} + +const unlinked = (expected: Links): Links => Object.fromEntries(Object.keys(expected).map((route) => [route, []])); + +/** The route links of a fresh index of `files`, in a folder of its own: indexing again over an index skips its unchanged files. */ +async function freshRouteLinks(files: Files): Promise { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-route-module-fresh-')); + try { + write(dir, files); + const fresh = await CodeGraph.init(dir, { index: true }); + try { + return routeLinks(fresh); + } finally { + fresh.close(); + } + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +} + +describe('sync links a route to the module it lazily loads', () => { + it.each(SCENARIOS)('when the module is added after the router: $name', async (s) => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-route-module-')); + write(root, s.initial); + cg = await CodeGraph.init(root, { index: true }); + expect(routeLinks(cg)).toEqual(unlinked(s.expected)); + + write(root, s.added); + expect((await cg.sync()).filesAdded).toBe(Object.keys(s.added).length); + // The router did not change: only the retry of its parked references links it. + expect(routeLinks(cg)).toEqual(s.expected); + expect(cg.getPendingReferenceCount()).toBe(0); + expect(await freshRouteLinks({ ...s.initial, ...s.added })).toEqual(s.expected); + }, 60_000); + + // The component a route renders is the module's, so an edit that gives the + // module one is what the route waited for. + it.each(EDITED)('when an edit gives the module its component: $name', async (s) => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-route-module-')); + write(root, s.initial); + cg = await CodeGraph.init(root, { index: true }); + expect(routeLinks(cg)).toEqual(unlinked(s.expected)); + + write(root, s.added); + expect((await cg.sync()).filesModified).toBe(1); + expect(routeLinks(cg)).toEqual(s.expected); + expect(cg.getPendingReferenceCount()).toBe(0); + expect(await freshRouteLinks({ ...s.initial, ...s.added })).toEqual(s.expected); + }, 60_000); + + it('when a deleted module comes back', async () => { + const [react] = SCENARIOS; + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-route-module-')); + write(root, { ...react!.initial, ...react!.added }); + cg = await CodeGraph.init(root, { index: true }); + expect(routeLinks(cg)['/team']).toEqual(['references src/pages/Team.tsx::Team']); + + fs.rmSync(path.join(root, 'src/pages/Team.tsx')); + expect((await cg.sync()).filesRemoved).toBe(1); + expect(routeLinks(cg)['/team']).toEqual([]); + expect(cg.getPendingReferenceCount()).toBe(0); + + write(root, { 'src/pages/Team.tsx': react!.added['src/pages/Team.tsx']! }); + expect((await cg.sync()).filesAdded).toBe(1); + expect(routeLinks(cg)).toEqual(react!.expected); + }, 60_000); + + it('in an index whose references an older version parked', async () => { + const [react] = SCENARIOS; + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-route-module-')); + write(root, react!.initial); + cg = await CodeGraph.init(root, { index: true }); + cg.close(); + cg = undefined; + + // What a version before 20 left: the tail cut at the path's last dot or colon. + db = DatabaseConnection.open(getDatabasePath(root)); + const refs = db.getDb().prepare("SELECT id, reference_name AS name FROM unresolved_refs WHERE reference_name LIKE '%lazy-import:%'").all() as Array<{ id: number; name: string }>; + expect(refs).toHaveLength(8); + for (const ref of refs) { + const legacy = ref.name.slice(Math.max(ref.name.lastIndexOf('.'), ref.name.lastIndexOf(':')) + 1); + db.getDb().prepare('UPDATE unresolved_refs SET name_tail = ? WHERE id = ?').run(legacy, ref.id); + } + db.getDb().exec(`DELETE FROM schema_versions WHERE version >= 20; + INSERT OR IGNORE INTO schema_versions(version, applied_at, description) VALUES (19, 0, 'legacy fixture');`); + db.close(); + db = undefined; + + cg = await CodeGraph.open(root); + write(root, react!.added); + await cg.sync(); + expect(routeLinks(cg)).toEqual(react!.expected); + expect(cg.getPendingReferenceCount()).toBe(0); + }, 60_000); +}); + +describe('referenceNameTail', () => { + it('parks a route\'s reference to a module under the stem of its path, behind module:', () => { + // React Router: a lazy route, and the lazy layout route around another. + expect(referenceNameTail('lazy-import:./pages/Team', 'references')).toBe('module:Team'); + expect(referenceNameTail('lazy-import:./routes/about.tsx', 'references')).toBe('module:about'); + expect(referenceNameTail('lazy-import:@/pages/Billing', 'references')).toBe('module:Billing'); + expect(referenceNameTail('lazy-import:./pages/Settings/', 'references')).toBe('module:Settings'); + expect(referenceNameTail('layout:lazy-import:./layouts/Shell', 'references')).toBe('module:Shell'); + // Vue Router: the component a route renders is a call, its layout a reference; + // vben's `#/` alias puts a `#` in the path. + expect(referenceNameTail('import:@/views/Login#Login', 'calls')).toBe('module:Login'); + expect(referenceNameTail('import:../views/Home.vue#Home', 'calls')).toBe('module:Home'); + expect(referenceNameTail('import:#/views/dashboard/analytics/index.vue#index', 'calls')).toBe('module:index'); + expect(referenceNameTail('layout:import:@/layout/index.vue#index', 'references')).toBe('module:index'); + // Angular: a member the import names, or the module's default. + expect(referenceNameTail('import:./home/home.component#HomeComponent', 'references')).toBe('module:home'); + expect(referenceNameTail('import:./about/about.component#default', 'references')).toBe('module:about'); + expect(referenceNameTail('layout:import:./admin/shell.component#ShellComponent', 'references')).toBe('module:shell'); + }); + + it('keeps the tail of a reference that names no module', () => { + // A layout named by its component, a module path of dots only, an import + // name with no member, and an import itself. + expect(referenceNameTail('layout:MainLayout', 'references')).toBe('MainLayout'); + expect(referenceNameTail('lazy-import:..', 'references')).not.toMatch(/^module:/); + expect(referenceNameTail('import:./x', 'calls')).toBe('/x'); + expect(referenceNameTail('./pages/Team', 'imports')).toBe('Team'); + // The same names as another kind of reference are not a route's. + expect(referenceNameTail('import:./home.component#HomeComponent', 'imports')).toBe('home'); + expect(referenceNameTail('lazy-import:./pages/Team', 'type_of')).toBe('/pages/Team'); + }); +}); + +describe('moduleReferenceKeys', () => { + it('gives a file the keys a module reference it could satisfy waits under', () => { + expect(moduleReferenceKeys('src/pages/Team.tsx')).toEqual(['module:pages', 'module:Team.tsx', 'module:Team']); + expect(moduleReferenceKeys('src/pages/Settings/index.tsx')).toEqual(['module:Settings', 'module:index.tsx', 'module:index']); + expect(moduleReferenceKeys('src/app/home/home.component.ts')).toEqual(['module:home', 'module:home.component.ts']); + }); +}); + +describe('schema v20', () => { + function fixture(): QueryBuilder { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-route-module-')); + db = DatabaseConnection.initialize(path.join(root, 'test.db')); + const queries = new QueryBuilder(db.getDb()); + queries.insertNode({ id: 'r', kind: 'route', name: '/team', qualifiedName: 'src/router.tsx::route:/team', filePath: 'src/router.tsx', + language: 'tsx', startLine: 1, endLine: 1, startColumn: 0, endColumn: 0, updatedAt: 0 }); + for (const [referenceName, referenceKind, status, tail] of [ + ['lazy-import:./pages/Team', 'references', 'failed', '/pages/Team'], + ['layout:lazy-import:./layouts/Shell', 'references', 'failed', '/layouts/Shell'], + ['lazy-import:./pages/About.tsx', 'references', 'failed', 'About.tsx'], + ['import:@/views/Login#Login', 'calls', 'failed', '@/views/Login#Login'], + ['layout:import:@/layout/index.vue#index', 'references', 'failed', 'vue#index'], + ['import:./home/home.component#HomeComponent', 'references', 'failed', 'component#HomeComponent'], + // Only a failed row is looked up by its tail. + ['lazy-import:./pages/Docs', 'references', 'pending', ''], + ['layout:MainLayout', 'references', 'failed', 'MainLayout'], + ['snippets/price.liquid', 'references', 'failed', 'price.liquid'], + ['./pages/Team', 'imports', 'failed', 'Team'], + ['Team', 'calls', 'failed', 'Team'], + ] as const) { + queries.insertUnresolvedRef({ fromNodeId: 'r', referenceName, referenceKind, line: 1, column: 0, filePath: 'src/router.tsx', language: 'tsx' }); + db.getDb().prepare('UPDATE unresolved_refs SET status = ?, name_tail = ? WHERE reference_name = ?').run(status, tail, referenceName); + } + return queries; + } + + const tails = () => db!.getDb().prepare('SELECT reference_name AS name, name_tail AS tail FROM unresolved_refs ORDER BY id').all(); + + it('rewrites the tail of a module reference parked by an older version, and replays cleanly', () => { + fixture(); + db!.getDb().exec(`DELETE FROM schema_versions WHERE version >= 20; + INSERT OR IGNORE INTO schema_versions(version, applied_at, description) VALUES (19, 0, 'legacy fixture');`); + db!.close(); + db = DatabaseConnection.open(path.join(root!, 'test.db')); + expect(getCurrentVersion(db.getDb())).toBe(CURRENT_SCHEMA_VERSION); + const migrated = tails(); + expect(migrated).toEqual([ + { name: 'lazy-import:./pages/Team', tail: 'module:Team' }, + { name: 'layout:lazy-import:./layouts/Shell', tail: 'module:Shell' }, + { name: 'lazy-import:./pages/About.tsx', tail: 'module:About' }, + { name: 'import:@/views/Login#Login', tail: 'module:Login' }, + { name: 'layout:import:@/layout/index.vue#index', tail: 'module:index' }, + { name: 'import:./home/home.component#HomeComponent', tail: 'module:home' }, + { name: 'lazy-import:./pages/Docs', tail: '' }, + { name: 'layout:MainLayout', tail: 'MainLayout' }, + { name: 'snippets/price.liquid', tail: 'price.liquid' }, + { name: './pages/Team', tail: 'Team' }, + { name: 'Team', tail: 'Team' }, + ]); + + // A module reference waits for a file that could be the module, not for + // a symbol: `Team` is the tail of the import and the call, not of the route. + const queries = new QueryBuilder(db.getDb()); + const retried = (names: string[]) => queries.getRetryableFailedReferences(names).map((ref) => ref.referenceName).sort(); + expect(retried(moduleReferenceKeys('src/pages/Team.tsx'))).toEqual(['lazy-import:./pages/Team']); + expect(retried(moduleReferenceKeys('src/app/home/home.component.ts'))).toEqual(['import:./home/home.component#HomeComponent']); + expect(retried(['Team', 'home', 'index'])).toEqual(['./pages/Team', 'Team']); + + db.getDb().exec('DELETE FROM schema_versions WHERE version >= 20'); + runMigrations(db.getDb(), 19); + expect(tails()).toEqual(migrated); + }); + + it('reads only the references its prefixes name, not every failed one', () => { + fixture(); + const prepare = vi.spyOn(db!.getDb(), 'prepare'); + db!.getDb().exec('DELETE FROM schema_versions WHERE version >= 20'); + runMigrations(db!.getDb(), 19); + const select = prepare.mock.calls.map(([sql]) => sql as string).find((sql) => sql.includes("GLOB 'lazy-import:*'")); + prepare.mockRestore(); + const plan = db!.getDb().prepare(`EXPLAIN QUERY PLAN ${select}`).all().map((row) => (row as { detail: string }).detail).join('; '); + expect(plan).toMatch(/USING (COVERING )?INDEX idx_unresolved_name/); + expect(plan).not.toMatch(/idx_unresolved_status|SCAN unresolved_refs/); + }); +}); diff --git a/__tests__/sync.test.ts b/__tests__/sync.test.ts index 7a722d5820..5729756a21 100644 --- a/__tests__/sync.test.ts +++ b/__tests__/sync.test.ts @@ -15,6 +15,39 @@ import CodeGraph, { LockUnavailableError } from '../src/index'; import { QueryBuilder } from '../src/db/queries'; import { __emitWatchEventForTests } from '../src/sync/watcher'; +/** + * Index `cg`'s project again from an empty database, as `codegraph index` + * does, and return the new handle: the graph a synced index has to match. + * indexAll over the synced index is no stand-in — it skips every file whose + * content is unchanged, so the edges the sync wrote survive it untouched. + */ +async function reindexFromScratch(cg: CodeGraph): Promise { + const root = cg.getProjectRoot(); + // recreate unlinks the database file; a held handle makes that EBUSY on Windows. + cg.close(); + const rebuilt = await CodeGraph.recreate(root); + await rebuilt.indexAll(); + return rebuilt; +} + +/** + * For a test that indexes twice in its body: on a busy host the default 5s + * timeout measures the machine rather than the tree (#1773). + */ +const REINDEX_TIMEOUT = 60_000; + +/** Every node and edge by natural key, so two indexes of the same files compare directly. */ +function graphOf(cg: CodeGraph): { nodes: string[]; edges: string[] } { + const keys = new Map( + cg.getFiles() + .flatMap((file) => cg.getNodesInFile(file.path)) + .map((n) => [n.id, `${n.kind} ${n.filePath}:${n.qualifiedName}:${n.startLine}`] as const) + ); + const edges = cg.getOutgoingEdgesFrom([...keys.keys()]) + .map((e) => `${e.kind} ${keys.get(e.source)} -> ${keys.get(e.target) ?? e.target}`); + return { nodes: [...keys.values()].sort(), edges: edges.sort() }; +} + describe('Sync Module', () => { describe('Sync Functionality', () => { let testDir: string; @@ -569,14 +602,11 @@ describe('Sync Module', () => { it('the synced graph matches a full re-index (the issue\'s exact complaint)', async () => { write('b.ts', `export function greet(): number {\n return 42;\n}\n`); await cg.sync(); - const synced = cg.getStats(); + const synced = graphOf(cg); - await cg.indexAll(); - const reindexed = cg.getStats(); - - expect(synced.edgeCount).toBe(reindexed.edgeCount); - expect(synced.nodeCount).toBe(reindexed.nodeCount); - }); + cg = await reindexFromScratch(cg); + expect(synced).toEqual(graphOf(cg)); + }, REINDEX_TIMEOUT); it('a second sync is a no-op and does not duplicate edges', async () => { write('b.ts', `export function greet(): number {\n return 42;\n}\n`); @@ -663,10 +693,10 @@ describe('Sync Module', () => { expect(def?.filePath).toBe('d.ts'); expect(greetCallers()).toContain('run'); // Parity with a full re-index — the issue's contract. - const synced = cg.getStats(); - await cg.indexAll(); - expect(cg.getStats().edgeCount).toBe(synced.edgeCount); - }); + const synced = graphOf(cg); + cg = await reindexFromScratch(cg); + expect(synced).toEqual(graphOf(cg)); + }, REINDEX_TIMEOUT); it('drops the edge on removal and restores it when the symbol returns', async () => { write('b.ts', `export function other(): number {\n return 1;\n}\n`); diff --git a/__tests__/ts-ambient-exports.test.ts b/__tests__/ts-ambient-exports.test.ts new file mode 100644 index 0000000000..df0b304bca --- /dev/null +++ b/__tests__/ts-ambient-exports.test.ts @@ -0,0 +1,183 @@ +/** + * A declaration inside `declare module 'x' { … }` or `declare global { … }` is + * exported, though nothing in it writes `export`: + * + * declare module 'chart.js' { // ghostfolio's chart.registry.ts + * interface PluginOptionsByType { … } + * } + * declare global { // angular-realworld's app.config.ts + * interface Window { __conduit_debug__?: ConduitDebug; } + * } + * + * Both indexed as unexported file-level interfaces, so the dead-code report + * listed them: nothing in the repository names them, because chart.js and the + * browser read them through the type they merge into. TypeScript's binder + * makes an ambient module body an export context, and a namespace nested in + * one is ambient too, unless the body holds an export declaration of its own + * (`export {}`, `export =`, `export default x`). The native extractor now says what + * the compiler says. `declare namespace X` is unchanged: in a module file it + * is that file's own namespace and merges with nothing outside. Runs against + * the native kernel. + */ +import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'; +import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars'; +import { tryKernelExtract, resetKernelForTests } from '../src/extraction/kernel'; +import type { ExtractionResult, Language } from '../src/types'; + + +/** A module file: augmentations, a global block, and the namespaces that are neither. */ +const MODULE_SOURCE = `import type { ChartType } from 'chart.js'; + +declare module 'chart.js' { + interface PluginOptionsByType { + verticalHoverLine: TType; + } + export interface WrittenOut {} + namespace Plugins { + interface Nested {} + } + type Alias = string; + enum Position { Top } + class Positioner {} + const defaults: Alias; +} + +declare global { + interface Window { + __conduit_debug__?: unknown; + } + namespace NodeJS { + interface ProcessEnv { + API_URL?: string; + } + } + var bitwardenContainerService: unknown; +} + +declare namespace Settings { + interface OwnNamespace {} +} + +namespace Plain { + interface Hidden {} + export interface Shown {} +} + +interface FileLocal {} + +export const appConfig = {}; +`; + +/** + * A script file declaring a module whole, the way an untyped library's typings + * do. \`export =\` is an export declaration, so the body exports only that. + */ +const SCRIPT_SOURCE = `declare module 'legacy-lib' { + interface Options {} + const main: (options: Options) => void; + export = main; +} + +declare module 'listed-lib' { + interface Unlisted {} + export interface Listed {} + export { Unlisted as Renamed }; +} + +declare module 'open-lib' { + interface Open {} +} +`; + +const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS'] as const; + +/** Kinds a statement declares — the nodes that carry an export flag of their own. */ +const DECLARATION_KINDS = new Set(['interface', 'class', 'enum', 'type_alias', 'function', 'variable', 'constant', 'component']); + +/** `name` → isExported, for every declaration. */ +function exportFlags(result: ExtractionResult): Record { + const flags: Record = {}; + for (const node of result.nodes) { + if (DECLARATION_KINDS.has(node.kind)) flags[node.name] = node.isExported === true; + } + return flags; +} + +describe('TypeScript ambient module bodies export what they declare', () => { + let savedEnv: Record = {}; + + beforeAll(async () => { + await initGrammars(); + await loadGrammarsForLanguages(['typescript', 'tsx']); + }); + + 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; + process.env.CODEGRAPH_KERNEL_LANGS = 'all'; + 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)(`declare module and declare global export their declarations: %s${label}`, (backend) => { + const result = extract(backend, 'src/chart.registry.ts', eol(MODULE_SOURCE), 'typescript'); + expect(exportFlags(result)).toEqual({ + // declare module 'chart.js', and a namespace nested in it. + PluginOptionsByType: true, + WrittenOut: true, + Nested: true, + Alias: true, + Position: true, + Positioner: true, + defaults: true, + // declare global, and a namespace nested in it. + Window: true, + ProcessEnv: true, + bitwardenContainerService: true, + // Neither is an outside scope: a module file's own namespaces. + OwnNamespace: false, + Hidden: false, + Shown: true, + FileLocal: false, + appConfig: true, + }); + }); + + it.each(backends)(`a body with an export declaration exports only what it names: %s${label}`, (backend) => { + const result = extract(backend, 'types/legacy.ts', eol(SCRIPT_SOURCE), 'typescript'); + expect(exportFlags(result)).toEqual({ + Options: false, + main: false, + Unlisted: false, + Listed: true, + Open: true, + }); + }); + + it.each(backends)(`tsx reads the same blocks: %s${label}`, (backend) => { + const source = eol(`declare module '@tanstack/react-router' {\n interface Register {\n router: unknown;\n }\n}\nexport const App = () =>
;\n`); + const result = extract(backend, 'src/main.tsx', source, 'tsx'); + expect(exportFlags(result).Register).toBe(true); + }); + } + +}); diff --git a/codegraph-kernel/src/ccpp/brace_scopes.rs b/codegraph-kernel/src/ccpp/brace_scopes.rs new file mode 100644 index 0000000000..4fa77b7d55 --- /dev/null +++ b/codegraph-kernel/src/ccpp/brace_scopes.rs @@ -0,0 +1,409 @@ +//! C++ scopes read from the source's braces, for a file whose parse tree has +//! errors — a port of src/extraction/languages/cpp-brace-scopes.ts (read its +//! header for why). The scan uses byte offsets; emitted columns use the +//! walker's shared UTF-16 column cache. +//! +//! Files with parse errors use these scopes during native extraction. + +use std::collections::HashMap; + +/// Intervals that nest properly, added in order of their opening offset. +pub struct NestedIntervals { + opens: Vec, + closes: Vec, + parents: Vec>, + values: Vec, +} + +impl NestedIntervals { + pub fn new() -> Self { + NestedIntervals { opens: Vec::new(), closes: Vec::new(), parents: Vec::new(), values: Vec::new() } + } + + /// Adds the interval `(open, close)`. One that opens before the last one + /// added, or crosses one that holds it, is refused (false). + pub fn add(&mut self, open: usize, close: usize, value: T) -> bool { + if close <= open || self.opens.last().is_some_and(|&last| open <= last) { + return false; + } + let parent = self.innermost(open); + if let Some(p) = parent { + if close >= self.closes[p] { + return false; + } + } + self.opens.push(open); + self.closes.push(close); + self.parents.push(parent); + self.values.push(value); + true + } + + /// The values of the intervals holding `offset` (open < offset < close), outermost first. + pub fn at(&self, offset: usize) -> Vec { + let mut out = Vec::new(); + let mut k = self.innermost(offset); + while let Some(i) = k { + out.push(self.values[i].clone()); + k = self.parents[i]; + } + out.reverse(); + out + } + + /// The innermost interval holding `offset`: the last interval opening + /// before `offset` or one of the intervals around that one. + fn innermost(&self, offset: usize) -> Option { + let count = self.opens.partition_point(|&o| o < offset); + let mut k = if count == 0 { None } else { Some(count - 1) }; + while let Some(i) = k { + if self.closes[i] > offset { + break; + } + k = self.parents[i]; + } + k + } +} + +pub struct BraceScopes { + closes: HashMap, + namespaces: NestedIntervals, +} + +impl BraceScopes { + /// The offset of the `}` that closes the `{` at `open`. + pub fn close_of(&self, open: usize) -> Option { + self.closes.get(&open).copied() + } + + /// The named namespaces whose braces hold `offset`, outermost first, as written. + pub fn namespaces_at(&self, offset: usize) -> Vec { + self.namespaces.at(offset) + } + +} + +fn is_blank(c: u8) -> bool { + matches!(c, b' ' | b'\t' | b'\r' | 0x0c | 0x0b) +} +fn is_word_start(c: u8) -> bool { + c.is_ascii_alphabetic() || c == b'_' || c == b'$' || c >= 0x80 +} +fn is_word_char(c: u8) -> bool { + is_word_start(c) || c.is_ascii_digit() +} +fn at(s: &[u8], i: usize) -> u8 { + s.get(i).copied().unwrap_or(0) +} +fn find(src: &str, from: usize, pat: &str) -> Option { + src.get(from..).and_then(|rest| rest.find(pat)).map(|k| from + k) +} + +/// The end of a `//` comment starting at `i`: its newline (a `\` before it continues the comment). +fn line_comment_end(src: &str, mut i: usize) -> usize { + let s = src.as_bytes(); + loop { + let Some(nl) = find(src, i, "\n") else { return s.len() }; + let mut k = nl as isize - 1; + if k >= 0 && s[k as usize] == b'\r' { + k -= 1; + } + if k < 0 || s[k as usize] != b'\\' { + return nl; + } + i = nl + 1; + } +} + +/// Past whitespace and comments from `i`. +fn skip_blank(src: &str, mut i: usize) -> usize { + let s = src.as_bytes(); + while i < s.len() { + let c = s[i]; + if is_blank(c) || c == b'\n' { + i += 1; + } else if c == b'/' && at(s, i + 1) == b'/' { + i = line_comment_end(src, i + 2); + } else if c == b'/' && at(s, i + 1) == b'*' { + i = find(src, i + 2, "*/").map_or(s.len(), |e| e + 2); + } else { + break; + } + } + i +} + +/// The end of the identifier at `i`, if one starts there. +fn word_end(s: &[u8], i: usize) -> Option { + if i >= s.len() || !is_word_start(s[i]) { + return None; + } + let mut j = i + 1; + while j < s.len() && is_word_char(s[j]) { + j += 1; + } + Some(j) +} + +/// Past the string or character literal whose quote is at `i` (it stops at a newline). +fn quoted_end(s: &[u8], i: usize) -> usize { + let quote = s[i]; + let mut j = i + 1; + while j < s.len() { + let c = s[j]; + if c == b'\\' { + j += if at(s, j + 1) == b'\r' && at(s, j + 2) == b'\n' { 3 } else { 2 }; + } else if c == quote { + return j + 1; + } else if c == b'\n' { + return j; + } else { + j += 1; + } + } + s.len() +} + +/// Past the `(…)` group at `i` (strings inside skipped). +fn paren_group_end(s: &[u8], i: usize) -> Option { + let mut depth = 0usize; + let mut j = i; + while j < s.len() { + let c = s[j]; + if c == b'"' || c == b'\'' { + j = quoted_end(s, j); + continue; + } + if c == b'(' { + depth += 1; + } else if c == b')' { + depth = depth.saturating_sub(1); + if depth == 0 { + return Some(j + 1); + } + } + j += 1; + } + None +} + +/// namespaceHead: after the `namespace` keyword ending at `from`, the name as +/// written and the offset of the body's `{`. +fn namespace_head(src: &str, from: usize) -> Option<(String, usize)> { + let s = src.as_bytes(); + let mut i = skip_blank(src, from); + while src.get(i..).is_some_and(|r| r.starts_with("[[")) { + let end = find(src, i + 2, "]]")?; + i = skip_blank(src, end + 2); + } + let mut name: Option<(usize, usize)> = None; + while let Some(end) = word_end(s, i) { + name = Some((name.map_or(i, |(start, _)| start), end)); + i = skip_blank(src, end); + if !src.get(i..).is_some_and(|r| r.starts_with("::")) { + break; + } + i = skip_blank(src, i + 2); + if let Some(inline) = word_end(s, i) { + if &src[i..inline] == "inline" { + i = skip_blank(src, inline); + } + } + } + if name.is_some() { + while let Some(end) = word_end(s, i) { + let paren = skip_blank(src, end); + if at(s, paren) != b'(' { + return None; + } + i = skip_blank(src, paren_group_end(s, paren)?); + } + } + if at(s, i) != b'{' { + return None; + } + Some((name.map_or(String::new(), |(start, end)| src[start..end].to_string()), i)) +} + +fn is_raw_delimiter(d: &[u8]) -> bool { + d.len() <= 16 && d.iter().all(|&b| (0x21..=0x7e).contains(&b) && !matches!(b, b'"' | b'(' | b')' | b'\\')) +} + +fn is_raw_prefix(w: &[u8]) -> bool { + matches!(w, b"R" | b"uR" | b"UR" | b"LR" | b"u8R") +} + +struct Branch { + at_if: Vec, + after_first: Option>, +} + +/// scanCppBraceScopes: `src` as the parser saw it (after preParse). None when +/// the braces don't balance. +pub fn scan(src: &str) -> Option { + let s = src.as_bytes(); + let n = s.len(); + let mut closes: HashMap = HashMap::new(); + let mut open: Vec = Vec::new(); + let mut branches: Vec = Vec::new(); + let mut namespace_at: Vec<(usize, String)> = Vec::new(); + let mut pending_namespace: Option<(String, usize)> = None; + let mut balanced = true; + let mut line_start = true; + let mut word_start: usize = 0; + let mut word_end_at: Option = None; + // Past a byte-order mark, so line 1 can be a directive. + let mut i = if src.starts_with('\u{feff}') { 3 } else { 0 }; + while i < n { + let c = s[i]; + if c == b'\n' { + line_start = true; + i += 1; + continue; + } + if is_blank(c) { + i += 1; + continue; + } + if c == b'/' && at(s, i + 1) == b'/' { + i = line_comment_end(src, i + 2); + continue; + } + if c == b'/' && at(s, i + 1) == b'*' { + i = find(src, i + 2, "*/").map_or(n, |e| e + 2); + continue; + } + if c == b'#' && line_start { + let mut j = i + 1; + while j < n && is_blank(s[j]) { + j += 1; + } + let directive: &[u8] = match word_end(s, j) { + Some(e) => &s[j..e], + None => b"", + }; + while j < n { + let d = s[j]; + if d == b'\n' { + let mut k = j as isize - 1; + if k >= 0 && s[k as usize] == b'\r' { + k -= 1; + } + if k < 0 || s[k as usize] != b'\\' { + break; + } + j += 1; + } else if d == b'/' && at(s, j + 1) == b'/' { + j = line_comment_end(src, j + 2); + } else if d == b'/' && at(s, j + 1) == b'*' { + j = find(src, j + 2, "*/").map_or(n, |e| e + 2); + } else if d == b'"' { + j = quoted_end(s, j); + } else { + j += 1; + } + } + match directive { + b"if" | b"ifdef" | b"ifndef" => branches.push(Branch { at_if: open.clone(), after_first: None }), + b"else" | b"elif" | b"elifdef" | b"elifndef" => { + if let Some(group) = branches.last_mut() { + let at_if = group.at_if.clone(); + let current = std::mem::replace(&mut open, at_if); + if group.after_first.is_none() { + group.after_first = Some(current); + } + } + } + b"endif" => { + if let Some(group) = branches.pop() { + if let Some(after) = group.after_first { + open = after; + } + } + } + _ => {} + } + i = j; + continue; + } + line_start = false; + if c == b'"' { + if word_end_at == Some(i) && is_raw_prefix(&s[word_start..i]) { + if let Some(paren) = find(src, i + 1, "(") { + let delimiter = &src[i + 1..paren]; + if is_raw_delimiter(delimiter.as_bytes()) { + let close = format!("){delimiter}\""); + i = find(src, paren + 1, &close).map_or(n, |e| e + delimiter.len() + 2); + continue; + } + } + } + i = quoted_end(s, i); + continue; + } + if c == b'\'' { + i = quoted_end(s, i); + continue; + } + if c.is_ascii_digit() || (c == b'.' && at(s, i + 1).is_ascii_digit()) { + let mut j = i + 1; + while j < n { + let d = s[j]; + if is_word_char(d) || d == b'.' { + j += 1; + } else if d == b'\'' && is_word_char(at(s, j + 1)) { + j += 2; + } else if (d == b'+' || d == b'-') && matches!(s[j - 1], b'e' | b'E' | b'p' | b'P') { + j += 1; + } else { + break; + } + } + word_start = i; + word_end_at = Some(j); + i = j; + continue; + } + if is_word_start(c) { + let end = word_end(s, i).unwrap_or(i + 1); + word_start = i; + word_end_at = Some(end); + if &s[i..end] == b"namespace" { + if let Some(head) = namespace_head(src, end) { + pending_namespace = Some(head); + } + } + i = end; + continue; + } + if c == b'{' { + open.push(i); + if pending_namespace.as_ref().is_some_and(|(_, brace)| *brace == i) { + let (name, _) = pending_namespace.take().unwrap(); + if !name.is_empty() { + namespace_at.push((i, name)); + } + } + } else if c == b'}' { + match open.pop() { + None => balanced = false, + Some(from) => { + closes.entry(from).or_insert(i); + } + } + } + i += 1; + } + if !balanced || !open.is_empty() { + return None; + } + namespace_at.sort_by_key(|(brace, _)| *brace); + let mut namespaces = NestedIntervals::new(); + for (brace, name) in namespace_at { + if let Some(&close) = closes.get(&brace) { + namespaces.add(brace, close, name); + } + } + Some(BraceScopes { closes, namespaces }) +} diff --git a/codegraph-kernel/src/ccpp/mod.rs b/codegraph-kernel/src/ccpp/mod.rs index da0b18356e..33a83cd1d6 100644 --- a/codegraph-kernel/src/ccpp/mod.rs +++ b/codegraph-kernel/src/ccpp/mod.rs @@ -14,6 +14,10 @@ //! - cpp namespace prefix stack (#1291): named `namespace a::b {` pushes the //! name AS WRITTEN onto the qualifiedName prefix; anonymous falls through. //! No namespace NODE is minted (#1093 crowd-out). +//! - cpp brace scopes (brace_scopes.rs): when the tree has errors, each node +//! is walked in the namespaces, and at declaration level the classes, the +//! source's braces put it in, not the tree's (error recovery closes scopes +//! at the wrong `}`). Files with parse errors use this recovery path. //! - out-of-line `Cls::method` defs: name = LAST `::` segment of the //! declarator's qualified_identifier (BFS that skips parameter_list + //! trailing_return_type), receiver = the template-stripped qualifier, @@ -90,6 +94,9 @@ use std::collections::{HashMap, HashSet, VecDeque}; use std::sync::OnceLock; use tree_sitter::Node; +mod brace_scopes; +use brace_scopes::{BraceScopes, NestedIntervals}; + // --- compiled regexes (JS \w/\s spelled as ASCII classes for parity) --------- @@ -338,6 +345,12 @@ pub struct Walker<'t> { /// Each class body's access specifiers as (start byte, visibility), by /// body node id: members look up the nearest preceding one. access_specifiers: RefCell>>, + /// A cpp file whose tree has errors is walked in the scopes its braces + /// open (visit_in_brace_scope; only when the parsed tree has errors): the + /// scan, and the class-like nodes extracted so far by their bodies' braces. + brace_scopes: Option, + class_scopes: NestedIntervals, + class_scope_rows: HashSet, } pub fn extract(file_path: &str, source: &str, language: &str) -> Result { @@ -349,6 +362,9 @@ pub fn extract(file_path: &str, source: &str, language: &str) -> Result Walker<'t> { md_ref_keys: HashSet::new(), line_count: source.bytes().filter(|b| *b == b'\n').count() as u32 + 1, access_specifiers: RefCell::new(HashMap::new()), + brace_scopes: None, + class_scopes: NestedIntervals::new(), + class_scope_rows: HashSet::new(), } } markdown_refs_impl!(); @@ -536,6 +555,61 @@ impl<'t> Walker<'t> { // --- visitNode ----------------------------------------------------------- fn visit_node(&mut self, node: Node<'t>) { + stack_guard!(); + if self.brace_scopes.is_some() { + self.visit_in_brace_scope(node); + } else { + self.dispatch_node(node); + } + } + + /// visitInCppBraceScope: walk `node` in the namespaces and classes its + /// source braces put it in (see brace_scopes.rs). The namespaces apply + /// everywhere; the enclosing classes only at declaration level, where the + /// stack above the file node holds nothing but class scopes. + fn visit_in_brace_scope(&mut self, node: Node<'t>) { + stack_guard!(); + let at = node.start_byte(); + let namespaces = self.brace_scopes.as_ref().map(|s| s.namespaces_at(at)).unwrap_or_default(); + let saved = std::mem::replace(&mut self.namespace_prefix, namespaces); + let mut base = self.stack.len(); + while base > 1 && self.class_scope_rows.contains(&self.stack[base - 1].row) { + base -= 1; + } + let mut walked: Option> = None; + if base == 1 { + let classes = self.class_scopes.at(at); + let same = classes.len() == self.stack.len() - base + && classes.iter().zip(&self.stack[base..]).all(|(c, s)| c.row == s.row); + if !same { + walked = Some(self.stack.split_off(base)); + self.stack.extend(classes); + } + } + self.dispatch_node(node); + if let Some(walked) = walked { + self.stack.truncate(base); + self.stack.extend(walked); + } + self.namespace_prefix = saved; + } + + /// cppBodyEnd: where a class-like node ends in brace scopes — the `}` + /// that closes its body — or None to keep the tree's. + fn brace_body_end(&self, body: Node<'t>) -> Option<(u32, u32)> { + let close = self.brace_scopes.as_ref()?.close_of(body.start_byte())?; + Some(self.cols.position(self.src, close + 1)) + } + + /// openCppClassScope: a class-like node's body as a scope for visit_in_brace_scope. + fn open_class_scope(&mut self, scope: &Scope, body: Node<'t>) { + let Some(close) = self.brace_scopes.as_ref().and_then(|s| s.close_of(body.start_byte())) else { return }; + if self.class_scopes.add(body.start_byte(), close, scope.clone()) { + self.class_scope_rows.insert(scope.row); + } + } + + fn dispatch_node(&mut self, node: Node<'t>) { stack_guard!(); let kind = node.kind(); let mut skip_children = false; @@ -609,6 +683,19 @@ impl<'t> Walker<'t> { } else if self.is_attribute_prototype_part(node) { skip_children = true; } else if kind == "declaration" && !self.inside_class_like() { + // In brace scopes, a class the tree reads as a declaration's type + // (glued to the tokens after it by error recovery) is still a class. + if self.brace_scopes.is_some() { + if let Some(t) = node.child_by_field_name("type") { + let class_like = matches!( + t.kind(), + "class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier" + ); + if class_like && t.child_by_field_name("body").is_some() { + self.visit_node(t); + } + } + } self.extract_variable(node); self.scan_fn_ref_subtree(node, 0); skip_children = true; diff --git a/codegraph-kernel/src/ccpp/types.rs b/codegraph-kernel/src/ccpp/types.rs index a874839885..5e1541ee4d 100644 --- a/codegraph-kernel/src/ccpp/types.rs +++ b/codegraph-kernel/src/ccpp/types.rs @@ -12,11 +12,14 @@ impl<'t> Walker<'t> { let extra = Extra { docstring: preceding_docstring(node, self.src), visibility: self.visibility_of(node), + end: self.brace_body_end(body), ..Extra::default() }; let Some(row) = self.create_node("class", &name, node, extra) else { return }; self.extract_inheritance(node, row); - self.stack.push(Scope { row, kind: "class", name }); + let scope = Scope { row, kind: "class", name }; + self.open_class_scope(&scope, body); + self.stack.push(scope); for c in named_kids(body) { self.visit_node(c); } @@ -31,11 +34,14 @@ impl<'t> Walker<'t> { let extra = Extra { docstring: preceding_docstring(node, self.src), visibility: if self.variant == Variant::Cpp { self.visibility_of(node) } else { None }, + end: self.brace_body_end(body), ..Extra::default() }; let Some(row) = self.create_node(kind, &name, node, extra) else { return }; self.extract_inheritance(node, row); - self.stack.push(Scope { row, kind, name }); + let scope = Scope { row, kind, name }; + self.open_class_scope(&scope, body); + self.stack.push(scope); for c in named_kids(body) { self.visit_node(c); } diff --git a/codegraph-kernel/src/go/mod.rs b/codegraph-kernel/src/go/mod.rs index af4c91eabb..196c240a58 100644 --- a/codegraph-kernel/src/go/mod.rs +++ b/codegraph-kernel/src/go/mod.rs @@ -107,7 +107,10 @@ pub fn extract(file_path: &str, source: &str) -> Result { w.node_ids.push(ids::file_node_id(file_path)); w.stack.push(Scope { row: 0, kind: "file", name: base_name.to_string() }); + let assumed_imports = w.assumed_imports(tree.root_node()); + w.imported_names.extend(assumed_imports.iter().map(|(name, _, _)| name.clone())); w.visit_node(tree.root_node()); + for (name, path, spec) in assumed_imports { w.emit_import_binding(&name, &path, spec); } w.flush_fn_ref_candidates(); w.flush_value_refs(tree.root_node()); w.stack.pop(); @@ -415,7 +418,8 @@ impl<'t> Walker<'t> { self.stack.pop(); } - /// extractTypeAlias for Go: type_spec → struct / interface / plain alias. + /// extractTypeAlias for Go: type_spec (`type A B`) and type_alias + /// (`type A = B`) → struct / interface / plain alias. fn extract_type_alias(&mut self, node: Node<'t>) -> bool { stack_guard!(); let name = self.extract_name(node); @@ -466,14 +470,22 @@ impl<'t> Walker<'t> { return true; } - self.create_node( + let row = self.create_node( "type_alias", &name, node, Extra { docstring, is_exported, ..Extra::default() }, ); - // (go type_spec has no `value` field — no type-ref walk; TS/tsx member - // extraction is TS-family-only) + // (go has no `value` field — no TS-style type-ref walk or member + // extraction.) An alias references what its `type` field names; a + // defined type (`type_spec`) declares a type of its own. + if let Some(row) = row { + let references = crate::buffers::EDGE_REFERENCES; + for ty in self.alias_type_names(node) { + let text = self.text(ty).to_string(); + self.push_ref_at(row, &text, references, ty); + } + } false } @@ -604,6 +616,50 @@ impl<'t> Walker<'t> { } } + /// Keep the written/path name and add the conventional package name only + /// when no other import in the file binds or assumes that name. + fn assumed_imports(&self, root: Node<'t>) -> Vec<(String, String, Node<'t>)> { + let mut imports = Vec::new(); + // Imports are top-level declarations. Do not revisit unrelated deep + // function bodies after the guarded walker has deferred the file. + let mut pending: Vec<_> = named_kids(root).filter(|n| n.kind() == "import_declaration").collect(); + while let Some(node) = pending.pop() { + if node.kind() == "import_spec" { imports.push(node); } + else { pending.extend(named_kids(node)); } + } + imports.sort_by_key(|node| node.start_byte()); + let mut bound = HashSet::new(); + let mut order = Vec::new(); + let mut assumed: HashMap)>> = HashMap::new(); + for spec in imports { + let Some(lit) = spec.child_by_field_name("path") + .or_else(|| named_kids(spec).find(|c| c.kind() == "interpreted_string_literal")) else { continue }; + let path = self.text(lit).trim_matches('"').to_string(); + let local = self.import_local_name(spec, &path); + bound.insert(local); + if spec.child_by_field_name("name").is_some() { continue; } + let mut parts = path.rsplit('/'); + let last = parts.next().unwrap_or(""); + let base = if last.starts_with('v') && last.len() > 1 + && last[1..].bytes().all(|b| b.is_ascii_digit()) { + parts.next().unwrap_or(last) + } else { last }; + let base = base.strip_prefix("go-").unwrap_or(base); + let name: String = base.chars().take_while(|c| c.is_alphanumeric() || *c == '_').collect(); + if name.chars().next().is_some_and(|c| c.is_alphabetic() || c == '_') { + let candidates = assumed.entry(name.clone()).or_default(); + if candidates.is_empty() { order.push(name); } + candidates.push((path, spec)); + } + } + order.into_iter().filter_map(|name| { + let candidates = &assumed[&name]; + if bound.contains(&name) || candidates.len() != 1 { return None; } + let (path, spec) = &candidates[0]; + Some((name, path.clone(), *spec)) + }).collect() + } + // --- bindings (resolution-binding-model-plan.md, Phase 3: Go) -------------------- /// The local name an import binds: the alias when written (`f "fmt"`, a @@ -770,6 +826,45 @@ impl<'t> Walker<'t> { } } + /// goAliasTypeNames (languages/go.ts): the name nodes of the types an + /// alias's `type` field names, in source order, but its own type + /// parameters and the predeclared types. Generic aliases parse as a + /// `type_spec` with an error child containing `=` in this grammar. + fn alias_type_names(&self, node: Node<'t>) -> Vec> { + let mut names = Vec::new(); + let is_alias = node.kind() == "type_alias" || named_kids(node).any(|c| c.kind() == "ERROR" && self.text(c).trim() == "="); + if !is_alias { return names; } + let Some(ty) = node.child_by_field_name("type") else { return names }; + let mut params: HashSet<&str> = HashSet::new(); + if let Some(list) = node.child_by_field_name("type_parameters") { + for decl in (0..list.named_child_count()).filter_map(|i| list.named_child(i)) { + for c in (0..decl.named_child_count()).filter_map(|j| decl.named_child(j)) { + if c.kind() == "identifier" { + params.insert(self.text(c)); + } + } + } + } + self.collect_alias_type_names(ty, ¶ms, &mut names); + names + } + + fn collect_alias_type_names(&self, node: Node<'t>, params: &HashSet<&str>, out: &mut Vec>) { + stack_guard!(); + if node.kind() == "type_identifier" { + let text = self.text(node); + if !params.contains(text) && !is_go_predeclared_type(text) { + out.push(node); + } + return; + } + for i in 0..node.named_child_count() { + if let Some(c) = node.named_child(i) { + self.collect_alias_type_names(c, params, out); + } + } + } + /// extractInheritance — the Go branches: interface embedding (a type_elem /// holding one named type; a union, `~T` or basic type is a constraint) /// and struct embedding (field_declaration without a field_identifier), @@ -849,4 +944,3 @@ fn is_go_predeclared_type(name: &str) -> bool { | "uint8" | "uint16" | "uint32" | "uint64" | "uintptr" ) } - diff --git a/codegraph-kernel/src/go/refs.rs b/codegraph-kernel/src/go/refs.rs index 4c1b72a78d..4e22c674c7 100644 --- a/codegraph-kernel/src/go/refs.rs +++ b/codegraph-kernel/src/go/refs.rs @@ -81,7 +81,8 @@ impl<'t> Walker<'t> { // A method value `c.store.Fetch` keeps its receiver (#1820). "selector_expression" => { if let Some(field) = v.child_by_field_name("field") { - self.fn_ref_cands.extend(Cand::member(from, self.text(v), field)); + let path = self.selector_path(v); + self.fn_ref_cands.extend(Cand::member(from, &path, field)); } } "literal_element" | "expression_list" => { @@ -93,6 +94,18 @@ impl<'t> Walker<'t> { } } + fn selector_path(&self, mut node: Node<'t>) -> String { + let mut fields = Vec::new(); + while node.kind() == "selector_expression" { + let (Some(operand), Some(field)) = (node.child_by_field_name("operand"), node.child_by_field_name("field")) else { break; }; + fields.push(self.text(field)); + node = operand; + } + let mut path = self.text(node).trim().to_string(); + for field in fields.into_iter().rev() { path.push('.'); path.push_str(field); } + path + } + pub(super) fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) { stack_guard!(); if depth > 12 { diff --git a/codegraph-kernel/src/resolve/cpp_aliases.rs b/codegraph-kernel/src/resolve/cpp_aliases.rs index ee5e625483..76ce08b47d 100644 --- a/codegraph-kernel/src/resolve/cpp_aliases.rs +++ b/codegraph-kernel/src/resolve/cpp_aliases.rs @@ -151,7 +151,8 @@ impl KernelResolver { } let mut alias = local.map(|(rhs, node)| (rhs, node, 1usize)); if alias.is_none() { - let scopes: Vec<_> = if raw.trim().starts_with("::") { Vec::new() } else { caller.as_ref().map(|n| n.qualified_name.split("::").map(str::to_string).collect()).unwrap_or_default() }; + let mut scopes: Vec<_> = if raw.trim().starts_with("::") { Vec::new() } else { caller.as_ref().map(|n| n.qualified_name.split("::").map(str::to_string).collect()).unwrap_or_default() }; + if is_inheritance_ref(&r.reference_kind) { scopes.pop(); } for i in (0..=scopes.len()).rev() { let scope = scopes[..i].join("::"); let mut found = None; @@ -203,9 +204,8 @@ mod tests { impl KernelResolver { /// Only the caller's function or class proves which declaration owns a receiver. - pub(super) fn cpp_receiver_alias(&mut self, receiver: &str, r: &ResolveRefIn) -> Res> { + pub(super) fn cpp_owned_receiver_declaration(&mut self, receiver: &str, r: &ResolveRefIn) -> Res> { let Some(caller) = self.node_by_id(&r.from_node_id)? else { return Ok(None); }; - let included = self.namespace_visible_files(&r.file_path, "cpp")?; let mut files = vec![r.file_path.clone()]; for ext in [".h", ".hpp", ".hxx"] { let header = re!(r"(?i)\.(?:c|cc|cpp|cxx)$").replace(&r.file_path, ext).to_string(); @@ -214,27 +214,58 @@ impl KernelResolver { let escaped_receiver = regex::escape(receiver); for file in files { let Some(source) = self.read_file(&file) else { continue; }; - let indexes: Vec<_> = if file == r.file_path { (0..(r.line.max(0) as usize).min(source.len())).rev().collect() } else { (0..source.len()).collect() }; + let lines = source.cpp_code_lines(); + let indexes: Vec<_> = if file == r.file_path { (0..(r.line.max(0) as usize).min(lines.len())).rev().collect() } else { (0..lines.len()).collect() }; + let classes = self.nodes_in_file(&file)?; for i in indexes { - let line = &source[i]; + let line = lines[i].as_str(); if !has_word(line, receiver) { continue; } - let Some(raw) = self.cpp_declarator_match(line, &escaped_receiver)? else { continue; }; - let at = line.find(receiver).unwrap_or(line.len()); - if line[..at].contains("//") || re!(r"^\s*(?://|/\*|\*)").is_match(line) { return Ok(None); } + let Some(raw) = self.cpp_declarator_match(line, &escaped_receiver)? else { + let range = shared_regex(&format!(r"\bfor\s*\(.*\b{}\s*:", escaped_receiver))?; + let structured = shared_regex(&format!(r"\bauto\s*&{{0,2}}\s*\[[^\]]*\b{}\b[^\]]*\]", escaped_receiver))?; + if range.find(line).is_some_and(|m| !line[m.end()..].starts_with(':')) || structured.is_match(line) { return Ok(None); } + continue; + }; let own_function = file == r.file_path && i as i64 + 1 >= caller.start_line; - let mut own_class = false; - if let Some((scope, _)) = caller.qualified_name.rsplit_once("::") { - own_class = self.nodes_by_qualified_name(scope)?.iter().any(|n| n.file_path == file && matches!(n.kind.as_str(), "class" | "struct" | "union") && i as i64 + 1 >= n.start_line && (i as i64) < n.end_line); - } + let own_class = caller.qualified_name.rsplit_once("::").is_some_and(|(scope, _)| { + classes.iter().filter(|n| matches!(n.kind.as_str(), "class" | "struct" | "union") + && i as i64 + 1 >= n.start_line && (i as i64) < n.end_line) + .max_by_key(|n| (n.start_line, n.start_column)) + .is_some_and(|n| n.qualified_name == scope) + }); if !own_function && !own_class { return Ok(None); } - let mut alias = self.cpp_alias_expansion(&raw, r, 0, &included)?; - if let Some(alias) = &mut alias { alias.pointer |= pointer_type(&raw); alias.reference |= reference_type(&raw); } - return Ok(alias); + return Ok(Some(raw)); } } Ok(None) } + pub(super) fn cpp_receiver_alias(&mut self, receiver: &str, r: &ResolveRefIn) -> Res> { + let Some(raw) = self.cpp_owned_receiver_declaration(receiver, r)? else { return Ok(None); }; + let included = self.namespace_visible_files(&r.file_path, "cpp")?; + let mut alias = self.cpp_alias_expansion(&raw, r, 0, &included)?; + if let Some(alias) = &mut alias { alias.pointer |= pointer_type(&raw); alias.reference |= reference_type(&raw); } + Ok(alias) + } + + pub(super) fn cpp_external_receiver(&mut self, receiver: &str, r: &ResolveRefIn) -> Res { + let Some(raw) = self.cpp_owned_receiver_declaration(receiver, r)? else { return Ok(false); }; + if raw.chars().filter(|c| *c == '<').count() != raw.chars().filter(|c| *c == '>').count() { return Ok(false); } + let Some(parts) = type_segments(&raw) else { return Ok(false); }; + let Some(name) = parts.last() else { return Ok(false); }; + if parts.len() < 2 || !name.starts_with(|c: char| c.is_ascii_lowercase()) || name.ends_with("_t") { return Ok(false); } + for namespace in &parts[..parts.len()-1] { + if self.nodes_by_name(namespace)?.iter().any(|n| matches!(n.language.as_str(), "c" | "cpp") + && matches!(n.kind.as_str(), "class" | "struct" | "union" | "enum" | "interface" | "type_alias")) { return Ok(false); } + } + let spelled = parts.join("::"); + if self.nodes_by_name(name)?.iter().any(|n| matches!(n.language.as_str(), "c" | "cpp") + && matches!(n.kind.as_str(), "class" | "struct" | "union" | "enum" | "interface" | "type_alias") + && (parts[0] != "std" || n.qualified_name == spelled || n.qualified_name.ends_with(&format!("::{spelled}")))) { return Ok(false); } + Ok(if pointer_type(&raw) { self.cpp_receiver_operator_is(receiver, "->", r) } + else { self.cpp_receiver_operator_is(receiver, ".", r) }) + } + /// Only alias dereference or a project template can outgrow the declared owner. pub(super) fn cpp_alias_allows_fallback(&mut self, r: &ResolveRefIn) -> Res { if r.language != "cpp" { return Ok(false); } diff --git a/codegraph-kernel/src/resolve/cpp_namespace.rs b/codegraph-kernel/src/resolve/cpp_namespace.rs index 2da11dfba0..274ae6e3ea 100644 --- a/codegraph-kernel/src/resolve/cpp_namespace.rs +++ b/codegraph-kernel/src/resolve/cpp_namespace.rs @@ -79,10 +79,9 @@ impl KernelResolver { let mut seen=HashSet::new();let mut pending=vec![file.to_string()]; while let Some(file)=pending.pop() { if !seen.insert(file.clone()) {continue;} - let Some(source)=self.read_file(&file) else {continue}; - let code=super::awaited::strip_ts_comments(source.text()); - for m in re!(r#"(?m)^\s*#\s*include\s*([<"])([^>"]+)[>"]"#).captures_iter(&code) { - if let Some(target)=self.resolve_cpp_include(&file,m[1].chars().next().unwrap(),&m[2],language)? {pending.push(target);} + for node in self.nodes_in_file(&file)?.iter().filter(|n| n.kind == "import" && matches!(n.language.as_str(), "c" | "cpp")) { + let delimiter = if node.signature.as_deref().is_some_and(|s| s.contains('<')) { '<' } else { '"' }; + if let Some(target)=self.resolve_cpp_include(&file,delimiter,&node.name,language)? {pending.push(target);} } } self.cpp_namespaces.includes.insert(file.to_string(),seen.clone());Ok(seen) diff --git a/codegraph-kernel/src/resolve/cpp_receivers.rs b/codegraph-kernel/src/resolve/cpp_receivers.rs index ab19f0eb23..33233b0fa8 100644 --- a/codegraph-kernel/src/resolve/cpp_receivers.rs +++ b/codegraph-kernel/src/resolve/cpp_receivers.rs @@ -3,6 +3,76 @@ use super::*; use super::iteration::{descendant_for_position, named_children}; impl KernelResolver { + /// Plain receivers use lexical AST declarations before any backward text scan. + pub(super) fn cpp_plain_call(&mut self, r: &ResolveRefIn) -> Res>> { + if r.language != "cpp" || r.reference_kind != "calls" { return Ok(None); } + let method = r.reference_name.rsplit(['.', '>']).next().unwrap_or(&r.reference_name); + if !re!(r"^[A-Za-z_]\w*$").is_match(method) { return Ok(None); } + let Some(source) = self.read_file(&r.file_path) else { return Ok(None) }; + let Some(tree) = self.parsed_tree(&source, r) else { return Ok(None) }; + let mut at = descendant_for_position(tree.root_node(), source.text(), ((r.line - 1).max(0) as usize, r.column.max(0) as usize + 1)); + while at.kind() != "call_expression" { + let Some(parent) = at.parent() else { return Ok(None) }; at = parent; + } + let Some(function) = at.child_by_field_name("function").filter(|n| n.kind() == "field_expression") else { return Ok(None) }; + let Some(field) = function.child_by_field_name("field") else { return Ok(None) }; + if &source.text()[field.start_byte()..field.end_byte()] != method { return Ok(None); } + let Some(argument) = function.child_by_field_name("argument") else { return Ok(None) }; + let operator = super::awaited::strip_ts_comments(&source.text()[argument.end_byte()..field.start_byte()]); + let mut receiver = argument; + while receiver.kind() == "parenthesized_expression" { + let children = named_children(receiver); + let [only] = children.as_slice() else { return Ok(None) }; receiver = *only; + } + if receiver.kind() != "identifier" { return Ok(None); } + let name = &source.text()[receiver.start_byte()..receiver.end_byte()]; + let mut raw = None; + let mut scope = receiver; + while let Some(parent) = scope.parent() { + if let Some(ty) = cpp_binding_type(parent, scope, name, &source) { raw = Some(ty); break; } + if parent.kind() == "for_range_loop" && parent.child_by_field_name("declarator").is_some_and(|n| cpp_declarator_name(n, &source) == name) { + raw = parent.child_by_field_name("type").zip(parent.child_by_field_name("declarator")).map(|(ty, declarator)| cpp_ast_declared_type(ty, declarator, &source)); + break; + } + scope = parent; + } + if raw.is_none() { + if let Some(caller) = self.node_by_id(&r.from_node_id)? { + if let Some((owner_name, _)) = caller.qualified_name.rsplit_once("::") { + let visible = self.namespace_visible_files(&r.file_path, "cpp")?; + let owners = self.nodes_by_qualified_name(owner_name)?; + let owners: Vec<_> = owners.iter().filter(|n| matches!(n.kind.as_str(), "class" | "struct" | "union") && visible.contains(&n.file_path)).collect(); + if let [owner] = owners.as_slice() { raw = self.cpp_field_type(owner, name, r)?; } + } + } + } + let Some(raw) = raw else { return Ok(None) }; + if raw.trim_end_matches('&').trim() == "auto" { return Ok(None); } + if raw.contains('[') { return Ok(Some(None)); } + let included = self.namespace_visible_files(&r.file_path, "cpp")?; + if self.cpp_alias_expansion(&raw, r, 0, &included)?.is_some_and(|alias| alias.pointer || (operator.trim() == "->" && alias.raw.as_deref().is_some_and(|ty| ty.ends_with("::iterator") || ty.ends_with("::const_iterator")))) { return Ok(None); } + let mut depth = 0usize; + let mut pointers = 0usize; + for ch in raw.chars() { + match ch { '<' => depth += 1, '>' => depth = depth.saturating_sub(1), '*' if depth == 0 => pointers += 1, _ => () } + } + let ty = match operator.trim() { + "." if pointers == 0 => raw.trim_end_matches('&').trim().to_string(), + "->" if pointers == 1 && !raw.contains('<') => raw.replace(['*', '&'], "").trim().to_string(), + "->" if pointers == 0 => { + let Some(hit) = re!(r"^std::(?:unique_ptr|shared_ptr)\s*<\s*([A-Za-z_]\w*(?:::[A-Za-z_]\w*)*)\s*>\s*&?$").captures(&raw) else { return Ok(Some(None)) }; + hit[1].to_string() + } + _ => return Ok(Some(None)), + }; + let primary = ty.split('<').next().unwrap_or(&ty).trim(); + if let Some(caller) = self.node_by_id(&r.from_node_id)? { if self.cpp_template_parameter(&caller, primary, r)? { return Ok(Some(None)); } } + if self.cpp_alias_expansion(primary, r, 0, &included)?.is_some_and(|alias| alias.pointer) { return Ok(None); } + let Some(owner) = self.cpp_type_owner(primary, r, 0, false)? else { return Ok(None) }; + // Preserve the existing guarded alias and specialization fallback on a miss. + Ok(self.match_bound_type_member(&owner.qualified_name, method, r)?.map(Some)) + } + pub(super) fn cpp_complex_call(&mut self, r: &ResolveRefIn) -> Res>> { if r.language != "cpp" || r.reference_kind != "calls" || !re!(r"^[A-Za-z_]\w*$").is_match(&r.reference_name) { return Ok(None); } let Some(source) = self.read_file(&r.file_path) else { return Ok(None) }; @@ -125,6 +195,15 @@ impl KernelResolver { } fn cpp_field_owner(&mut self, owner: &Arc, field: &str, r: &ResolveRefIn) -> Res>> { + let Some(ty) = self.cpp_field_type(owner, field, r)? else { return Ok(None) }; + let primary = ty.split('<').next().unwrap_or(&ty).trim(); + let mut site = r.clone().at(owner); site.from_node_id = owner.id.clone(); site.column = owner.start_column; + if self.cpp_template_parameter(owner, primary, &site)? { return Ok(None); } + let included = self.namespace_visible_files(&r.file_path, "cpp")?; + self.cpp_type_owner_visible(primary, &site, 0, false, &included) + } + + fn cpp_field_type(&mut self, owner: &Arc, field: &str, r: &ResolveRefIn) -> Res> { let Some(source) = self.read_file(&owner.file_path) else { return Ok(None) }; let mut site = r.clone().at(owner); site.from_node_id = owner.id.clone(); site.column = owner.start_column; let Some(tree) = self.cpp_field_tree(&source, &site) else { return Ok(None) }; @@ -142,15 +221,11 @@ impl KernelResolver { let mut cursor = declaration.walk(); for (index, child) in declaration.children(&mut cursor).enumerate() { if declaration.field_name_for_child(index as u32) == Some("declarator") && cpp_declarator_name(child, &source) == field { - types.push(source.text()[ty.start_byte()..ty.end_byte()].to_string()); + types.push(cpp_ast_declared_type(ty, child, &source)); } } } - let [ty] = types.as_slice() else { return Ok(None) }; - let primary = ty.split('<').next().unwrap_or(ty).trim(); - if self.cpp_template_parameter(owner, primary, &site)? { return Ok(None); } - let included = self.namespace_visible_files(&r.file_path, "cpp")?; - self.cpp_type_owner_visible(primary, &site, 0, false, &included) + Ok(match types.as_slice() { [only] => Some(only.clone()), _ => None }) } fn cpp_range_binding(&self, node: tree_sitter::Node, source: &SourceFile) -> bool { @@ -196,20 +271,76 @@ fn cpp_declarator_name<'a>(mut node: tree_sitter::Node, source: &'a SourceFile) fn cpp_binding_type(parent: tree_sitter::Node, at: tree_sitter::Node, name: &str, source: &SourceFile) -> Option { if parent.kind() == "compound_statement" { for declaration in named_children(parent).into_iter().rev().filter(|n| n.kind() == "declaration" && n.end_byte() <= at.start_byte()) { + // C++ parses a parenthesized initializer as a function declarator. + // Recognize only a standard smart pointer initialized from a known pointer value. + if let Some(declarator) = declaration.child_by_field_name("declarator").filter(|n| n.kind() == "function_declarator") { + if declarator.child_by_field_name("declarator").is_some_and(|n| cpp_declarator_name(n, source) == name) { + let ty = declaration.child_by_field_name("type")?; + let raw = &source.text()[ty.start_byte()..ty.end_byte()]; + if re!(r"^std::(?:unique_ptr|shared_ptr)\s*<\s*[A-Za-z_]\w*(?:::[A-Za-z_]\w*)*\s*>$").is_match(raw) { + let parameters = declarator.child_by_field_name("parameters").map(named_children).unwrap_or_default(); + if let [parameter] = parameters.as_slice() { + if parameter.child_by_field_name("declarator").is_none() { + if let Some(argument) = parameter.child_by_field_name("type").filter(|n| n.kind() == "type_identifier") { + let argument = &source.text()[argument.start_byte()..argument.end_byte()]; + let mut child = declaration; + while let Some(scope) = child.parent() { + if let Some(bound) = cpp_binding_type(scope, child, argument, source) { + if bound.ends_with('*') && !bound.contains('[') { return Some(raw.to_string()); } + break; + } + child = scope; + } + } + } + } + } + // This declaration shadows the receiver even when its type is unproven. + return Some(String::new()); + } + } let mut cursor = declaration.walk(); - if declaration.children(&mut cursor).enumerate().any(|(i, child)| declaration.field_name_for_child(i as u32) == Some("declarator") && cpp_declarator_name(child, source) == name) { - return Some(declaration.child_by_field_name("type").map(|ty| source.text()[ty.start_byte()..ty.end_byte()].to_string()).unwrap_or_default()); + if declaration.children(&mut cursor).enumerate().any(|(i, child)| declaration.field_name_for_child(i as u32) == Some("declarator") && cpp_declarator_binds(child, name, source)) { + let declarator = named_children(declaration).into_iter().find(|n| cpp_declarator_binds(*n, name, source))?; + return Some(declaration.child_by_field_name("type").map(|ty| cpp_ast_declared_type(ty, declarator, source)).unwrap_or_default()); } } } if matches!(parent.kind(), "lambda_expression" | "function_definition") { let mut queue = named_children(parent).into_iter().filter(|n| n.kind() != "compound_statement").collect::>(); while let Some(parameter) = queue.pop() { - if parameter.kind() == "parameter_declaration" && parameter.child_by_field_name("declarator").is_some_and(|n| cpp_declarator_name(n, source) == name) { - return Some(parameter.child_by_field_name("type").map(|ty| source.text()[ty.start_byte()..ty.end_byte()].to_string()).unwrap_or_default()); + if parameter.kind() == "parameter_declaration" && parameter.child_by_field_name("declarator").is_some_and(|n| cpp_declarator_binds(n, name, source)) { + let declarator = parameter.child_by_field_name("declarator")?; + return Some(parameter.child_by_field_name("type").map(|ty| cpp_ast_declared_type(ty, declarator, source)).unwrap_or_default()); } if parameter.kind() != "compound_statement" { queue.extend(named_children(parameter)); } } } None } + +fn cpp_ast_declared_type(ty: tree_sitter::Node, mut declarator: tree_sitter::Node, source: &SourceFile) -> String { + let mut raw = source.text()[ty.start_byte()..ty.end_byte()].to_string(); + loop { + match declarator.kind() { + "pointer_declarator" => raw.push('*'), + "reference_declarator" => raw.push('&'), + "array_declarator" | "structured_binding_declarator" => raw.push_str("[]"), + _ => (), + } + let inner = declarator.child_by_field_name("declarator").or_else(|| (declarator.kind() == "reference_declarator").then(|| declarator.named_child(0)).flatten()); + let Some(inner) = inner else { break }; declarator = inner; + } + raw +} + +fn cpp_declarator_binds(node: tree_sitter::Node, name: &str, source: &SourceFile) -> bool { + if cpp_declarator_name(node, source) == name { return true; } + if matches!(node.kind(), "init_declarator" | "reference_declarator" | "pointer_declarator" | "parenthesized_declarator") { + return node.child_by_field_name("declarator").or_else(|| node.named_child(0)).is_some_and(|n| cpp_declarator_binds(n, name, source)); + } + if node.kind() == "structured_binding_declarator" { + return named_children(node).iter().any(|n| &source.text()[n.start_byte()..n.end_byte()] == name); + } + false +} diff --git a/codegraph-kernel/src/resolve/cpp_types.rs b/codegraph-kernel/src/resolve/cpp_types.rs index 330ed52b3d..bbcccd7701 100644 --- a/codegraph-kernel/src/resolve/cpp_types.rs +++ b/codegraph-kernel/src/resolve/cpp_types.rs @@ -2,6 +2,87 @@ use super::*; impl KernelResolver { + pub(super) fn resolve_cpp_supertype(&mut self, r: &ResolveRefIn) -> Res { + let Some(derived) = self.node_by_id(&r.from_node_id)? else { return Ok(ResolveOutcome::unresolved()); }; + if !matches!(derived.kind.as_str(), "class" | "struct" | "union") { return Ok(ResolveOutcome::unresolved()); } + let mut written = r.reference_name.trim().to_string(); + if let Some(lines) = self.read_file(&r.file_path) { + let text = lines.iter().skip((r.line - 1).max(0) as usize).take(12).cloned().collect::>().join("\n"); + let at = super::names::js_unit_to_byte(&text, r.column.max(0) as usize).min(text.len()); + let mut depth = 0usize; + let mut end = text.len(); + for (i, ch) in text[at..].char_indices() { + match ch { + '<' | '(' => depth += 1, + '>' | ')' => depth = depth.saturating_sub(1), + ',' | '{' | ';' if depth == 0 => { end = at + i; break; } + _ => (), + } + } + let raw = text[at..end].trim(); + if super::cpp_aliases::type_segments(raw) == super::cpp_aliases::type_segments(&written) { written = raw.to_string(); } + } + let Some(parts) = super::cpp_aliases::type_segments(&written) else { return Ok(ResolveOutcome::unresolved()); }; + if self.cpp_template_parameter(&derived, &written, r)? { return Ok(ResolveOutcome::unresolved()); } + let files = match self.sorted_files() { Some(files) => files, None => Arc::new(self.table()?.files.iter().cloned().collect()) }; + let visible: HashSet<_> = files.iter().filter(|file| **file == r.file_path + || !re!(r"(?i)\.(?:c|cc|cpp|cxx|c\+\+|cu|metal)$").is_match(file)).cloned().collect(); + let mut target = self.cpp_type_owner_visible(&written, r, 0, false, &visible)?; + if target.is_none() { + if let Some(lines) = self.read_file(&r.file_path) { + let mut directives = Vec::new(); + if let Some(tree) = self.parsed_tree(&lines, r) { + let mut site = super::iteration::descendant_for_position(tree.root_node(), lines.text(), + ((r.line - 1).max(0) as usize, r.column.max(0) as usize + 1)); + let before = site.start_byte(); + loop { + if matches!(site.kind(), "translation_unit" | "declaration_list" | "field_declaration_list" | "compound_statement") { + let mut cursor = site.walk(); + let mut local: Vec<_> = site.named_children(&mut cursor) + .filter(|n| n.end_byte() <= before && matches!(n.kind(), "using_declaration" | "namespace_alias_definition")) + .map(|n| lines.text()[n.byte_range()].to_string()).collect(); + local.reverse(); + directives.extend(local); + } + let Some(parent) = site.parent() else { break; }; + site = parent; + } + } + for line in &directives { + let spelled = if let Some(hit) = re!(r"\bnamespace\s+([A-Za-z_]\w*)\s*=\s*((?:::)?[\w:]+)\s*;").captures(line) { + (hit[1] == parts[0]).then(|| format!("{}{}", &hit[2], written.trim_start_matches("::").strip_prefix(&parts[0]).unwrap_or(""))) + } else if !written.starts_with("::") { + if let Some(hit) = re!(r"\busing\s+namespace\s+((?:::)?[\w:]+)\s*;").captures(line) { + Some(format!("{}::{written}", &hit[1])) + } else if let Some(hit) = re!(r"\busing\s+((?:::)?[\w:]+)::([A-Za-z_]\w*)\s*;").captures(line) { + (hit[2] == parts[0]).then(|| format!("{}::{written}", &hit[1])) + } else { None } + } else { None }; + if let Some(spelled) = spelled { + target = self.cpp_type_owner_visible(&spelled, r, 0, false, &visible)?; + if target.is_some() { break; } + } + } + } + } + let mut confidence = 0.9; + let mut resolved_by = "qualified-name"; + if target.is_none() { + let tail = parts.join("::"); + let candidates: Vec<_> = self.nodes_by_name(parts.last().unwrap())?.iter() + .filter(|n| matches!(n.language.as_str(), "c" | "cpp") && matches!(n.kind.as_str(), "class" | "struct" | "union") + && visible.contains(&n.file_path) && (n.qualified_name == tail || n.qualified_name.ends_with(&format!("::{tail}")))) + .cloned().collect(); + let own: Vec<_> = candidates.iter().filter(|n| n.file_path == r.file_path).cloned().collect(); + let candidates = if own.is_empty() { candidates } else { own }; + if let [only] = candidates.as_slice() { target = Some(only.clone()); confidence = 0.7; resolved_by = "exact-match"; } + } + match target { + Some(node) => self.finish_pre_framework(r, KCand { node, confidence, resolved_by }), + None => Ok(ResolveOutcome::unresolved()), + } + } + pub(super) fn cpp_type_owner(&mut self, raw: &str, r: &ResolveRefIn, depth: u32, constructor: bool) -> Res>> { let included = self.namespace_visible_files(&r.file_path, "cpp")?; self.cpp_type_owner_visible(raw, r, depth, constructor, &included) @@ -11,20 +92,23 @@ impl KernelResolver { if depth > 4 { return Ok(None); } if let Some(alias) = self.cpp_alias_expansion(raw, r, depth, included)? { if constructor && (alias.pointer || alias.reference) { return Ok(None); } - return match alias.raw { Some(raw) => self.cpp_type_owner_visible(&raw, &alias.site, depth + 1, constructor, included), None => Ok(None) }; + let target = match alias.raw { Some(raw) => self.cpp_type_owner_visible(&raw, &alias.site, depth + 1, constructor, included)?, None => None }; + if target.is_some() || !is_inheritance_ref(&r.reference_kind) { return Ok(target); } + return Ok(self.node_by_id(&alias.site.from_node_id)?.filter(|n| n.kind == "type_alias")); } let normalized = super::cpp_aliases::type_segments(raw).map(|p| p.join("::")).unwrap_or_else(|| raw.to_string()); let ty = normalized.trim(); - let scopes = if raw.starts_with("::") { Vec::new() } else { + let mut scopes = if raw.starts_with("::") { Vec::new() } else { self.node_by_id(&r.from_node_id)?.map(|n| n.qualified_name.split("::").map(str::to_string).collect::>()).unwrap_or_default() }; + if is_inheritance_ref(&r.reference_kind) { scopes.pop(); } let mut names: Vec<_> = (1..=scopes.len()).rev().map(|i| format!("{}::{ty}", scopes[..i].join("::"))).collect(); names.push(ty.to_string()); let simple = ty.rsplit("::").next().unwrap_or(ty); for name in names { let named = self.nodes_by_qualified_name(&name)?; let mut visible = Vec::new(); - for node in named.iter().filter(|n| n.language == "cpp" && matches!(n.kind.as_str(), "class" | "struct" | "union" | "type_alias" | "enum")) { + for node in named.iter().filter(|n| matches!(n.language.as_str(), "c" | "cpp") && matches!(n.kind.as_str(), "class" | "struct" | "union" | "type_alias" | "enum")) { if included.contains(&node.file_path) { visible.push(node.clone()); } } if visible.is_empty() { continue; } @@ -37,7 +121,7 @@ impl KernelResolver { if ty.contains("::") { let named = self.nodes_by_name(simple)?; let mut visible = Vec::new(); - for node in named.iter().filter(|n| n.language == "cpp" && n.qualified_name == simple && matches!(n.kind.as_str(), "class" | "struct" | "union" | "type_alias")) { + for node in named.iter().filter(|n| matches!(n.language.as_str(), "c" | "cpp") && n.qualified_name == simple && matches!(n.kind.as_str(), "class" | "struct" | "union" | "type_alias")) { if !included.contains(&node.file_path) { continue; } let frames = self.namespace_frames(&node.file_path)?; let mut frames: Vec<_> = frames.into_iter().filter(|(start, end, _)| *start <= node.start_line && *end >= node.start_line).collect(); diff --git a/codegraph-kernel/src/resolve/fields.rs b/codegraph-kernel/src/resolve/fields.rs index 57b0e8e8c8..4a0b723bfa 100644 --- a/codegraph-kernel/src/resolve/fields.rs +++ b/codegraph-kernel/src/resolve/fields.rs @@ -210,6 +210,33 @@ impl KernelResolver { let text = lines.iter().skip((owner.start_line - 1).max(0) as usize) .take((owner.end_line - owner.start_line + 1).max(0) as usize) .map(|line| strip_line_comments(line)).collect::>().join("\n"); + if owner.kind == "type_alias" { + // Alias method sets follow their targets; defined types do not. + let site = ResolveRefIn { from_node_id: owner.id.clone(), reference_name: owner.name.clone(), reference_kind: "references".to_string(), line: owner.start_line, column: owner.start_column, file_path: owner.file_path.clone(), language: "go".to_string(), row_id: None, candidates: None, failure_reason: None }; + if let Some(tree) = self.parsed_tree(&lines, &site) { + let mut declaration = super::iteration::descendant_for_position( + tree.root_node(), lines.text(), + ((owner.start_line - 1).max(0) as usize, owner.start_column.max(0) as usize + 1), + ); + while !matches!(declaration.kind(), "type_alias" | "type_spec") { + let Some(parent) = declaration.parent() else { break; }; + declaration = parent; + } + let is_alias = declaration.kind() == "type_alias" || { + let mut cursor = declaration.walk(); + let found = declaration.children(&mut cursor).any(|n| n.kind() == "ERROR" + && lines.text()[n.start_byte()..n.end_byte()].trim() == "="); + found + }; + if is_alias { + if let Some(ty) = declaration.child_by_field_name("type") { + let raw = &lines.text()[ty.start_byte()..ty.end_byte()]; + if let Some(base) = self.go_type_package(raw, &owner.file_path)? { out.push(base); } + } + continue; + } + } + } let Some(body) = text.split_once('{').map(|(_, b)| b.rsplit_once('}').map_or(b, |(b, _)| b)) else { continue }; // Hide nested anonymous struct/interface bodies while preserving declaration separators. let mut depth = 0usize; diff --git a/codegraph-kernel/src/resolve/method_call.rs b/codegraph-kernel/src/resolve/method_call.rs index 6f7b828122..4f0d6b0b3a 100644 --- a/codegraph-kernel/src/resolve/method_call.rs +++ b/codegraph-kernel/src/resolve/method_call.rs @@ -658,6 +658,7 @@ impl KernelResolver { McShape::Done(res) => return Ok(res), }; + if r.language == "cpp" && self.cpp_external_receiver(&object_or_class, r)? { return Ok(None); } if matches!(r.language.as_str(), "java" | "kotlin") { if let Some(hit) = self.enum_constant_call(&object_or_class, &method_name, r)? { return Ok(Some(hit)); } } diff --git a/codegraph-kernel/src/resolve/mod.rs b/codegraph-kernel/src/resolve/mod.rs index e5572e17f5..cf01d24e81 100644 --- a/codegraph-kernel/src/resolve/mod.rs +++ b/codegraph-kernel/src/resolve/mod.rs @@ -342,6 +342,7 @@ pub(super) struct SourceFile { java_static_imports: OnceCell, php_file_scope: OnceCell, rust_code_lines: OnceCell>, + cpp_code_lines: OnceCell>, python_file: OnceCell, python_field_file: OnceCell, cpp_field_tree: OnceCell>>, @@ -360,6 +361,7 @@ impl SourceFile { java_static_imports: OnceCell::new(), php_file_scope: OnceCell::new(), rust_code_lines: OnceCell::new(), + cpp_code_lines: OnceCell::new(), python_file: OnceCell::new(), python_field_file: OnceCell::new(), cpp_field_tree: OnceCell::new(), @@ -414,6 +416,11 @@ impl SourceFile { .get_or_init(|| fields::mask_rust_code(self.text()).split('\n').map(str::to_string).collect()) } + /// C++ lines with raw literals and comments masked, shared by receiver lookups. + pub(super) fn cpp_code_lines(&self) -> &[String] { + self.cpp_code_lines.get_or_init(|| awaited::strip_ts_comments(&cpp::mask_cpp_raw_strings(self.text())).split('\n').map(str::to_string).collect()) + } + /// The file's Python statements and the scopes they run in /// (bound::PyFile). pub(super) fn python_file(&self) -> &bound::PyFile { @@ -1248,6 +1255,21 @@ impl Drop for KernelResolver { mod tests { use super::*; + #[test] + fn cpp_receiver_lines_exclude_raw_string_declarations() { + for prefix in ["R", "u8R", "uR", "UR", "LR"] { + for delimiter in ["", "payload"] { + let text = format!("Slice *ptr;\nauto text = {prefix}\"{delimiter}(\nWidget *ptr;\n){delimiter}\";\nptr->size();"); + let source = SourceFile::new(text.lines().map(str::to_string).collect()); + let code = source.cpp_code_lines(); + assert_eq!(code.len(), 5); + assert_eq!(code[0], "Slice *ptr;"); + assert!(!code[2].contains("Widget"), "raw literal leaked into declarations: {prefix}/{delimiter}"); + assert_eq!(code[4], "ptr->size();"); + } + } + } + #[test] fn utf16_len_counts_units_from_bytes() { for s in ["", "abc", "é", "€", "😀", "a😀é€x"] { diff --git a/codegraph-kernel/src/resolve/names.rs b/codegraph-kernel/src/resolve/names.rs index 0bdb50ab5c..224337607c 100644 --- a/codegraph-kernel/src/resolve/names.rs +++ b/codegraph-kernel/src/resolve/names.rs @@ -217,6 +217,20 @@ impl KernelResolver { Ok(true) } + fn cpp_test_file_in_translation_unit(&mut self, candidate: &KNode, r: &ResolveRefIn) -> Res { + if !matches!(candidate.language.as_str(), "c" | "cpp") || !matches!(r.language.as_str(), "c" | "cpp") { return Ok(false); } + let visible = self.namespace_visible_files(&r.file_path, &r.language)?; + if visible.contains(&candidate.file_path) { return Ok(true); } + // A test-named header can declare methods implemented in its same-stem + // source. Including an ordinary header cannot expose its test suite. + let stem = re!(r"(?i)\.(?:c|cc|cpp|cxx|c\+\+)$").replace(&candidate.file_path, "").to_string(); + if stem == candidate.file_path { return Ok(false); } + Ok(["h", "hh", "hpp", "hxx"].iter().any(|ext| { + let header = format!("{stem}.{ext}"); + super::resolver_upstream::test_suite_path(&header) && visible.contains(&header) + })) + } + /// isRustTraitImplMethod (name-matcher.ts) — scan upward for the nearest /// `impl` header; `impl Trait for` wins, a top-level item ends the scan. pub(super) fn is_rust_trait_impl_method(&mut self, candidate: &KNode) -> Res { @@ -291,7 +305,8 @@ impl KernelResolver { return Ok(true); } if self.is_minified_script(&candidate.file_path) - || (super::resolver_upstream::test_suite_path(&candidate.file_path) && !is_test_path(&r.file_path)) + || (super::resolver_upstream::test_suite_path(&candidate.file_path) && !is_test_path(&r.file_path) + && !self.cpp_test_file_in_translation_unit(candidate, r)?) || self.sfc_private(candidate) || !self.php_class_visible(candidate, r)? || !self.language_type_visible(candidate, r)? { return Ok(false); } let lang = candidate.language.as_str(); @@ -357,7 +372,12 @@ impl KernelResolver { if r.language != "go" { return Ok(false); } - self.is_receiver_less_call(r) + if self.is_receiver_less_call(r)? { return Ok(true); } + let Some(lines) = self.read_file(&r.file_path) else { return Ok(false); }; + let Some(line) = lines.get((r.line - 1).max(0) as usize) else { return Ok(false); }; + let at = super::names::js_unit_to_byte(line, r.column.max(0) as usize).min(line.len()); + let conversion = Self::cached_regex(&format!(r"^\(\s*\*?\s*{}\s*\)\s*\(", regex::escape(&r.reference_name)))?; + Ok(conversion.is_match(&line[at..])) } fn is_bare_r_call(&mut self, r: &ResolveRefIn) -> bool { @@ -1083,7 +1103,65 @@ impl KernelResolver { Ok(Some(KCand { node: candidates[0].clone(), confidence: 0.85, resolved_by: "exact-match" })) } + fn go_collapsed_chain_call(&mut self, r: &ResolveRefIn) -> Option> { + if r.language != "go" || r.reference_kind != "calls" || r.reference_name.contains('.') { return None; } + let file = self.read_file(&r.file_path)?; + let tree = self.parsed_tree(&file, r)?; + let mut node = super::iteration::descendant_for_position(tree.root_node(), file.text(), + ((r.line - 1).max(0) as usize, r.column.max(0) as usize + 1)); + loop { + if node.kind() == "call_expression" { + if let Some(function) = node.child_by_field_name("function") { + if function.kind() == "selector_expression" { + if let (Some(field), Some(receiver)) = (function.child_by_field_name("field"), function.child_by_field_name("operand")) { + if file.text()[field.byte_range()] == r.reference_name + && receiver.kind() == "call_expression" { + let mut inner = receiver; + let mut chain = Vec::new(); + loop { + let Some(callee) = inner.child_by_field_name("function") else { return Some(Vec::new()); }; + if callee.kind() != "selector_expression" { return Some(Vec::new()); } + let (Some(member), Some(operand)) = (callee.child_by_field_name("field"), callee.child_by_field_name("operand")) else { return Some(Vec::new()); }; + chain.push(file.text()[member.byte_range()].to_string()); + if operand.kind() == "identifier" { + chain.push(file.text()[operand.byte_range()].to_string()); + chain.reverse(); + return Some(chain); + } + if operand.kind() != "call_expression" { return Some(Vec::new()); } + inner = operand; + } + } + } + } + } + } + let parent = node.parent()?; + if matches!(parent.kind(), "argument_list" | "block" | "source_file") { return None; } + node = parent; + } + } + pub(super) fn match_reference_bare(&mut self, r: &ResolveRefIn) -> Res> { + // A collapsed Go expression call has no proven receiver. Name-only + // fallback must not borrow a method from an unrelated type. + if let Some(chain) = self.go_collapsed_chain_call(r) { + // Every fluent step must have a declared result in its proven + // receiver's package. Unproven steps cannot borrow a name match. + if let [receiver, method, rest @ ..] = chain.as_slice() { + let mut inner = match self.infer_local_receiver_type(receiver, r, true)? { + Some(raw) => self.go_method_on_declared_type(&raw, &r.file_path, method, r)?, + None => self.match_go_factory_receiver(receiver, method, r)?, + }; + for method in rest.iter().chain(std::iter::once(&r.reference_name)) { + let Some(proven) = inner else { return Ok(None); }; + let Some(result) = proven.node.return_type.as_deref() else { return Ok(None); }; + inner = self.go_method_on_declared_type(result, &proven.node.file_path, method, r)?; + } + return Ok(inner); + } + return Ok(None); + } if let Some(node) = self.js_typed_destructured_member(r)? { return Ok(Some(KCand { node, confidence: 0.9, resolved_by: "exact-match" })); } diff --git a/codegraph-kernel/src/resolve/pipeline.rs b/codegraph-kernel/src/resolve/pipeline.rs index b54cd92be9..3d37bfd397 100644 --- a/codegraph-kernel/src/resolve/pipeline.rs +++ b/codegraph-kernel/src/resolve/pipeline.rs @@ -243,6 +243,12 @@ impl KernelResolver { } pub(super) fn resolve_nonbare_ref(&mut self, r: &ResolveRefIn) -> Res { + // Vue route imports name a rendered component, never the module's file node. + if r.reference_kind == "calls" && r.from_node_id.starts_with("route:") + && r.from_node_id.ends_with(":vue") && r.reference_name.starts_with("import:") + && r.reference_name.contains('#') && self.framework_claims(&r.reference_name) { + return Ok(ResolveOutcome::no_candidates()); + } if super::lang_scope::is_dart_member_read(r) { let Some(candidate) = self.match_dart_member_read(r)? else { return Ok(ResolveOutcome::unresolved()); @@ -591,6 +597,33 @@ if super::lang_scope::is_dart_member_read(r) { return Ok(None); } let mut cand = cand; + if r.language == "go" && cand.node.language == "go" { + let is_type = matches!(cand.node.kind.as_str(), "struct" | "interface" | "type_alias"); + if r.reference_kind == "calls" && is_type && !r.reference_name.contains('.') && !self.is_bare_go_call(r)? { + // An expression's member call cannot name a bare type. + return Ok(None); + } + if matches!(r.reference_kind.as_str(), "references" | "instantiates") + && !self.node_by_id(&r.from_node_id)?.is_some_and(|n| n.kind == "route") { + let name = r.reference_name.rsplit('.').next().unwrap_or(&r.reference_name); + let package = if let Some((root, _)) = r.reference_name.rsplit_once('.') { + self.go_imported_package_dir(&r.file_path, root)? + } else if let Some(imp) = self.go_ref_qualifier(r)? { + self.go_package_dir(&imp.source, &r.file_path) + } else if self.go_written_qualifier(r)?.is_none() { + Some(pos_dirname(&r.file_path).to_string()) + } else { None }; + if let Some(package) = package { + let types: Vec<_> = self.nodes_by_name(name)?.iter() + .filter(|n| n.language == "go" && matches!(n.kind.as_str(), "struct" | "interface" | "type_alias") + && pos_dirname(&n.file_path) == package) + .cloned().collect(); + if let Some(target) = prefer_call_site_file(types, &r.file_path).first() { + cand.node = target.clone(); + } else if !is_type { return Ok(None); } + } else if !is_type { return Ok(None); } + } + } self.cap_chain_confidence(&mut cand, r)?; if !is_inheritance_ref(&r.reference_kind) { return Ok(Some(cand)); @@ -743,6 +776,17 @@ if super::lang_scope::is_dart_member_read(r) { /// The per-ref pipeline: the Rust `::`-path arm, then one route. pub(super) fn resolve_ref(&mut self, r: &ResolveRefIn) -> Res { + if let Some(result) = self.cpp_plain_call(r)? { + return match result { Some(candidate) => self.finish_pre_framework(r, candidate), None => Ok(self.refused()) }; + } + if r.language == "cpp" && r.reference_kind == "calls" { + if let McShape::Parsed { receiver, .. } = self.method_call_shape(r)? { + if self.cpp_external_receiver(&receiver, r)? { return Ok(ResolveOutcome::unresolved()); } + } + } + if matches!(r.language.as_str(), "c" | "cpp") && is_inheritance_ref(&r.reference_kind) { + return self.resolve_cpp_supertype(r); + } if let Some(outcome) = self.resolve_shopify_file(r)? { return Ok(outcome); } if matches!(r.language.as_str(), "lua" | "luau") && r.reference_kind == "imports" { return match self.resolve_lua_require(r)? { Some(c) => self.finish_pre_framework(r, c), None => Ok(ResolveOutcome::unresolved()) }; diff --git a/codegraph-kernel/src/resolve/receivers.rs b/codegraph-kernel/src/resolve/receivers.rs index c42a21c7d2..f90269d6d3 100644 --- a/codegraph-kernel/src/resolve/receivers.rs +++ b/codegraph-kernel/src/resolve/receivers.rs @@ -637,6 +637,8 @@ impl KernelResolver { /// terminator. The JS lookahead `(?=[;=,)\[{(]|$)` is post-checked on the /// remainder: the greedy `\s*` tail can't shrink into a passing position. pub(super) fn cpp_declarator_match(&mut self, line: &str, escaped_receiver: &str) -> Res> { + let code = super::awaited::strip_ts_comments(line); + let line = code.as_str(); let re = shared_regex(&format!( r"([A-Za-z_][A-Za-z0-9_:]*(?:\s*<[^;=(){{}}]+>)?(?:\s*[*&]+)?)\s*(?-u:\b){}(?-u:\b)\s*", escaped_receiver diff --git a/codegraph-kernel/src/resolve/resolver_upstream.rs b/codegraph-kernel/src/resolve/resolver_upstream.rs index a5b9280b70..adf054bd07 100644 --- a/codegraph-kernel/src/resolve/resolver_upstream.rs +++ b/codegraph-kernel/src/resolve/resolver_upstream.rs @@ -530,7 +530,7 @@ impl KernelResolver { let at = super::names::js_unit_to_byte(line, r.column.max(0) as usize) .min(line.len()); let root = if line[at..].starts_with(name) { - re!(r"(?:^|[^\w.])([A-Za-z_]\w*)\.$") + re!(r"(?:^|[^\w.]|\.{3})([A-Za-z_]\w*)\.$") .captures(&line[..at]).map(|hit| hit[1].to_string()) } else { let bare = Self::cached_regex(&format!( @@ -538,7 +538,7 @@ impl KernelResolver { ))?; if bare.is_match(line) { return Ok(None); } let qualified = Self::cached_regex(&format!( - r"(?:^|[^\w.])([A-Za-z_]\w*)\.{}\b", regex::escape(name) + r"(?:^|[^\w.]|\.{{3}})([A-Za-z_]\w*)\.{}\b", regex::escape(name) ))?; qualified.captures(line).map(|hit| hit[1].to_string()) }; diff --git a/codegraph-kernel/src/resolve/tables.rs b/codegraph-kernel/src/resolve/tables.rs index 2cc5deed86..42901cc5ac 100644 --- a/codegraph-kernel/src/resolve/tables.rs +++ b/codegraph-kernel/src/resolve/tables.rs @@ -383,7 +383,7 @@ pub(super) fn framework_claims_reference(framework: &str, name: &str) -> bool { // react-native-bridge's claimsReference returns false — JS-visible // method names reach the resolver through the name-exists arm. "react-native-bridge" => false, - "react" => name.starts_with("lazy-import:"), + "react" => name.starts_with("lazy-import:") || name.starts_with("layout:"), "react-router" => rr_nav_re().is_match(name) || name.starts_with("react-router-module:"), "rails" => rails_claim_re().is_match(name), "solid-start" => name.starts_with("solid-start-target:"), diff --git a/codegraph-kernel/src/textutil.rs b/codegraph-kernel/src/textutil.rs index dc5546fc2a..83a9d9334e 100644 --- a/codegraph-kernel/src/textutil.rs +++ b/codegraph-kernel/src/textutil.rs @@ -197,6 +197,12 @@ impl Cols { self.starts.len() as u32 } + /// The 1-based line and UTF-16 column at a byte offset. + pub fn position(&self, src: &str, byte_pos: usize) -> (u32, u32) { + let row = self.starts.partition_point(|&start| start <= byte_pos).saturating_sub(1); + (row as u32 + 1, self.col(src, row, byte_pos)) + } + /// Column (UTF-16 units) of `byte_pos` on line `row`. pub fn col(&self, src: &str, row: usize, byte_pos: usize) -> u32 { let ls = self.starts.get(row).copied().unwrap_or(0); @@ -405,6 +411,7 @@ mod tests { continue; } assert_eq!(cols.col(src, row, byte), col16(src, &starts, row, byte), "row {row} byte {byte}"); + assert_eq!(cols.position(src, byte), (row as u32 + 1, col16(src, &starts, row, byte))); } } // ASCII fast path agrees too. @@ -412,6 +419,8 @@ mod tests { let ac = Cols::new(a); let astarts = line_starts(a); assert_eq!(ac.col(a, 1, 8), col16(a, &astarts, 1, 8)); + assert_eq!(ac.position(a, 8), (2, 2)); + assert_eq!(ac.position(a, a.len()), (3, 0)); } #[test] diff --git a/codegraph-kernel/src/tsjs/mod.rs b/codegraph-kernel/src/tsjs/mod.rs index fec52f56ef..5dca642698 100644 --- a/codegraph-kernel/src/tsjs/mod.rs +++ b/codegraph-kernel/src/tsjs/mod.rs @@ -830,7 +830,7 @@ impl<'t> Walker<'t> { } cur = p.parent(); } - false + is_ambient_export(node) } fn has_keyword_child(&self, node: Node, kw: &str) -> bool { @@ -923,3 +923,44 @@ fn body_of(node: Node) -> Option { resolve_field_body(node).or_else(|| node.child_by_field_name("body")) } +/// isAmbientExport (languages/typescript.ts): declared in a `declare module +/// 'x' { … }` or `declare global { … }` body, which TypeScript exports without +/// an `export` keyword unless the body holds an export declaration of its own. +fn is_ambient_export(node: Node) -> bool { + let mut cur = node.parent(); + while let Some(p) = cur { + if p.kind() == "statement_block" && has_export_declaration(p) { + return false; + } + if p.kind() == "ambient_declaration" { + // `declare global` is the keyword and a bare block, with no field names. + for i in 0..p.child_count() { + if p.child(i).is_some_and(|c| c.kind() == "global") { + return true; + } + } + if let Some(declared) = p.named_child(0) { + if declared.kind() == "module" + && declared.child_by_field_name("name").is_some_and(|n| n.kind() == "string") + { + return true; + } + } + } + cur = p.parent(); + } + false +} + +/// hasExportDeclaration: a statement that exports by name or by assignment +/// (`export {…}`, `export =`, `export default x`), not by declaring. +fn has_export_declaration(block: Node) -> bool { + for i in 0..block.named_child_count() { + if let Some(statement) = block.named_child(i) { + if statement.kind() == "export_statement" && statement.child_by_field_name("declaration").is_none() { + return true; + } + } + } + false +} diff --git a/codegraph-kernel/src/walker.rs b/codegraph-kernel/src/walker.rs index b6f9ec8725..32fef28803 100644 --- a/codegraph-kernel/src/walker.rs +++ b/codegraph-kernel/src/walker.rs @@ -4,6 +4,7 @@ use tree_sitter::Node; /// One scope-stack entry: the row of the node that opened it (0 = the file /// node), its kind, and its name for qualified-name building. +#[derive(Clone)] pub(crate) struct Scope { pub row: u32, pub kind: &'static str, diff --git a/docs/benchmarks/index-drift-cg33.md b/docs/benchmarks/index-drift-cg33.md index 58fdce46a3..2778beaf08 100644 --- a/docs/benchmarks/index-drift-cg33.md +++ b/docs/benchmarks/index-drift-cg33.md @@ -125,6 +125,69 @@ named `push`, or a Rust method named `join`. **The full rebuild is the wrong one here** — converging would mean teaching sync to manufacture thousands of wrong edges. Left as is, deliberately. +### Navigation waits on a route, not a name (2026-10) + +A router's navigation call — `history.push('/login')`, `navigate('/login')`, +`router.push('/x')`, `goto('/x')` — names its route by path and its reference +by the router's method, so a route that changes in another file is invisible +to every pass above: + +- **A call parked while its route was missing.** The #1240 retry looks failed + refs up by the names the synced files define, and a call's name tail is + `push`, never a path. On proshop_mern, deleting and restoring + `` left six `history.push('/login')` calls failed and + their six `navigates` edges missing. +- **A call that bound elsewhere meanwhile.** It resolved, so nothing revisits + it: to a catch-all screen (Obytes' Expo starter: `router.replace('/login')` + stayed on `[...messing].tsx` after `login.tsx` came back), to a parameter + route (`/users/new` on `/users/:id`), or to the other arm of a conditional + (sveltekit-realworld's `redirect` lost its `/login` arm for good). +- **A route renamed in place.** `runPostExtract` renames a route whose path is + built from a constant another file holds, an Angular route under its mount, + or a React Router table route, and keeps its node, so the calls bound to it + keep pointing at a path it no longer has. `definitionDelta` is read before + that pass. +- **A link in markup.** ``, `routerLink` and the like are a + synthesizer's edges, and a routes file that matches none of the synthesis + triggers (an Angular `Routes` array, Ghostfolio's `home-page.routes.ts`) + does not send a sync to the synthesizers. + +Sync now reads every route node at its first file change, only when a router +that resolves navigation calls is detected, and again after `runPostExtract`. +When a route is on one side only (`changedRoutes`), the calls that routers' +`navigation` hooks say it can answer go back to the pending set for the +orphan sweep: the failed ones, found by their method tails through +`idx_unresolved_failed_tail`, and the resolved ones, whose `navigates` edges +are turned back into the references that made them, as the rebind does. A +call counts when the router's `claimsReference` accepts its whole name and its +file is in an app whose table holds the route, and a name more such calls +share than the same 500 ceiling is skipped. The sync also refreshes synthesis. +A sync that changes no route reads the routes twice and stops there. + +Measured by deleting a route (or renaming it), syncing, restoring it and +syncing again, with both states diffed against a fresh index of the same tree. +On `main` each of ten scenarios drifted in one of the two states; with the fix +all twenty match: proshop_mern (``), next-saas-starter +(the `/sign-in` and `/pricing` pages), create-t3-turbo (`/`), vue-realworld +(`/login` in the router config), sveltekit-realworld (`login/+page.svelte`), +angular-realworld (`login` in `app.routes.ts`), Obytes' Expo starter +(`app/login.tsx`), Ghostfolio (`home-page.routes.ts`), and bulletproof-react +with its `/auth/login` path constant renamed and back — the one that drifts +while the route is renamed rather than after it returns. CleanArchitecture's +React client, whose routes are a table `App.jsx` maps into ``, drifted +when `App.jsx` stopped mapping it and started again: its two +`navigate('/login')` calls stayed failed, though `AppRoutes.jsx` itself never +changed. With the fix every step of that replay matches. A sync that changes +no route spends 0–3 ms on the two reads (proshop's 49 routes, Ghostfolio's +203); a route change on Ghostfolio spends 12–17 ms putting 40–50 calls back. + +Known limits, deliberate: a call bound by name to a method while its route +was missing (a `redirect` that matched a project method called `redirect`) is +not revisited, since only `navigates` edges are looked at; a project's own +wrapper (Expo's `safePush`) is not looked up, since its tail is its own name +and the failed-tail index cannot find a suffix; and a Vue route whose `name:` +alone changes changes no route node. + ### `codegraph status` — decided: no drift metric The issue asked whether `status` should surface divergence. Decision: **no**. diff --git a/docs/design/ccpp-kernel-port-checklist.md b/docs/design/ccpp-kernel-port-checklist.md index 6f664d73cb..a9c3648286 100644 --- a/docs/design/ccpp-kernel-port-checklist.md +++ b/docs/design/ccpp-kernel-port-checklist.md @@ -187,6 +187,7 @@ function_declarator → null, 12-hop guard). | Line | Mechanism | Must-mirror details | |---|---|---| | 962 | cpp namespace prefix stack (#1291) | named `namespace_definition` pushes its name (C++17 `a::b` as written) onto the QN prefix while walking children; anonymous falls through bare | +| — | cpp brace scopes (erroring trees only) | when the tree has errors, every visitNode takes its namespace prefix from the source's braces, and at declaration level (only class scopes above the file node) its enclosing classes: `languages/cpp-brace-scopes.ts` ↔ `ccpp/brace_scopes.rs`. A class-like node's end is its body's `}`; a class-like `type` of a namespace-level `declaration` is walked as a class. Native brace-scope recovery is canonical for erroring C++ trees and has no opt-in gate | | 2795 | C file-scope variables | only when NO function ancestor; iterate declarators; accept ONLY init_declarator / pointer_declarator / array_declarator — a BARE identifier declarator is a macro-prototype misparse, skip (loses uninit scalars by design); name via cDeclaratorIdentifier; signature `= `; kind constant/variable via isConst | | 4313 | explicit operator calls (#1247) | callee = `function` field + ERROR-wrapped `operator_name` sibling; compact symbolic spacing (`operator *`→`operator*`, word forms keep space); receiver `->`→`.`; DROP unless receiver is `this` (bare name) or simple identifier/member chain (silent miss over wrong edge) | | ~4340 | field_expression method calls | `recv.method`/`ptr->method` → `recv.method`; SKIP_RECEIVERS {self,this,cls,super} → bare name; LITERAL receiver → emit nothing (#1230) | diff --git a/docs/design/codegraph-ui-design-spec.md b/docs/design/codegraph-ui-design-spec.md index e07b72ae27..1c41a0d85c 100644 --- a/docs/design/codegraph-ui-design-spec.md +++ b/docs/design/codegraph-ui-design-spec.md @@ -355,7 +355,7 @@ entry-points panel. **The caveat is part of the screen, not a note on it.** A persistent 11.5px `--ink-3` line sits above the rows, between two hairline rules, and never collapses or dismisses: *"No static reference in the index — dynamic use is possible."* Under the list, -every reason a candidate was left off is printed with its count ("1 677 in test files", "378 exported, or declared in a header", +every reason a candidate was left off is printed with its count ("1 677 in test files", "378 exported, declared in a header, or merged into a library or global type", "40 overriding a member declared further up"), preceded by the scale — *"2 494 symbols in this index carry no incoming reference at all; 2 474 of them were left off this list."* Twenty rows drawn from twenty candidates and twenty drawn from two and a half thousand are different screens and only that sentence tells them apart. @@ -378,8 +378,10 @@ modules are not dimmed — a module with one `.pb.go` in it is still one somebod **What the list refuses to claim** is the whole design. Behind it, `src/graph/dead-code.ts` starts from "no incoming edge other than `contains`" and subtracts every candidate there is any reason to believe something reaches: exported symbols and -header declarations, test and generated files, abstract and interface members, anything carrying a `decorates` edge, overrides -of an ancestor's member, names the language calls by itself, vendored directories, files nothing in the index reaches (those are +header declarations, test and generated files, abstract and interface members, anything carrying a decorator (a `decorates` +edge, or a decorator from outside the index the resolver could not follow), overrides of an ancestor's member, a TypeScript +hook its framework calls by name on a decorated class that skips the `implements` clause (which names are hooks is read off the +classes that do write the interface out), names the language calls by itself, vendored directories, files nothing in the index reaches (those are islands — the Map's job, not this list's), names the resolver failed to resolve somewhere, names shared with a symbol that IS referenced, and — the only rule that reads a file — names written more than once in a file that can reach them. diff --git a/docs/design/framework-coverage.md b/docs/design/framework-coverage.md index c21614dfbe..2d01b92aaf 100644 --- a/docs/design/framework-coverage.md +++ b/docs/design/framework-coverage.md @@ -1,6 +1,6 @@ # Framework & language coverage — what is done, what is left -**Last verified: 2026-08-29** (Angular row: 2026-10-06) against the build at that date. Re-verify with the +**Last verified: 2026-08-29** (Angular row: 2026-10-06; React Router row: 2026-10-07) against the build at that date. Re-verify with the queries in [Checking this file is still true](#checking-this-file-is-still-true) before trusting a row; this is a snapshot, not a live view. @@ -38,7 +38,7 @@ arms disagree unresolved rather than guessed. |---|---|---|---|---| | Expo Router | `frameworks/expo-router.ts` | `expo-router-synthesizer.ts` | `expo-router.test.ts`, `monorepo-app-frameworks.test.ts`, `monorepo-app-frameworks-sync.test.ts` | upstream: evanbacon.dev (`+api` endpoints), react-native-true-sheet (nearest manifest decides the app) | | Next.js | `frameworks/nextjs.ts` | `next-router-synthesizer.ts` | `nextjs.test.ts` | next-saas-starter | -| React Router / Remix | `frameworks/react-router.ts` | `react-router-synthesizer.ts` | `react-router.test.ts`, `react-router-framework.test.ts`, `remix-routes.test.ts` | proshop (44 edges), proshop-v2 (28), react-redux-realworld (22), react-boilerplate (`styled(Link)`), takenote (v5 ``); pinned official framework config and flat filenames; bulletproof-react: nested children, lazy routes and paths.x.path constants through per-app aliases; paths.x.getHref() links and navigate calls read literal or template destinations; fork recheck: 11 navigates on bulletproof-react `9506629` (2026-10-01), all destinations checked against config and route registrations | +| React Router / Remix | `frameworks/react-router.ts` | `react-router-synthesizer.ts` | `react-router.test.ts`, `react-router-framework.test.ts`, `remix-routes.test.ts` | proshop (44 edges), proshop-v2 (28), react-redux-realworld (22), react-boilerplate (`styled(Link)`), takenote (v5 ``); pinned official framework config and flat filenames; bulletproof-react: nested children, lazy routes and paths.x.path constants through per-app aliases; paths.x.getHref() links and navigate calls read literal or template destinations; upstream route-table and JSX index/layout regressions cover imported route tables, nested layouts and index pages; fork recheck: 11 navigates on bulletproof-react `9506629` (2026-10-01), all destinations checked against config and route registrations | | TanStack Router / Start | `frameworks/tanstack-router.ts` | `tanstack-router-synthesizer.ts` | `tanstack-router.test.ts`, `tanstack-start.test.ts` | TanStack examples, fastapi-template frontend; pinned Start server-handler syntax | | Vue Router / Nuxt | `frameworks/vue-router.ts` (Nuxt file routes: `nuxtResolver` in `frameworks/vue.ts`) | `vue-router-synthesizer.ts` | `vue-router.test.ts` | vue-realworld (23 edges); vue-element-admin (62 routes), vue-admin-template (14), vben (192), halo console (34) — named tables, module files, `children` + layouts; Nuxt: mealie, elk, nuxt/movies | | SvelteKit | `frameworks/sveltekit-router.ts` | `sveltekit-synthesizer.ts` | `sveltekit-router.test.ts`, `sveltekit-route-names.test.ts` | sveltekit-realworld (31 edges); shadcn-svelte and skeleton (`(group)` layouts: 13 and 23 edges), svelte.dev (74), kit's test apps (47) | @@ -395,6 +395,70 @@ Each of these cost real debugging time; they are not hypothetical. template-bound handler there was listed as dead code. A new detector that gates on a dependency should ask `dependsOn`, not read a `package.json` itself, so it sees the same manifests every other detector does. +13. **A route table can be named only by another file.** The ASP.NET Core + React template keeps its routes in `AppRoutes.js`, a file that never names + the router, and renders them from `App.js` with `AppRoutes.map(({ element, + ...rest }) => )`; codedthemes' admin + kits put one route object per file and list them in + `createBrowserRouter([MainRoutes, LoginRoutes])`. Read on its own, such a + file cannot be told from a menu's `{ path, element }` list, and per-file + extraction cannot see the file that hands it over, nor run again when that + file changes. So React keeps those routes in the cross-file pass + (`FrameworkResolver.crossFileNodes`, reconciled in `runPostExtract` on every + index and sync, removals included): it starts from the files that import + React Router, takes only a `useRoutes` / `create*Router` argument or a + `.map` whose callback renders a `` from the item's own fields + (`{...route}`, `{...rest}`, `path={route.path}` — never + `path={r.layout + r.path}`), follows the import to the table and the tables + it names in turn, and owns its routes by id (`…:table:`). One limit + it shares with every router: a table rendered by a component that is + itself mounted under another file's `` reads as if + at the root. A sync that changes such a route resolves the calls it can + answer again (16), as for any other route. +14. **An index route is at its parent's address, so it needs that address + written down.** `} />` inside `}>` is the page at `/`, and Layout is the + layout of every route nested in it (trap 2: emitting Layout as a second + `/` page drew the app's chrome as the home screen and its header's links + from `/` alone). A `` with no element only groups its children + and yields its address the same way. But a `` inside a component + is mounted wherever another file's `}>` puts it: read as `/`, crwn-clothing's `Shop` index made a second + home page. So an index route at the top of a `` is read only + inside the router itself (`createRoutesFromElements(…)`, ``), + and one under a path the file does not spell out (`path={paths.agents}`) + not at all; `path={"agents"}` is spelled out. Reading what an element + renders has three traps of its own: skip elements written in an attribute + unless the attribute hands over a page (`component`, `element`, `page`) — + `}>` bound fifteen routes of one app to the + spinner; trim the line break Prettier writes after `element={`, which hid + every long element; and read `` as rendering nothing of its own. + Version 5's `` is a catch-all, not + the page at its parent's address. A table written in another file and + mapped inside a `` gets no layout edge: the layout's name + would be looked up in the table's file, and the cross-file pass compares + only route names, so a changed layout would never reach the table's routes. +15. **`resolve()` sees only references written in the framework's own + languages** (`getResolvingFrameworks`): Express's `logger` middleware rule + once took etcd's Go `*zap.Logger` result types for a method. A navigation + written in markup (`.svelte`, `.vue`) needs that language in `languages`. + A resolver that reads a language it extracts nothing from lists it in + `resolveLanguages` instead — Svelte's `$lib/…` imports in `.ts` route + modules, ASP.NET's Razor `@model` — because widening `languages` also runs + `extract()` on those files. `claimsReference()` is still asked of every + detected framework. +16. **A navigation call waits for a route, not for a name.** A call indexed + before its route existed is parked as failed (or bound to a catch-all, a + parameter route, the other arm of a conditional), and sync revisits a + failed reference only by matching its last name segment against the names + the synced files define — never `push` or `navigate`. Set the resolver's + `navigation`: `tails`, the method names its calls end in, and `scope`, the + apps whose calls one of its routes can answer (its own app's table, or every + file for a table the whole project shares). A sync that adds, removes or + renames a route — a cross-file table route included — then resolves those + calls again, failed or not; without it the synced index keeps an answer a + full index does not have. `sync-navigation-retry.test.ts` runs all seven + routers through it, and asserts each router's calls end in a declared tail. --- diff --git a/docs/viewer-launch-changelog.md b/docs/viewer-launch-changelog.md index e5141d8b97..9bafed8d6e 100644 --- a/docs/viewer-launch-changelog.md +++ b/docs/viewer-launch-changelog.md @@ -141,6 +141,10 @@ These describe `codegraph ui` and its screens. They were taken out of `## [Unrel - **A server action written through a wrapper starts its transition on the right page.** `export const signIn = validatedAction(schema, async (data) => { … redirect('/dashboard') })` — the arrow inside is no symbol of its own — now belongs to `signIn` on the Screens tab, and `signIn` is attributed to the page whose component hands it to `useActionState(signIn, …)`, read from the source when the graph holds no such edge. +- **React Router apps that keep their routes in a table get a Screens picture.** The ASP.NET Core React template's `AppRoutes` array, a `routes` array handed to `useRoutes` or `createBrowserRouter` from another file, and admin kits' `[MainRoutes, LoginRoutes]` route objects drew no screens, because their routes were never read. They are now, with the links and `navigate(…)` calls between them. A parent route object's component is drawn as the layout around the screens inside it: its own links count from each of those screens, and when an `index: true` child claims its address, that child is the screen there. Re-index React projects after upgrading. + +- **A React Router layout written as JSX is drawn around every screen inside it.** In `}>} />…`, the screen at `/` is Home rather than Layout, and the links in Layout's header count from each screen nested in it instead of only from `/`. A guard with no path of its own, like `}>`, does the same for its redirect to the login page. Re-index React projects after upgrading. + ## Fixes — Steps — what a call does, and when - **A Steps picture with nothing past its anchor says why.** Starting Steps from a helper that only computes drew one box and no explanation. The panel now says that nothing it sets in motion is a step the picture draws, explains that calls between plain functions fold into the lines, and links to the symbol's callers and callees. @@ -176,6 +180,10 @@ These describe `codegraph ui` and its screens. They were taken out of `## [Unrel ## Fixes — Symbols, tests and the viewer - **A slow file cannot freeze the viewer.** Highlighting runs in a child process that is stopped when its deadline expires; the file is shown without highlighting while other requests remain available. Native request parses also have a cooperative time budget. The COBOL scanner handles a short sequence-area line at end of input, including ` .`. +- **Dead code no longer lists methods a framework calls for you.** A method with a framework's decorator on it — Angular's `@HostListener` or `@HostBinding`, NestJS's `@Cron`, `@OnEvent` or `@Process`, TypeORM's `@BeforeInsert`, Spring's `@InitBinder` — was listed as unused whenever the decorator came from a package outside your project. So was a lifecycle hook like Angular's `ngOnInit` or NestJS's `onModuleInit` on a class that skips the `implements` clause, though the framework calls it by name all the same. Both are now left off and counted under the list. What counts as a hook is learned from your own classes that do write the interface out, not from a built-in list, so an unused helper on the same component is still listed. Nothing needs a re-index. + +- **Dead code no longer lists the types you add to a library's or the browser's.** An interface written inside `declare module 'chart.js' { … }` or `declare global { … }`, like chart.js's `PluginOptionsByType`, TanStack Router's `Register`, the browser's `Window`, or a custom element's entry in `HTMLElementTagNameMap`, merges into a type declared outside your project. The library or the browser reads it from there, so nothing in your code names it, and every one was listed as unused. TypeScript exports what these blocks declare without an `export` keyword, and CodeGraph now records them the same way, so they are left off and counted with the exported symbols. A `declare namespace` in an ordinary module file still belongs to that file and is checked as before. Re-index TypeScript projects after upgrading. + - **The Map opens on a picture, not a single box.** A project whose code sits in one flat folder, like Express's `lib/`, an R package's `R/`, an Erlang app's `src/` or fmt's `include/fmt/`, opened as one box with nothing to say. It now opens on the whole repository when that draws more. Files at the repository's top level also count when the Map picks where the program lives: git's hundreds of top-level `.c` files had made `builtin/` look like most of the code. diff --git a/site/src/content/docs/guides/framework-routes.md b/site/src/content/docs/guides/framework-routes.md index 87d09a5e34..73df605b86 100644 --- a/site/src/content/docs/guides/framework-routes.md +++ b/site/src/content/docs/guides/framework-routes.md @@ -91,3 +91,7 @@ Express treats the last registration argument as the handler, excluding a traili Axum and Actix handler links use the handler argument position, including multiline and nested registrations. Actix scope prefixes remain part of the route path. Angular template calls in property bindings, interpolations, structural directives and control-flow blocks link the component member they invoke. A method passed to a child, such as `[displayWith]="displayFn"`, links as a function reference. Template locals hide same-named component members; inherited component members remain unsupported. Framework detection reads the root manifest and the first two directory levels. Remaining slots cover directories above JS/TS code, shallowest first, within the manifest limit. + +React Router also reads imported route tables from their module-level export bindings, and JSX index and layout routes. Incremental sync retries lazy module links and navigation destinations when their files or routes appear. Angular shared-library navigation is reconsidered when another app changes the sole-app fallback. Framework resolvers apply only to their supported languages, including TSX/JSX for SolidStart route targets. + +Cross-file route-table nodes and their references are persisted in one transaction per framework. A failed write rolls back that reconciliation, allowing the next pass to retry it. diff --git a/site/src/content/docs/reference/languages.md b/site/src/content/docs/reference/languages.md index 317bc14419..909ee28fc3 100644 --- a/site/src/content/docs/reference/languages.md +++ b/site/src/content/docs/reference/languages.md @@ -84,3 +84,7 @@ Python `super()` and `super(Cls, self)` calls follow the class's method resoluti A Python method passed as a value through a module-level variable, such as `pool.submit(settings.conn.fetch)` after `global conn; conn = Store()`, links to the method of the class assigned to it. Those classes come from constructor calls or annotations (`conn: Store = make_store()`) in the variable's module, at module level or in a function or class body that declares it `global`, and from writes like `settings.conn = Store()` in other production modules. With several classes, the link goes to the declaration they all inherit. The module may be spelled in full (`import pkg.settings`, then `pkg.settings.conn.fetch`), but a longer chain such as `settings.conn.pool.fetch` reads an attribute and keeps the usual lookup. The usual lookup also decides when an unannotated assignment is something other than a constructor call (a factory, a conditional, a tuple target, a loop, `with`, `del`, an import or a star import), when an annotation and the constructor name different classes, when the writing function or its module rebinds the class's name (`Store = Decoy`) or the file imports it from two places, when `setattr`, `patch.object`, `__dict__` (a computed key included), `globals()`, `vars()` or `sys.modules[__name__]` can rebind the variable, when another module writes it through a name its function or module rebinds or that it imports from two places, when a class or function of the same name rebinds it, and when the calling function binds the name itself. Test files and `if __name__ == "__main__":` blocks don't change the variable's classes. A reference links to nothing when any of the variable's classes lacks a callable method of that name, or when it sits in a test file that installs its own double. C++ receivers declared through typedef or using aliases follow the alias declaration scope and nested owners. Pointer, array and function members are not supertypes. Shopify section and snippet references stay inside their nearest theme; JSON under `templates/` or `sections/` is Liquid only beside a theme marker. Adding or removing a marker reconciles JSON membership and re-resolves unchanged Liquid links, including after reopening the index. + +Go unaliased imports retain their path name and add an unambiguous conventional package name for versioned and `go-` paths. Conventional names are available when reading package factory chains and preserve import order. Imported module-root types use the same package scope as root source files. Type positions prefer types in the written package. Aliases forward methods to their target; defined types keep their own methods. Fluent chains follow each declared return type. Chained calls with an unproven receiver stay unresolved instead of borrowing an unrelated method by name. + +C++ base classes use the enclosing declaration scope, with source-file classes limited to their translation unit. Dependent template bases and ambiguous candidates remain unresolved. Test-named headers included by production files remain visible, including same-stem implementations. Calls on declared lowercase external library types do not link to unrelated project methods when the member operator agrees with the declaration. Multiline raw-string contents are excluded from receiver declaration scans. diff --git a/src/codegraph.ts b/src/codegraph.ts index 807378cbca..6284fee0bb 100644 --- a/src/codegraph.ts +++ b/src/codegraph.ts @@ -33,7 +33,7 @@ import { import { DatabaseConnection, getDatabasePath, removeDatabaseFiles } from './db'; import { WalCheckpointValve, resolveWalValveMb } from './db/wal-valve'; import { QueryBuilder, type NodeSpan } from './db/queries'; -import { importPathKeys } from './db/reference-tail'; +import { importPathKeys, moduleReferenceKeys } from './db/reference-tail'; import { isInitialized, createDirectory, @@ -989,7 +989,11 @@ export class CodeGraph { const fullReconcile = !options.paths || options.paths.length === 0; const gitState = this.orchestrator.beginGitIndexState(fullReconcile); - const result = await this.orchestrator.sync(options.onProgress, options.paths, backpressure); + const watchRoutes = this.resolver.hasNavigationRouters(); + let routesBefore: Node[] | null = null; + const result = await this.orchestrator.sync(options.onProgress, options.paths, backpressure, () => { + if (watchRoutes && routesBefore === null) routesBefore = this.queries.getNodesByKind('route'); + }); // Fold the store phase's WAL BEFORE the post-store reads below // (resolution reads on the main thread) — same rationale as @@ -1000,25 +1004,17 @@ export class CodeGraph { // every sync that touched files so edits to `app.module.ts` propagate // to controllers in unchanged files. The pass is idempotent and cheap // (regex over *.module.ts only). - if (result.filesAdded > 0 || result.filesModified > 0) { - // Re-detect frameworks: a sync can add the dependency and the route together. + if (result.filesAdded > 0 || result.filesModified > 0 || result.filesRemoved > 0) { this.redetectFrameworks(); this.resolver.runPostExtract(); - } else if (result.filesRemoved > 0) { - // A pure-removal sync still resolves refs below — the deletion path - // resurrects the removed file's incoming edges as pending refs - // (#1240 removal case) and the orphan sweep consumes them. In a - // long-lived process (daemon) the resolver's name caches were - // warmed against the pre-removal graph; drop them so resolution - // sees the post-removal state. (runPostExtract above clears caches - // itself, so the changed-files branch is already covered.) - this.resolver.clearCaches(); - // RedwoodSDK classifies a route as a page from its handler's JSX, so - // deleting a handler file must re-run that pass; a route prefixed by - // where another file registers it must drop a deleted registration. - if (this.queries.getNodesByKind('route').some(n => n.id.startsWith('route:redwood:') || - n.qualifiedName.includes('::fastify-plugin:') || n.qualifiedName.includes('::solid-table:'))) - this.resolver.runPostExtract(); + } + if (routesBefore) { + const routes = resolution().changedRoutes(routesBefore, this.queries.getNodesByKind('route')); + this.resolver.reopenNavigationsFor(routes, result.changedFilePaths ?? []); + if (routes.length > 0) { + this.queries.setSynthesisPending(true); + for (const route of routes) this.synthesisDirty.add(route.filePath); + } } // Resolve references if files were updated @@ -1059,9 +1055,10 @@ export class CodeGraph { // and re-resolve just that set. On a sync where no failed ref // matches, this is one indexed lookup. const tRetry = Date.now(); - const retryable = this.queries.getRetryableFailedReferences( - this.queries.getNodeNamesByFiles(result.changedFilePaths) - ); + const retryable = this.queries.getRetryableFailedReferences([...new Set([ + ...this.queries.getNodeNamesByFiles(result.changedFilePaths), + ...result.changedFilePaths.flatMap(moduleReferenceKeys), + ])]); const retryRows = new Set(retryable.map(ref => ref.rowId)); const importRetry = this.queries.getRetryableFailedImports( (result.addedFilePaths ?? []).flatMap(importPathKeys), @@ -1070,6 +1067,7 @@ export class CodeGraph { for (const ref of importRetry) { if (!retryRows.has(ref.rowId)) { retryable.push(ref); retryRows.add(ref.rowId); } } + retryable.sort((a, b) => (a.rowId ?? 0) - (b.rowId ?? 0)); if (retryable.length > 0) { options.onProgress?.({ phase: 'resolving', @@ -1894,6 +1892,24 @@ export class CodeGraph { return this.queries.getUnresolvedSupertypeSourcesAmong(nodeIds); } + /** + * The names each of the given symbols refers to, by reference kind, where + * the resolver could not follow the reference — a decorator or an interface + * from outside the index, which leaves no edge. Ids with none are absent. + */ + getUnresolvedReferenceNamesFrom(nodeIds: Iterable, kinds: readonly Edge['kind'][]): Map { + return this.queries.getUnresolvedReferenceNamesFrom(nodeIds, kinds); + } + + /** + * Every symbol that refers to each of the given names, by reference kind, + * where the resolver could not follow it: for `implements`, every class that + * names a given interface from outside the index. + */ + getUnresolvedReferenceSourcesNamed(names: Iterable, kinds: readonly Edge['kind'][]): Map> { + return this.queries.getUnresolvedReferenceSourcesNamed(names, kinds); + } + /** * The symbols with the most distinct dependents, most first — the index's * hubs. Distinct dependents, not edges: a helper called forty times from one diff --git a/src/db/migrations.ts b/src/db/migrations.ts index eb94d105d4..f5562cc470 100644 --- a/src/db/migrations.ts +++ b/src/db/migrations.ts @@ -10,7 +10,7 @@ import { referenceNameTail } from './reference-tail'; /** * Current schema version */ -export const CURRENT_SCHEMA_VERSION = 18; +export const CURRENT_SCHEMA_VERSION = 20; /** * Migration definition @@ -351,6 +351,54 @@ CREATE TABLE synth_skips ( for (const row of rows) update.run(referenceNameTail(row.reference_name, 'imports'), row.id); }, }, + { + version: 19, + description: 'Retry a failed path reference when sync adds the file it names: file-name tails', + up: (db) => { + if ((db.prepare('PRAGMA table_info(unresolved_refs)').all() as unknown[]).length === 0) return; + // A path reference parked before this version — a Liquid + // `snippets/price.liquid` — carries its extension as its tail, which no + // node is named. Rewrite it to the file name a failed path reference is + // parked under now. Idempotent: a rewritten tail rewrites to itself. + const update = db.prepare('UPDATE unresolved_refs SET name_tail = ? WHERE id = ?'); + const rows = db + .prepare("SELECT id, reference_name, name_tail FROM unresolved_refs WHERE status = 'failed' AND reference_kind = 'references' AND reference_name LIKE '%/%.%'") + .all() as Array<{ id: number; reference_name: string; name_tail: string }>; + for (const row of rows) { + const tail = referenceNameTail(row.reference_name, 'references'); + if (tail !== row.name_tail) update.run(tail, row.id); + } + }, + }, + { + version: 20, + description: 'Retry a failed route module reference when sync adds or changes its file: module tails', + up: (db) => { + if ((db.prepare('PRAGMA table_info(unresolved_refs)').all() as unknown[]).length === 0) return; + // A route's reference to the module it lazily loads, parked before this + // version — React Router's `lazy-import:./pages/Team`, Vue Router's and + // Angular's `import:./home/home.component#HomeComponent`, each also + // behind `layout:` — carries a fragment of its path as its tail + // ('/pages/Team', 'component#HomeComponent'), which no file's keys + // match. Rewrite it to the module tail it is parked under now. + // Idempotent: a rewritten tail rewrites to itself. + // + // Each prefix is a range of idx_unresolved_name. The `+` keeps the + // planner off idx_unresolved_status, which it otherwise picks although + // 'failed' is nearly every row: on vscode's index that read 830K rows, + // about a second, to select none. + const update = db.prepare('UPDATE unresolved_refs SET name_tail = ? WHERE id = ?'); + const rows = db + .prepare(`SELECT id, reference_name, reference_kind, name_tail FROM unresolved_refs + WHERE (reference_name GLOB 'lazy-import:*' OR reference_name GLOB 'import:*' OR reference_name GLOB 'layout:*') + AND +status = 'failed' AND +reference_kind IN ('references', 'calls')`) + .all() as Array<{ id: number; reference_name: string; reference_kind: string; name_tail: string }>; + for (const row of rows) { + const tail = referenceNameTail(row.reference_name, row.reference_kind); + if (tail !== row.name_tail) update.run(tail, row.id); + } + }, + }, ]; /** diff --git a/src/db/queries.ts b/src/db/queries.ts index e4eaa787f9..6a3382ddaa 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -1528,6 +1528,18 @@ export class QueryBuilder { } } + *iterateNodesByKindIn(kind: NodeKind | readonly NodeKind[], languages: readonly string[]): IterableIterator { + const kinds: readonly NodeKind[] = typeof kind === 'string' ? [kind] : kind; + if (kinds.length === 0 || languages.length === 0) return; + const kindTest = kinds.length === 1 ? 'kind = ?' : `kind IN (${kinds.map(() => '?').join(', ')})`; + const stmt = this.db.prepare( + `SELECT * FROM nodes WHERE ${kindTest} AND language IN (${languages.map(() => '?').join(', ')}) ORDER BY file_path, start_line, id` + ); + for (const row of stmt.iterate(...kinds, ...languages)) { + yield rowToNode(row as NodeRow); + } + } + /** * Get all nodes in the database */ @@ -2591,19 +2603,69 @@ export class QueryBuilder { * Chunked probe over `idx_unresolved_from_node`. */ getUnresolvedSupertypeSourcesAmong(nodeIds: Iterable): Set { + return new Set(this.getUnresolvedReferenceNamesFrom(nodeIds, ['extends', 'implements']).keys()); + } + + /** + * The names each of `nodeIds` holds an unresolved reference of `kinds` to. + * + * A reference into code outside the index leaves no edge, only this row: a + * decorator imported from a framework (`@HostListener`, `@Cron`) or an + * interface named in an `implements` clause. Pending rows count with failed + * ones, as in {@link getUnresolvedSupertypeSourcesAmong}. Ids with no such row + * are absent from the map. Chunked probe over `idx_unresolved_from_node`. + */ + getUnresolvedReferenceNamesFrom(nodeIds: Iterable, kinds: readonly string[]): Map { const unique = [...new Set(nodeIds)]; - const found = new Set(); + const found = new Map(); + if (unique.length === 0 || kinds.length === 0) return found; + const kindPlaceholders = kinds.map(() => '?').join(','); for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) { const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE); const placeholders = chunk.map(() => '?').join(','); const rows = this.db .prepare( - `SELECT DISTINCT from_node_id AS id FROM unresolved_refs + `SELECT DISTINCT from_node_id AS id, reference_name AS name FROM unresolved_refs WHERE from_node_id IN (${placeholders}) - AND reference_kind IN ('extends', 'implements')` + AND reference_kind IN (${kindPlaceholders})` ) - .all(...chunk) as Array<{ id: string }>; - for (const row of rows) found.add(row.id); + .all(...chunk, ...kinds) as Array<{ id: string; name: string }>; + for (const row of rows) { + const names = found.get(row.id); + if (names) names.push(row.name); + else found.set(row.id, [row.name]); + } + } + return found; + } + + /** + * Every node holding an unresolved reference of `kinds` to each of `names` — + * the mirror of {@link getUnresolvedReferenceNamesFrom}, keyed by the name: + * for `implements`, every class that names an interface outside the index. + * Names nothing refers to are absent from the map. Chunked probe over + * `idx_unresolved_name`. + */ + getUnresolvedReferenceSourcesNamed(names: Iterable, kinds: readonly string[]): Map> { + const unique = [...new Set(names)].filter((name) => name.length > 0); + const found = new Map>(); + if (unique.length === 0 || kinds.length === 0) return found; + const kindPlaceholders = kinds.map(() => '?').join(','); + for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) { + const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE); + const placeholders = chunk.map(() => '?').join(','); + const rows = this.db + .prepare( + `SELECT DISTINCT reference_name AS name, from_node_id AS id FROM unresolved_refs + WHERE reference_name IN (${placeholders}) + AND reference_kind IN (${kindPlaceholders})` + ) + .all(...chunk, ...kinds) as Array<{ name: string; id: string }>; + for (const row of rows) { + const ids = found.get(row.name); + if (ids) ids.add(row.id); + else found.set(row.name, new Set([row.id])); + } } return found; } @@ -3176,7 +3238,8 @@ export class QueryBuilder { * `contains`. */ getCrossFileIncomingEdgesWithTarget( - filePath: string + filePath: string, + includeSameFile = false ): Array { const sql = `SELECT e.*, tgt.name AS target_name, tgt.kind AS target_kind, src.file_path AS source_file_path, src.language AS source_language @@ -3185,8 +3248,8 @@ export class QueryBuilder { JOIN nodes src ON src.id = e.source WHERE tgt.file_path = ? AND e.kind != 'contains' - AND src.file_path != ?`; - const rows = this.db.prepare(sql).all(filePath, filePath) as Array< + AND (? = 1 OR src.file_path != ?)`; + const rows = this.db.prepare(sql).all(filePath, includeSameFile ? 1 : 0, filePath) as Array< EdgeRow & { target_name: string; target_kind: NodeKind; source_file_path: string; source_language: Language } >; return rows.map(row => ({ @@ -3981,6 +4044,9 @@ export class QueryBuilder { * (`get`, `map`, …) that one new definition won't resolve — the same * rationale as resolution's AMBIGUOUS_NAME_CEILING (#999) — and retrying an * arbitrary subset would be both wasted work and incoherent coverage. + * + * A name can also be one of a changed file's `moduleReferenceKeys`, which a + * route's reference to the module it lazily loads is parked under. */ getRetryableFailedReferences(names: string[], perNameCeiling: number = 500): UnresolvedReference[] { if (names.length === 0) return []; @@ -4090,6 +4156,70 @@ export class QueryBuilder { })); } + /** + * Failed `calls` refs whose name tail is one of `tails` — the navigation + * calls (`history.push`, `navigate`) a sync looks through after a route + * appeared or went away. Read through the failed-tail index; the caller + * decides on each whole name. + */ + getFailedCallsByTail(tails: string[]): Array<{ rowId: number; referenceName: string; filePath: string }> { + const out: Array<{ rowId: number; referenceName: string; filePath: string }> = []; + const unique = [...new Set(tails)]; + for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) { + const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE); + const placeholders = chunk.map(() => '?').join(','); + const rows = this.db + .prepare( + `SELECT id, reference_name, file_path FROM unresolved_refs + WHERE status = 'failed' AND reference_kind = 'calls' AND name_tail IN (${placeholders})` + ) + .all(...chunk) as Array<{ id: number; reference_name: string; file_path: string }>; + for (const row of rows) out.push({ rowId: row.id, referenceName: row.reference_name, filePath: row.file_path }); + } + return out; + } + + /** + * The `navigates` edges a router's resolver made (not a synthesizer's), with + * the source file and language a resurrection needs — the navigation calls + * a sync re-resolves after a route appeared or went away. + */ + getResolvedNavigations(): Array { + const rows = this.db + .prepare( + `SELECT e.*, src.file_path AS source_file_path, src.language AS source_language + FROM edges e + JOIN nodes src ON src.id = e.source + WHERE e.kind = 'navigates' AND (e.provenance IS NULL OR e.provenance != 'heuristic')` + ) + .all() as Array; + return rows.map((row) => ({ + ...rowToEdge(row), + edgeId: row.id, + sourceFilePath: row.source_file_path, + sourceLanguage: row.source_language, + })); + } + + /** + * Put failed refs back in the pending set, for the next resolution pass — + * the sync's orphan sweep — to try again. Returns the number re-opened. + */ + reopenFailedReferences(rowIds: number[]): number { + if (rowIds.length === 0) return 0; + let changed = 0; + this.db.transaction(() => { + for (let i = 0; i < rowIds.length; i += SQLITE_PARAM_CHUNK_SIZE) { + const chunk = rowIds.slice(i, i + SQLITE_PARAM_CHUNK_SIZE); + const placeholders = chunk.map(() => '?').join(','); + changed += this.db + .prepare(`UPDATE unresolved_refs SET status = 'pending' WHERE status = 'failed' AND id IN (${placeholders})`) + .run(...chunk).changes; + } + })(); + return changed; + } + /** * Resolution edges whose TARGET symbol is named one of `names` — the edges a * sync must re-resolve after `names` gained or lost a definition (CG-33). diff --git a/src/db/reference-tail.ts b/src/db/reference-tail.ts index 4c7ed319b0..61c16e4a1d 100644 --- a/src/db/reference-tail.ts +++ b/src/db/reference-tail.ts @@ -1,8 +1,9 @@ /** * The keys sync's failed-ref retry (#1240) matches on: the tail a failed ref * is parked under, and the names a newly added file can be imported as. - * Shared by the query layer, which writes and reads them, and the migration - * that rewrote the tails parked before path imports had one of their own. + * Shared by the query layer, which writes and reads them, and the migrations + * that rewrote the tails parked before path imports, path references and + * module references had ones of their own. */ /** @@ -17,14 +18,39 @@ * `inc/db.php` → 'db' — the stem {@link importPathKeys} takes from a file * that could satisfy it. The dotted tail was the extension ('dart', 'php') or * a path fragment ('/foo'), which no file ever matched. + * + * A route's reference to the module it lazily loads names a file the same + * way, by the module's path with or without its extension: React Router's + * `lazy-import:./pages/Team`, Vue Router's and Angular's + * `import:./home/home.component#HomeComponent`. It is parked under that + * path's stem behind 'module:' — 'module:Team', 'module:home' — the key + * {@link moduleReferenceKeys} gives a file that could be the module. A bare + * stem would be any symbol's name, which a module reference does not wait + * for: 'index' and 'types' are the tails of thousands of failed calls. Its + * dotted tail was a path fragment ('/pages/Team') or what follows the + * module's own dot ('component#HomeComponent'), which no file ever matched. + * + * Any other reference written as a path to a file names it by the file's own + * name: Liquid's `{% render 'price' %}` is `snippets/price.liquid`, parked as + * 'price.liquid' — the name path matching looks the file up by, and the name + * of the node a file that appears later is given. Its dotted tail was the + * extension, 'liquid'. A call's slashes are in its arguments, a comment or a + * division, so only a `references` ref is read as a path. */ export function referenceNameTail(referenceName: string, referenceKind?: string): string { - if (referenceKind === 'imports') { - const path = referenceName.replace(/\/+$/, ''); - const slash = path.lastIndexOf('/'); - const stem = slash >= 0 ? fileNameStem(path.slice(slash + 1)) : ''; - // `..` and `.` name a folder only through the importing file's location. - if (!/^\.*$/.test(stem)) return stem; + if (referenceKind === 'imports' && referenceName.replace(/\/+$/, '').includes('/')) { + const stem = pathStem(referenceName); + if (stem) return stem; + } + // Vue Router's route names the component it renders as a call. + if (referenceKind === 'references' || referenceKind === 'calls') { + const route = MODULE_REFERENCE.exec(referenceName); + const stem = route ? pathStem(route[1] ?? route[2]!) : ''; + if (stem) return MODULE_KEY + stem; + } + if (referenceKind === 'references') { + const fileName = referenceName.slice(referenceName.lastIndexOf('/') + 1); + if (fileName !== referenceName && FILE_NAME.test(fileName)) return fileName; } // Erlang refs carry a written arity (`f/1`, `mod::fn/2` — #1610); the tail a // new symbol's plain name could match is the arity-less function name. @@ -33,6 +59,28 @@ export function referenceNameTail(referenceName: string, referenceKind?: string) return idx >= 0 ? base.slice(idx + 1) : base; } +/** A file name with an extension: 'price.liquid', 'icon.logo.liquid', 'about.tsx' — not 'x.component#Name'. */ +const FILE_NAME = /^[\w$@+~.-]+\.[A-Za-z][A-Za-z0-9]*$/; + +/** + * A route's reference to the module it lazily loads, capturing the module's + * path: React Router's `lazy-import:`, Vue Router's and Angular's + * `import:#` — a path may hold a `#` itself, as vben's `#/views/…` + * alias does — each also behind `layout:`, for a layout route's module. + */ +const MODULE_REFERENCE = /^(?:layout:)?(?:lazy-import:(.+)|import:(.+)#[^#]+)$/; + +/** What a module reference's tail starts with. No symbol's name does, so a lookup by symbol names never finds one. */ +const MODULE_KEY = 'module:'; + +/** A path's last segment up to its first dot — `./pages/Team` → 'Team', `a/b.dart` → 'b' — or '' when that is only dots. */ +function pathStem(path: string): string { + const trimmed = path.replace(/\/+$/, ''); + const stem = fileNameStem(trimmed.slice(trimmed.lastIndexOf('/') + 1)); + // `..` and `.` name a folder only through the referring file's location. + return /^\.*$/.test(stem) ? '' : stem; +} + /** A file or folder name up to its first dot: 'b.dart' → 'b', 'types.d.ts' → 'types', '.eslintrc.js' → '.eslintrc'. */ function fileNameStem(name: string): string { const dot = name.indexOf('.', 1); @@ -55,3 +103,15 @@ export function importPathKeys(filePath: string): string[] { } return [...keys]; } + +/** + * The keys a module reference the file at `filePath` could satisfy is parked + * under: its {@link importPathKeys} behind 'module:' — `src/pages/Team.tsx` + * is the module of `lazy-import:./pages/Team`, `pages/Team/index.tsx` of the + * same path through its folder. A sync looks them up for the files it adds + * AND the ones it changes: a route renders the component its module exports, + * so an edit that gives the module one is what the route waited for. + */ +export function moduleReferenceKeys(filePath: string): string[] { + return importPathKeys(filePath).map((key) => MODULE_KEY + key); +} diff --git a/src/extraction/index.ts b/src/extraction/index.ts index fcba87814f..32fb55a039 100644 --- a/src/extraction/index.ts +++ b/src/extraction/index.ts @@ -17,7 +17,6 @@ import { ExtractionError, Edge, UnresolvedReference, - ReferenceKind, } from '../types'; import { QueryBuilder, NodeIdentity } from '../db/queries'; import { extractFromSource } from './tree-sitter'; @@ -41,6 +40,7 @@ import { extractWakuRoutes, isWakuRouteFile } from '../resolution/frameworks/wak import { extractAnalogRoutes, isAnalogPage } from '../resolution/frameworks/analog'; import type { ResolutionContext } from '../resolution/types'; import { createYielder, type MaybeYield } from '../resolution/cooperative-yield'; +import { resurrectRefFromDroppedEdge } from '../resolution/resurrect-ref'; import { MAX_SOURCE_FILE_SIZE_BYTES, oversizeStamp, readBoundedSource, readBoundedSourceSync } from '../file-limits'; export { oversizeStamp }; @@ -1775,38 +1775,6 @@ function scanDirectoryWalk( return files; } -/** - * Resurrect a resolution edge that is about to be dropped (its target symbol - * was removed, renamed, or its whole file deleted) as the ORIGINAL unresolved - * reference that created it, read from the refName/refKind stamp - * `createEdges` writes into edge metadata. Inserted as status='pending', the - * ref is consumed by the same sync's resolution sweep: it rebinds to an - * alternative definition if one exists, or parks as status='failed' where the - * #1240 retry finds it if the symbol later reappears. - * - * Returns null — drop silently, the pre-#1240 behavior — for edges without a - * refName stamp (created before the stamp existed, or synthesized): rebuilding - * a ref from the target's plain node name would strip the receiver/qualifier - * context the original text carried (`h.greet` → `greet`) and could rebind - * somewhere a full re-index never would. Silent beats wrong. - */ -function resurrectRefFromDroppedEdge( - e: Edge & { sourceFilePath: string; sourceLanguage: Language } -): UnresolvedReference | null { - const refName = e.metadata?.refName; - if (typeof refName !== 'string' || refName.length === 0) return null; - const refKind = typeof e.metadata?.refKind === 'string' ? (e.metadata.refKind as ReferenceKind) : e.kind; - return { - fromNodeId: e.source, - referenceName: refName, - referenceKind: refKind, - line: e.line ?? 0, - column: e.column ?? 0, - filePath: e.sourceFilePath, - language: e.sourceLanguage, - }; -} - /** * What tells a file's nodes apart across a re-index, coarsest first. (kind, * name) is the #899 key and settles every name a file defines once; a name it diff --git a/src/extraction/languages/cpp-brace-scopes.ts b/src/extraction/languages/cpp-brace-scopes.ts new file mode 100644 index 0000000000..f83968117e --- /dev/null +++ b/src/extraction/languages/cpp-brace-scopes.ts @@ -0,0 +1,360 @@ +/** + * C++ scopes read from the source's braces, for a file whose parse tree has + * errors. + * + * tree-sitter recovers from a construct it can't parse by inserting the token + * it expected, and inside a class body that token is often a `}`: an unknown + * macro in front of a member (`PROTOBUF_FUTURE_ADD_EARLY_NODISCARD + * absl::string_view name() const`, `struct ALIGN_AS(64U) HandleImpl`) closes + * the class there. Every `}` after it then closes the scope one level out, so + * the rest of the class, and of each namespace around it, parses as + * declarations of an outer scope: protobuf's `FieldDescriptor` indexed as + * `FieldDescriptor` instead of `google::protobuf::FieldDescriptor`, rocksdb's + * `struct Opts` outside the class it is declared in. Recovery also runs the + * other way and keeps a scope open past its own `}`. + * + * The braces are rarely what the parser misread, so the walker takes each + * declaration's namespaces and enclosing classes from them instead + * (TreeSitterExtractor.visitInCppBraceScope; mirrored in the kernel, + * ccpp/mod.rs). The scan skips comments, string and character literals (raw + * strings and digit separators too) and preprocessor lines. Each branch of an + * `#if` is read from the braces open at the `#if`, and the scan continues + * after `#endif` with the first branch's — so `#if A` `struct X : B {` + * `#else` `struct X {` `#endif` opens one scope, not two. A file whose braces + * don't balance gets no scopes and keeps the tree's. + */ + +/** Intervals that nest properly, added in order of their opening offset. */ +export class NestedIntervals { + private readonly opens: number[] = []; + private readonly closes: number[] = []; + private readonly parents: number[] = []; + private readonly values: T[] = []; + + /** + * Adds the interval `(open, close)`. One that opens before the last one + * added, or crosses one that holds it, is refused (false). + */ + add(open: number, close: number, value: T): boolean { + const count = this.opens.length; + if (close <= open || (count > 0 && open <= this.opens[count - 1]!)) return false; + const parent = this.innermost(open); + if (parent >= 0 && close >= this.closes[parent]!) return false; + this.opens.push(open); + this.closes.push(close); + this.parents.push(parent); + this.values.push(value); + return true; + } + + /** The values of the intervals holding `offset` (open < offset < close), outermost first. */ + at(offset: number): T[] { + const out: T[] = []; + for (let k = this.innermost(offset); k >= 0; k = this.parents[k]!) out.push(this.values[k]!); + return out.reverse(); + } + + /** + * The innermost interval holding `offset`, or -1. It is the last interval + * opening before `offset` or one of the intervals around that one. + */ + private innermost(offset: number): number { + let lo = 0; + let hi = this.opens.length - 1; + let k = -1; + while (lo <= hi) { + const mid = (lo + hi) >> 1; + if (this.opens[mid]! < offset) { + k = mid; + lo = mid + 1; + } else { + hi = mid - 1; + } + } + while (k >= 0 && this.closes[k]! <= offset) k = this.parents[k]!; + return k; + } +} + +export interface CppBraceScopes { + /** The offset of the `}` that closes the `{` at `open`. */ + closeOf(open: number): number | undefined; + /** The named namespaces whose braces hold `offset`, outermost first, as written (`a::b`). */ + namespacesAt(offset: number): string[]; + /** The 1-based line and the column of `offset`. */ + positionOf(offset: number): { line: number; column: number }; +} + +const NEWLINE = 10; +const isBlank = (c: number): boolean => c === 32 || c === 9 || c === 13 || c === 12 || c === 11; +const isWordStart = (c: number): boolean => + (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || c === 95 || c === 36 || c >= 128; +const isDigit = (c: number): boolean => c >= 48 && c <= 57; +const isWordChar = (c: number): boolean => isWordStart(c) || isDigit(c); +const RAW_STRING_PREFIX = /^(?:u8|[uUL])?R$/; +// A raw string's delimiter: up to 16 printable ASCII characters but `"`, `(`, `)` and `\`. +const RAW_STRING_DELIMITER = /^[!#-'*-[\]-~]{0,16}$/; + +/** The end of a `//` comment starting at `i`: its newline (a `\` before it continues the comment). */ +function lineCommentEnd(source: string, i: number): number { + for (;;) { + const nl = source.indexOf('\n', i); + if (nl < 0) return source.length; + let k = nl - 1; + if (source.charCodeAt(k) === 13) k--; + if (source.charCodeAt(k) !== 92) return nl; + i = nl + 1; + } +} + +/** Past whitespace and comments from `i`. */ +function skipBlank(source: string, i: number): number { + const n = source.length; + while (i < n) { + const c = source.charCodeAt(i); + if (isBlank(c) || c === NEWLINE) i++; + else if (c === 47 && source.charCodeAt(i + 1) === 47) i = lineCommentEnd(source, i + 2); + else if (c === 47 && source.charCodeAt(i + 1) === 42) { + const end = source.indexOf('*/', i + 2); + i = end < 0 ? n : end + 2; + } else break; + } + return i; +} + +/** The end of the identifier at `i`, or -1 when none starts there. */ +function wordEnd(source: string, i: number): number { + if (!isWordStart(source.charCodeAt(i))) return -1; + let j = i + 1; + while (j < source.length && isWordChar(source.charCodeAt(j))) j++; + return j; +} + +/** Past the `(…)` group at `i` (strings inside skipped), or -1. */ +function parenGroupEnd(source: string, i: number): number { + let depth = 0; + for (let j = i; j < source.length; j++) { + const c = source.charCodeAt(j); + if (c === 34 || c === 39) j = quotedEnd(source, j) - 1; + else if (c === 40) depth++; + else if (c === 41 && --depth === 0) return j + 1; + } + return -1; +} + +/** Past the string or character literal whose quote is at `i` (it stops at a newline). */ +function quotedEnd(source: string, i: number): number { + const quote = source.charCodeAt(i); + let j = i + 1; + while (j < source.length) { + const c = source.charCodeAt(j); + if (c === 92) j += source.charCodeAt(j + 1) === 13 && source.charCodeAt(j + 2) === NEWLINE ? 3 : 2; + else if (c === quote) return j + 1; + else if (c === NEWLINE) return j; + else j++; + } + return source.length; +} + +/** + * After the `namespace` keyword ending at `from`: the name as written and the + * offset of the `{` that opens the body. Null for anything else, such as + * `using namespace std;` or `namespace fs = std::filesystem;`. Takes C++17 + * attributes (`namespace [[deprecated]] old {`), nested and inline names + * (`namespace a::inline b {`), and an attribute macro after the name + * (`namespace std _GLIBCXX_VISIBILITY(default) {`). + */ +function namespaceHead(source: string, from: number): { name: string; brace: number } | null { + let i = skipBlank(source, from); + while (source.startsWith('[[', i)) { + const end = source.indexOf(']]', i + 2); + if (end < 0) return null; + i = skipBlank(source, end + 2); + } + let nameStart = -1; + let nameEnd = -1; + for (let end = wordEnd(source, i); end >= 0; end = wordEnd(source, i)) { + if (nameStart < 0) nameStart = i; + nameEnd = end; + i = skipBlank(source, end); + if (!source.startsWith('::', i)) break; + i = skipBlank(source, i + 2); + const inline = wordEnd(source, i); + if (inline >= 0 && source.slice(i, inline) === 'inline') i = skipBlank(source, inline); + } + for (let end = wordEnd(source, i); end >= 0 && nameStart >= 0; end = wordEnd(source, i)) { + const paren = skipBlank(source, end); + const after = source.charCodeAt(paren) === 40 ? parenGroupEnd(source, paren) : -1; + if (after < 0) return null; + i = skipBlank(source, after); + } + if (source.charCodeAt(i) !== 123) return null; + return { name: nameStart < 0 ? '' : source.slice(nameStart, nameEnd), brace: i }; +} + +/** + * Scan `source` (a C++ file as the parser saw it, after preParse) for its + * braces and namespaces. Null when the braces don't balance. + */ +export function scanCppBraceScopes(source: string): CppBraceScopes | null { + const n = source.length; + const closes = new Map(); + let open: number[] = []; + // Per `#if` group: the braces open at the `#if`, and those open after its + // first branch (null until a second branch starts). + const branches: Array<{ atIf: number[]; afterFirst: number[] | null }> = []; + const namespaceAt = new Map(); // `{` offset → name, for named namespaces + let pendingNamespace: { name: string; brace: number } | null = null; + let balanced = true; + let lineStart = true; + let wordStart = -1; // the last identifier or number read + let wordEndAt = -1; + let i = source.charCodeAt(0) === 0xfeff ? 1 : 0; // past a byte-order mark, so line 1 can be a directive + while (i < n) { + const c = source.charCodeAt(i); + if (c === NEWLINE) { + lineStart = true; + i++; + continue; + } + if (isBlank(c)) { + i++; + continue; + } + if (c === 47 && source.charCodeAt(i + 1) === 47) { + i = lineCommentEnd(source, i + 2); + continue; + } + if (c === 47 && source.charCodeAt(i + 1) === 42) { + const end = source.indexOf('*/', i + 2); + i = end < 0 ? n : end + 2; + continue; + } + if (c === 35 && lineStart) { + // A directive runs to the end of its line, through `\` continuations + // and comments. Only `"` strings are skipped in it: an apostrophe in + // `#error Don't …` is no character literal. + let j = i + 1; + while (j < n && isBlank(source.charCodeAt(j))) j++; + const nameEnd = wordEnd(source, j); + const directive = nameEnd < 0 ? '' : source.slice(j, nameEnd); + while (j < n) { + const d = source.charCodeAt(j); + if (d === NEWLINE) { + let k = j - 1; + if (source.charCodeAt(k) === 13) k--; + if (source.charCodeAt(k) !== 92) break; + j++; + } else if (d === 47 && source.charCodeAt(j + 1) === 47) { + j = lineCommentEnd(source, j + 2); + } else if (d === 47 && source.charCodeAt(j + 1) === 42) { + const end = source.indexOf('*/', j + 2); + j = end < 0 ? n : end + 2; + } else if (d === 34) { + j = quotedEnd(source, j); + } else { + j++; + } + } + if (directive === 'if' || directive === 'ifdef' || directive === 'ifndef') { + branches.push({ atIf: open.slice(), afterFirst: null }); + } else if ( + (directive === 'else' || directive === 'elif' || directive === 'elifdef' || directive === 'elifndef') && + branches.length > 0 + ) { + const group = branches[branches.length - 1]!; + if (group.afterFirst === null) group.afterFirst = open; + open = group.atIf.slice(); + } else if (directive === 'endif' && branches.length > 0) { + const group = branches.pop()!; + if (group.afterFirst !== null) open = group.afterFirst; + } + i = j; + continue; + } + lineStart = false; + if (c === 34) { + // A raw string (`R"x(…)x"`) can hold any brace and quote; its prefix is + // the word that ends right at the quote. + if (wordEndAt === i && i - wordStart <= 3 && RAW_STRING_PREFIX.test(source.slice(wordStart, i))) { + const paren = source.indexOf('(', i + 1); + const delimiter = paren < 0 ? '' : source.slice(i + 1, paren); + if (paren >= 0 && RAW_STRING_DELIMITER.test(delimiter)) { + const end = source.indexOf(`)${delimiter}"`, paren + 1); + i = end < 0 ? n : end + delimiter.length + 2; + continue; + } + } + i = quotedEnd(source, i); + continue; + } + if (c === 39) { + i = quotedEnd(source, i); + continue; + } + if (isDigit(c) || (c === 46 && isDigit(source.charCodeAt(i + 1)))) { + // A number, digit separators (`1'000'000`) and exponent signs included. + let j = i + 1; + while (j < n) { + const d = source.charCodeAt(j); + if (isWordChar(d) || d === 46) j++; + else if (d === 39 && isWordChar(source.charCodeAt(j + 1))) j += 2; + else if ((d === 43 || d === 45) && /[eEpP]/.test(source[j - 1]!)) j++; + else break; + } + wordStart = i; + wordEndAt = j; + i = j; + continue; + } + if (isWordStart(c)) { + const end = wordEnd(source, i); + wordStart = i; + wordEndAt = end; + if (end - i === 9 && source.startsWith('namespace', i)) { + const head = namespaceHead(source, end); + if (head) pendingNamespace = head; + } + i = end; + continue; + } + if (c === 123) { + open.push(i); + if (pendingNamespace && pendingNamespace.brace === i) { + if (pendingNamespace.name) namespaceAt.set(i, pendingNamespace.name); + pendingNamespace = null; + } + } else if (c === 125) { + const from = open.pop(); + if (from === undefined) balanced = false; + else if (!closes.has(from)) closes.set(from, i); + } + i++; + } + if (!balanced || open.length > 0) return null; + + const namespaces = new NestedIntervals(); + for (const [brace, name] of [...namespaceAt].sort((a, b) => a[0] - b[0])) { + const close = closes.get(brace); + if (close !== undefined) namespaces.add(brace, close, name); + } + let lineStarts: number[] | null = null; + return { + closeOf: (at) => closes.get(at), + namespacesAt: (offset) => namespaces.at(offset), + positionOf: (offset) => { + if (!lineStarts) { + lineStarts = [0]; + for (let k = source.indexOf('\n'); k >= 0; k = source.indexOf('\n', k + 1)) lineStarts.push(k + 1); + } + let lo = 0; + let hi = lineStarts.length - 1; + while (lo < hi) { + const mid = (lo + hi + 1) >> 1; + if (lineStarts[mid]! <= offset) lo = mid; + else hi = mid - 1; + } + return { line: lo + 1, column: offset - lineStarts[lo]! }; + }, + }; +} diff --git a/src/extraction/languages/go.ts b/src/extraction/languages/go.ts index f4b008bb4e..08e62fb2e1 100644 --- a/src/extraction/languages/go.ts +++ b/src/extraction/languages/go.ts @@ -51,6 +51,40 @@ export function goEmbeddedTypeName(type: SyntaxNode | null | undefined, source: return GO_PREDECLARED_TYPES.has(getNodeText(type, source)) ? undefined : type; } +/** + * The name nodes of the types a Go alias declaration names, in source order: + * `Event` in `type Event = mvccpb.Event`, `List` and `Event` in `type Page = + * List[Event]`, each where it is written, so resolution reads a package + * qualifier back. The alias's own type parameters (`T` in `type Items[T any] + * = List[T]`) and Go's predeclared types are not types it names. A generic + * alias has no rule in tree-sitter-go 0.23: it parses as a `type_spec` whose + * `=` is an error, and reads as an alias here too. Undefined for a defined + * type (`type WatchChan <-chan Event`), which declares a type of its own. + */ +export function goAliasTypeNames(node: SyntaxNode, source: string): SyntaxNode[] | undefined { + const isAlias = node.type === 'type_alias' || + (node.type === 'type_spec' && node.children.some((c: SyntaxNode) => c.type === 'ERROR' && getNodeText(c, source).trim() === '=')); + const type = isAlias ? getChildByField(node, 'type') : null; + if (!type) return undefined; + const params = new Set(); + for (const decl of getChildByField(node, 'type_parameters')?.namedChildren ?? []) { + for (const c of decl.namedChildren) { + if (c.type === 'identifier') params.add(getNodeText(c, source)); + } + } + const names: SyntaxNode[] = []; + const walk = (n: SyntaxNode): void => { + if (n.type === 'type_identifier') { + const text = getNodeText(n, source); + if (!params.has(text) && !GO_PREDECLARED_TYPES.has(text)) names.push(n); + return; + } + for (const c of n.namedChildren) walk(c); + }; + walk(type); + return names; +} + export const goExtractor: LanguageExtractor = { functionTypes: ['function_declaration'], classTypes: [], // Go doesn't have classes @@ -79,7 +113,8 @@ export const goExtractor: LanguageExtractor = { return sig; }, resolveTypeAliasKind: (node, _source) => { - // Go type_spec: `type Foo struct { ... }` or `type Bar interface { ... }` + // Go type_spec: `type Foo struct { ... }` or `type Bar interface { ... }`, + // and an alias of a literal (`type Foo = struct { ... }`) alike. // The inner type is in the 'type' field of the type_spec node const typeChild = getChildByField(node, 'type'); if (!typeChild) return undefined; diff --git a/src/extraction/languages/typescript.ts b/src/extraction/languages/typescript.ts index c3bcc4d639..74ae1ef4f4 100644 --- a/src/extraction/languages/typescript.ts +++ b/src/extraction/languages/typescript.ts @@ -57,6 +57,43 @@ export function blankFlowSyntax(source: string, filePath?: string): string { .replace(/\.\.\.(?=\s*[},])/g, ' '); } +/** + * Whether `node` is declared in a `declare module 'x' { … }` or `declare + * global { … }` body, which exports it without an `export` keyword. + * + * This is TypeScript's own rule: the binder makes an ambient module body an + * export context unless the body holds an export declaration (`export {…}`, + * `export * from`, `export =`, `export default x`), and a namespace nested in + * one is ambient too. What such a body declares belongs to that module or to + * the global scope, not to this file (`interface Window`, chart.js's + * `PluginOptionsByType`): other files reach it through them, and so does the + * library whose type it extends. `declare namespace X` is not one: in a module + * file, X belongs to that file. Mirrored in the kernel (tsjs/mod.rs + * `is_ambient_export`). + */ +export function isAmbientExport(node: SyntaxNode): boolean { + for (let current = node.parent; current; current = current.parent) { + if (current.type === 'statement_block' && hasExportDeclaration(current)) return false; + if (current.type !== 'ambient_declaration') continue; + // `declare global` is the keyword and a bare block, with no field names. + for (let i = 0; i < current.childCount; i++) { + if (current.child(i)?.type === 'global') return true; + } + const declared = current.namedChild(0); + if (declared?.type === 'module' && getChildByField(declared, 'name')?.type === 'string') return true; + } + return false; +} + +/** A statement in `block` that exports by name or by assignment, not by declaring. */ +function hasExportDeclaration(block: SyntaxNode): boolean { + for (let i = 0; i < block.namedChildCount; i++) { + const statement = block.namedChild(i); + if (statement?.type === 'export_statement' && !getChildByField(statement, 'declaration')) return true; + } + return false; +} + export const typescriptExtractor: LanguageExtractor = { preParse: blankFlowSyntax, functionTypes: ['function_declaration', 'generator_function_declaration', 'arrow_function', 'function_expression', 'generator_function'], @@ -143,7 +180,7 @@ export const typescriptExtractor: LanguageExtractor = { if (current.type === 'export_statement') return true; current = current.parent; } - return false; + return isAmbientExport(node); }, isAsync: (node) => { for (let i = 0; i < node.childCount; i++) { diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 5866639146..2688d24cbc 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -25,8 +25,9 @@ import { isGeneratedFile, isMinifiedContent } from './generated-detection'; import type { LanguageExtractor, ExtractorContext } from './tree-sitter-types'; import { EXTRACTORS } from './languages'; import { stripCppTemplateArgs, isCppConstructorDeclaration } from './languages/c-cpp'; +import { NestedIntervals, scanCppBraceScopes, type CppBraceScopes } from './languages/cpp-brace-scopes'; import { rustImplTypeName } from './languages/rust'; -import { goEmbeddedTypeName } from './languages/go'; +import { goAliasTypeNames, goEmbeddedTypeName } from './languages/go'; import { dartMisparsedGenericCall, dartReceiverOf, isDartTypeName, pushDartTypeRefs } from './languages/dart'; import { LiquidExtractor } from './liquid-extractor'; import { RazorExtractor } from './razor-extractor'; @@ -85,6 +86,8 @@ const SIGNATURE_METHOD_NODE_TYPES = new Set(['method_signature']); * those would otherwise mint free functions (upstream #1862). */ const SIGNATURE_METHOD_LANGUAGES = new Set(['typescript', 'tsx', 'arkts', 'javascript', 'jsx']); +/** C++ specifiers that define a class-like type (or an enum) when they carry a body. */ +const CPP_CLASS_LIKE_SPECIFIERS = new Set(['class_specifier', 'struct_specifier', 'union_specifier', 'enum_specifier']); /** Vue store collections whose object-literal members are the symbols an agent * looks for. Extracted as function nodes so `actions`/`mutations`/`getters` are @@ -629,6 +632,13 @@ export class TreeSitterExtractor { // files, and a node per block would flood search with same-named symbols // (the #1093 crowd-out failure mode). Always empty outside C/C++. private namespacePrefix: string[] = []; + // A C++ file whose tree has errors is walked in the scopes its braces + // open (see visitInCppBraceScope): the scan of them, null for every other + // file, and the class-like nodes extracted so far by the braces of their + // bodies. + private cppBraceScopes: CppBraceScopes | null = null; + private cppClassScopes = new NestedIntervals(); + private cppClassScopeIds = new Set(); // C++ local function-pointer bindings, per enclosing symbol: // `auto kernel = &flash_fwd_kernel<…>;` recorded as callerId → kernel → // {flash_fwd_kernel}, so a later `kernel<<>>(params)` (or plain @@ -712,6 +722,9 @@ export class TreeSitterExtractor { if (['javascript', 'typescript', 'jsx', 'tsx'].includes(this.language)) { this.collectLaterExports(this.tree.rootNode); } + if (this.language === 'cpp' && this.tree.rootNode.hasError) { + this.cppBraceScopes = scanCppBraceScopes(this.source); + } // Create file node representing the source file const fileNode: Node = { @@ -1171,6 +1184,65 @@ export class TreeSitterExtractor { * Visit a node and extract information */ private visitNode(node: SyntaxNode): void { + if (this.cppBraceScopes) this.visitInCppBraceScope(node, this.cppBraceScopes); + else this.dispatchNode(node); + } + + /** + * Walk a node of a C++ file whose tree has errors in the namespaces and + * classes its source braces put it in. That is where the tree put it unless + * error recovery closed a scope at the wrong `}` or ran one past its own — + * the rest of a class then parses as declarations of the namespace around + * it (rocksdb's `struct Opts`), and the rest of each namespace one level out + * (protobuf's `FieldDescriptor` at the top level). See cpp-brace-scopes.ts. + * + * The namespaces apply everywhere. The enclosing classes are set only at + * declaration level, where the stack above the file node holds nothing but + * class scopes; inside a function (a local class's members) the walk's own + * stack stands. + */ + private visitInCppBraceScope(node: SyntaxNode, scopes: CppBraceScopes): void { + const at = node.startIndex; + const namespaces = this.namespacePrefix; + this.namespacePrefix = scopes.namespacesAt(at); + const stack = this.nodeStack; + let base = stack.length; + while (base > 1 && this.cppClassScopeIds.has(stack[base - 1]!)) base--; + let walked: string[] | null = null; + if (base === 1) { + const classes = this.cppClassScopes.at(at); + if (classes.length !== stack.length - base || classes.some((id, i) => stack[base + i] !== id)) { + walked = stack.splice(base); + stack.push(...classes); + } + } + this.dispatchNode(node); + if (walked) stack.splice(base, stack.length - base, ...walked); + this.namespacePrefix = namespaces; + } + + /** + * The textual extent of a C++ class-like body while the file is walked in + * brace scopes: where the node ends (the `}` that closes its body) to pass + * to createNode, or null to keep the tree's. + */ + private cppBodyEnd(body: SyntaxNode | null | undefined): Partial | null { + const close = body ? this.cppBraceScopes?.closeOf(body.startIndex) : undefined; + if (close === undefined) return null; + const end = this.cppBraceScopes!.positionOf(close + 1); + return { endLine: end.line, endColumn: end.column }; + } + + /** Open a class-like node's body as a scope for visitInCppBraceScope. */ + private openCppClassScope(node: Node, body: SyntaxNode | null | undefined): void { + if (!body) return; + const close = this.cppBraceScopes?.closeOf(body.startIndex); + if (close !== undefined && this.cppClassScopes.add(body.startIndex, close, node.id)) { + this.cppClassScopeIds.add(node.id); + } + } + + private dispatchNode(node: SyntaxNode): void { if (!this.extractor) return; const nodeType = node.type; @@ -1378,8 +1450,9 @@ export class TreeSitterExtractor { skipChildren = true; // extractEnum visits body children } // Check for type alias declarations (e.g. `type X = ...` in TypeScript) - // For Go, type_spec wraps struct/interface definitions — resolveTypeAliasKind - // detects these and extractTypeAlias creates the correct node kind. + // For Go, type_spec (and type_alias, `type A = B`) wraps struct/interface + // definitions — resolveTypeAliasKind detects these and extractTypeAlias + // creates the correct node kind. else if (this.extractor.typeAliasTypes.includes(nodeType)) { skipChildren = this.extractTypeAlias(node); } @@ -1424,6 +1497,14 @@ export class TreeSitterExtractor { this.extractor.variableTypes.includes(nodeType) && (!this.isInsideClassLikeNode() || this.isClassScopeConstantAssignment(node)) ) { + // In a file walked in brace scopes, a class the tree reads as the type + // of a declaration (`class X {…}` glued to the tokens after it by + // error recovery) is still a class. In a class body the children walk + // reached it; out of one, this branch skips that walk. + const declaredType = this.cppBraceScopes ? getChildByField(node, 'type') : null; + if (declaredType && CPP_CLASS_LIKE_SPECIFIERS.has(declaredType.type) && getChildByField(declaredType, 'body')) { + this.visitNode(declaredType); + } this.extractVariable(node); // extractVariable doesn't walk every initializer shape (object literals // are deliberately skipped; Python/Ruby don't walk at all), so scan the @@ -2154,8 +2235,10 @@ export class TreeSitterExtractor { docstring, visibility, isExported, + ...this.cppBodyEnd(resolvedBody), }); if (!classNode) return; + this.openCppClassScope(classNode, resolvedBody); // Extract extends/implements this.extractInheritance(node, classNode.id); @@ -2368,8 +2451,10 @@ export class TreeSitterExtractor { docstring, visibility, isExported, + ...this.cppBodyEnd(body), }); if (!aggregateNode) return; + this.openCppClassScope(aggregateNode, body); // Extract inheritance (e.g. Swift: struct HTTPMethod: RawRepresentable) this.extractInheritance(node, aggregateNode.id); @@ -3970,6 +4055,17 @@ export class TreeSitterExtractor { // entry's string-literal name as a searchable member (issue #634). this.extractTsTupleContractNames(value, typeAliasNode); } + } else if (this.language === 'go') { + // Go's `type Event = mvccpb.Event` names its type in the `type` field. + for (const type of goAliasTypeNames(node, this.source) ?? []) { + this.unresolvedReferences.push({ + fromNodeId: typeAliasNode.id, + referenceName: getNodeText(type, this.source), + referenceKind: 'references', + line: type.startPosition.row + 1, + column: type.startPosition.column, + }); + } } } return false; diff --git a/src/graph/dead-code.ts b/src/graph/dead-code.ts index 5dce8f6db1..d712a00009 100644 --- a/src/graph/dead-code.ts +++ b/src/graph/dead-code.ts @@ -17,7 +17,12 @@ * candidate is dropped from the list the moment there is any reason to believe * something outside the graph reaches it: * - * - it is **exported** (something outside this repository may import it); + * - it is **exported** (something outside this repository may import it). That + * includes whatever a TypeScript `declare module 'x'` or `declare global` + * block declares, which TypeScript exports without the keyword: `interface + * Window` or chart.js's `PluginOptionsByType` merges into a type declared + * outside the index, and the runtime or the library reads it through that + * type, so nothing in the repository names it; * - it lives in a **test** or a **generated** file (not code anyone deletes by * hand); * - it is **abstract** or declared on an interface (a declaration is dispatched @@ -25,11 +30,18 @@ * - it is **decorated** (`@app.route`, `@Component`, `@EventHandler`) — a * decorator is a registration, and the framework that reads it is not in the * graph. Seen as the symbol's own outgoing `decorates` edge, which is where - * the engine records it; `node.decorators` is only populated by a couple of - * languages and is checked as well rather than instead; + * the engine records it, or, for a decorator imported from outside the index + * (`@HostListener`, `@Cron`), as the resolver's unresolved `decorates` row — + * the same record the ancestor rule below reads for a supertype it could not + * follow. `node.decorators` is only populated by a couple of languages and is + * checked as well rather than instead; * - it **overrides** a member an ancestor declares (calls land on the ancestor; * see {@link overrideCandidates} for why an ancestor we cannot read counts * the same way); + * - it is a **hook its framework calls by name**: `ngOnInit` on a component + * that never writes `implements OnInit`. Which names are hooks is not kept in + * a table here — the index already says it, in what the classes that DO + * write the interface out declare (see {@link byNameHookCandidates}); * - it has a name the language calls by itself (`constructor`, `__enter__`, * `main`); * - it sits in a **vendored** directory (`vendor/`, `third_party/`, @@ -90,7 +102,7 @@ import fs from 'fs'; import path from 'path'; import type CodeGraph from '../index'; -import type { Node, NodeKind } from '../types'; +import type { EdgeKind, Node, NodeKind } from '../types'; import { isTestFile } from '../search/query-utils'; // ============================================================================= @@ -184,6 +196,18 @@ const OVERRIDABLE_KINDS: ReadonlySet = new Set([ 'field', ]); +/** + * Languages whose `implements` clause is erased before the program runs, so a + * framework that calls an interface's member can only be finding it by name — + * the premise of {@link byNameHookCandidates}. Java, C#, Kotlin and the rest + * check the interface itself: Spring never calls `afterPropertiesSet` on a + * bean that merely has a method of that name. + */ +const BY_NAME_DISPATCH_LANGUAGES: ReadonlySet = new Set(['typescript', 'tsx']); + +/** Member kinds a framework calls as a hook. */ +const HOOK_KINDS: readonly NodeKind[] = ['method', 'function']; + /** * Names a language or a runtime calls without anything in the source naming * them. @@ -193,7 +217,8 @@ const OVERRIDABLE_KINDS: ReadonlySet = new Set([ * stale, and it would hide real dead code behind a name coincidence. The * entries below are the ones where the *language itself* does the calling, so * no source file could name them even in principle. Framework hooks are caught - * by the decorator and override rules instead, which are structural. + * by the decorator, override and by-name hook rules instead, which are + * structural. */ const IMPLICIT_ENTRY_NAMES: ReadonlySet = new Set([ 'constructor', @@ -333,7 +358,11 @@ export interface DeadCodeExclusions { tests: number; /** In a tool-generated file. */ generated: number; - /** Exported, or declared in a header — reachable from outside this index. */ + /** + * Exported, or declared in a header — reachable from outside this index. A + * TypeScript `declare module` / `declare global` declaration is exported + * implicitly: it merges into a library's or the global scope's type. + */ exported: number; /** * In a language this index records no export marker for, so nothing here can @@ -346,6 +375,12 @@ export interface DeadCodeExclusions { decorated: number; /** Overrides a member an ancestor declares, or an ancestor we cannot read. */ overriding: number; + /** + * A hook its framework calls by name, on a class that skips the interface: + * the class carries a decorator, and the name is one an interface outside + * the index carries, going by the classes that write it out. + */ + hooks: number; /** Named something the language calls by itself. */ implicit: number; /** In a vendored directory — carried code, reached by something outside the index. */ @@ -437,6 +472,7 @@ export function buildDeadCodeReport(cg: CodeGraph, query: DeadCodeQuery = {}): D declarations: 0, decorated: 0, overriding: 0, + hooks: 0, implicit: 0, vendored: 0, testScope: 0, @@ -508,14 +544,7 @@ export function buildDeadCodeReport(cg: CodeGraph, query: DeadCodeQuery = {}): D // A `decorates` edge runs FROM the decorated symbol to the decorator, so // this is an outgoing-edge question, not something the candidate query could // have answered. - const decorated = new Set( - cg - .getOutgoingEdgesFrom( - surviving.map((row) => row.node.id), - ['decorates'] - ) - .map((edge) => edge.source) - ); + const decorated = carriesDecorator(cg, surviving.map((row) => row.node)); const containers = containersOf(cg, surviving.map((row) => row.node)); const overriding = overrideCandidates(cg, surviving.map((row) => row.node), containers); // One batched count for every file still in play. A file nothing reaches is @@ -532,10 +561,19 @@ export function buildDeadCodeReport(cg: CodeGraph, query: DeadCodeQuery = {}): D const names = surviving.map((row) => row.node.name); const unresolved = cg.getUnresolvedNamesAmong(names); const ambiguousNames = cg.getAmbiguousReferencedNames(names); + // Asked only about the members no earlier rule in the loop below drops. + const hooks = byNameHookCandidates( + cg, + surviving + .map((row) => row.node) + .filter((node) => !decorated.has(node.id) && !overriding.has(node.id)), + containers, + candidates.map((row) => row.node) + ); const kept: Array<{ node: Node; generated: boolean }> = []; for (const row of surviving) { - if (decorated.has(row.node.id) || (row.node.decorators?.length ?? 0) > 0) { + if (decorated.has(row.node.id)) { excluded.decorated += 1; continue; } @@ -548,6 +586,10 @@ export function buildDeadCodeReport(cg: CodeGraph, query: DeadCodeQuery = {}): D excluded.overriding += 1; continue; } + if (hooks.has(row.node.id)) { + excluded.hooks += 1; + continue; + } if (unresolved.has(row.node.name)) { excluded.unresolvedName += 1; continue; @@ -814,44 +856,8 @@ function overrideCandidates( const containerIds = [...new Set([...containers.values()].map((node) => node.id))]; if (containerIds.length === 0) return dropped; - // Level-by-level upward walk over EVERY container at once: one query per - // level rather than one per container. `reach` maps an ancestor back to the - // containers it is an ancestor of. - const reach = new Map>(); - const seen = new Map(containerIds.map((id) => [id, new Set([id])])); - let frontier = containerIds.map((id) => ({ id, roots: new Set([id]) })); - - for (let depth = 0; depth < MAX_OVERRIDE_ANCESTOR_DEPTH && frontier.length > 0; depth++) { - const rootsOf = new Map(frontier.map((item) => [item.id, item.roots])); - const edges = cg.getOutgoingEdgesFrom( - frontier.map((item) => item.id), - ['extends', 'implements'] - ); - const next = new Map>(); - for (const edge of edges) { - if (edge.target === edge.source) continue; - const roots = rootsOf.get(edge.source); - if (!roots) continue; - const merged = next.get(edge.target) ?? new Set(); - const visited = seen.get(edge.target) ?? new Set(); - const known = reach.get(edge.target) ?? new Set(); - // An ancestor may already be a candidate root. Only skip pairs we have - // propagated, so later descendants still reach all of its ancestors. - for (const root of roots) { - if (visited.has(root)) continue; - visited.add(root); - merged.add(root); - known.add(root); - } - seen.set(edge.target, visited); - if (merged.size > 0) next.set(edge.target, merged); - if (known.size > 0) reach.set(edge.target, known); - } - frontier = []; - for (const [id, roots] of next) { - frontier.push({ id, roots }); - } - } + // `reach` maps an ancestor back to the containers it is an ancestor of. + const reach = ancestorsOf(cg, containerIds, ['extends', 'implements']); const opaqueContainers = new Set(); const externalBased = cg.getUnresolvedSupertypeSourcesAmong([...containerIds, ...reach.keys()]); @@ -901,3 +907,208 @@ function overrideCandidates( } return dropped; } + +/** + * Every ancestor of `ids` along `kinds` edges, mapped back to the ids it is an + * ancestor of. A level-by-level upward walk over EVERY id at once — one query + * per level rather than one per id — up to {@link MAX_OVERRIDE_ANCESTOR_DEPTH}. + */ +function ancestorsOf( + cg: CodeGraph, + ids: readonly string[], + kinds: EdgeKind[] +): Map> { + const reach = new Map>(); + const seen = new Map(ids.map((id) => [id, new Set([id])])); + let frontier = ids.map((id) => ({ id, roots: new Set([id]) })); + + for (let depth = 0; depth < MAX_OVERRIDE_ANCESTOR_DEPTH && frontier.length > 0; depth++) { + const rootsOf = new Map(frontier.map((item) => [item.id, item.roots])); + const edges = cg.getOutgoingEdgesFrom( + frontier.map((item) => item.id), + kinds + ); + const next = new Map>(); + for (const edge of edges) { + if (edge.target === edge.source) continue; + const roots = rootsOf.get(edge.source); + if (!roots) continue; + const merged = next.get(edge.target) ?? new Set(); + const visited = seen.get(edge.target) ?? new Set(); + const known = reach.get(edge.target) ?? new Set(); + // An ancestor may already be a root. Only skip pairs we have propagated, + // so later descendants still reach all of its ancestors. + for (const root of roots) { + if (visited.has(root)) continue; + visited.add(root); + merged.add(root); + known.add(root); + } + seen.set(edge.target, visited); + if (merged.size > 0) next.set(edge.target, merged); + if (known.size > 0) reach.set(edge.target, known); + } + frontier = []; + for (const [id, roots] of next) { + frontier.push({ id, roots }); + } + } + return reach; +} + +/** + * Which of `nodes` carry a decorator: their own outgoing `decorates` edge, an + * unresolved `decorates` row (the only trace a decorator imported from outside + * the index, like `@HostListener` or `@Cron`, leaves), or the `decorators` + * list a couple of languages record on the node itself. + */ +function carriesDecorator(cg: CodeGraph, nodes: readonly Node[]): Set { + const ids = nodes.map((node) => node.id); + const found = new Set(cg.getOutgoingEdgesFrom(ids, ['decorates']).map((edge) => edge.source)); + for (const id of cg.getUnresolvedReferenceNamesFrom(ids, ['decorates']).keys()) found.add(id); + for (const node of nodes) if ((node.decorators?.length ?? 0) > 0) found.add(node.id); + return found; +} + +/** + * Which members are hooks their framework calls by name — the ids to drop. + * + * Angular calls `ngOnInit` on every component it creates, and NestJS calls + * `onModuleInit` on every provider, whether or not the class writes + * `implements OnInit`: TypeScript erases the clause, so the framework can only + * be finding the method by its name. A class that writes the clause is already + * off the list, because its ancestor is outside the index + * ({@link overrideCandidates}). One that skips it is tied to the framework by + * nothing but the name. + * + * Which names are hooks is not a table kept here; a table would go stale and + * hide real dead code behind a name coincidence. The index says it already: + * what `OnInit` carries is what the classes that DO write `implements OnInit` + * have. A method is dropped when all of this holds: + * + * - **its class carries a decorator**, so a framework creates it + * (`@Component`, `@Injectable`). Nothing creates an undecorated class, so + * nothing calls its hooks. And the class's decorator says nothing about its + * other methods: an unused helper on a component stays on the list; + * - **an interface outside the index carries the name**. Every TypeScript class + * that writes the interface into an `implements` clause has a method of that + * name, declared or inherited from an ancestor that declares it, which is + * what TypeScript itself checks; a class that inherits from outside the index + * may have it from there, so it is no evidence either way. At least two of + * them, and more than half, declare it themselves: one class's methods are + * that class, not a contract, and a name that only a few declare while the + * rest are unreadable is not evidence of one either; + * - **more than half of those classes carry a decorator** as well: the + * interface is one that framework-created classes fill, not a library type + * whose method happens to share the name. + * + * TypeScript only — see {@link BY_NAME_DISPATCH_LANGUAGES}. + */ +function byNameHookCandidates( + cg: CodeGraph, + nodes: readonly Node[], + containers: ReadonlyMap, + unreferenced: readonly Node[] +): Set { + const dropped = new Set(); + const isHookShaped = (node: Node): boolean => + HOOK_KINDS.includes(node.kind) && BY_NAME_DISPATCH_LANGUAGES.has(node.language); + const methods = nodes.filter( + (node) => isHookShaped(node) && containers.get(node.id)?.kind === 'class' + ); + if (methods.length === 0) return dropped; + + const classes = new Map(); + for (const node of methods) { + const container = containers.get(node.id); + if (container) classes.set(container.id, container); + } + const registered = carriesDecorator(cg, [...classes.values()]); + const eligible = methods.filter((node) => registered.has(containers.get(node.id)?.id ?? '')); + if (eligible.length === 0) return dropped; + + // Every class declaring a method of each name. A hook is called by its + // framework and never by the code in the index, so each class's own + // declaration of it is unreferenced too: it is in `unreferenced` already, and + // the index need not be searched by name. (A name with a declaration that + // something does call is the ambiguous-name rule's, which drops every + // candidate of that name.) + const names = new Set(eligible.map((node) => node.name)); + const declarations = unreferenced.filter((node) => names.has(node.name) && isHookShaped(node)); + const declaredIn = containersOf( + cg, + declarations.filter((node) => !containers.has(node.id)) + ); + const declaring = new Map>(); + for (const node of declarations) { + const container = containers.get(node.id) ?? declaredIn.get(node.id); + if (container?.kind !== 'class') continue; + const ids = declaring.get(node.name); + if (ids) ids.add(container.id); + else declaring.set(node.name, new Set([container.id])); + } + + // The interfaces outside the index each of those classes writes out. + const declarers = new Set([...declaring.values()].flatMap((ids) => [...ids])); + const interfacesOf = cg.getUnresolvedReferenceNamesFrom(declarers, ['implements']); + const interfaces = new Set([...interfacesOf.values()].flat()); + if (interfaces.size === 0) return dropped; + + // Every TypeScript class writing each of those interfaces out, kept where + // more than half of them are framework-created. + const writers = cg.getUnresolvedReferenceSourcesNamed(interfaces, ['implements']); + const writerIds = [...new Set([...writers.values()].flatMap((ids) => [...ids]))]; + const typescript = [...cg.getNodesByIds(writerIds).values()].filter((node) => + BY_NAME_DISPATCH_LANGUAGES.has(node.language) + ); + const typescriptIds = new Set(typescript.map((node) => node.id)); + const registeredWriters = carriesDecorator(cg, typescript); + const implementers = new Map(); + for (const [name, ids] of writers) { + const own = [...ids].filter((id) => typescriptIds.has(id)); + const registeredCount = own.filter((id) => registeredWriters.has(id)).length; + if (registeredCount > own.length / 2) implementers.set(name, own); + } + if (implementers.size === 0) return dropped; + + // What each implementer inherits from, and which inherit from outside the + // index somewhere up the chain. + const implementerIds = [...new Set([...implementers.values()].flat())]; + const ancestors = ancestorsOf(cg, implementerIds, ['extends']); + const inherits = new Map(); + for (const [ancestor, roots] of ancestors) { + for (const root of roots) { + const bucket = inherits.get(root); + if (bucket) bucket.push(ancestor); + else inherits.set(root, [ancestor]); + } + } + const outside = cg.getUnresolvedReferenceNamesFrom([...implementerIds, ...ancestors.keys()], ['extends']); + const opaque = new Set(implementerIds.filter((id) => outside.has(id))); + for (const [ancestor, roots] of ancestors) { + if (outside.has(ancestor)) for (const root of roots) opaque.add(root); + } + + const hookNames = new Set(); + for (const name of names) { + const declared = declaring.get(name); + if (!declared) continue; + const has = (id: string): boolean => + declared.has(id) || + opaque.has(id) || + (inherits.get(id) ?? []).some((ancestor) => declared.has(ancestor)); + const contracts = new Set([...declared].flatMap((id) => interfacesOf.get(id) ?? [])); + for (const contract of contracts) { + const own = implementers.get(contract); + if (!own) continue; + const declaredCount = own.filter((id) => declared.has(id)).length; + if (declaredCount >= 2 && declaredCount > own.length / 2 && own.every(has)) { + hookNames.add(name); + break; + } + } + } + + for (const node of eligible) if (hookNames.has(node.name)) dropped.add(node.id); + return dropped; +} diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts index 899eb9a4a3..2e4877d654 100644 --- a/src/mcp/tools.ts +++ b/src/mcp/tools.ts @@ -3742,6 +3742,14 @@ export class ToolHandler { registeredAt, }; } + if (m?.synthesizedBy === 'interface-impl' && typeof m.promotedInto === 'string') { + // Go: the implementing struct gets this method from a type it embeds. + return { + label: `interface dispatch — runs the method \`${m.promotedInto}\` gets by embedding (dynamic dispatch)`, + compact: `dynamic: interface → method promoted into ${m.promotedInto}${at}`, + registeredAt, + }; + } if (m?.synthesizedBy === 'interface-impl') { return { label: `interface/abstract dispatch — runs the implementation override (dynamic dispatch)`, diff --git a/src/resolution/callback-synthesizer.ts b/src/resolution/callback-synthesizer.ts index e720737115..3b6cc4f532 100644 --- a/src/resolution/callback-synthesizer.ts +++ b/src/resolution/callback-synthesizer.ts @@ -1095,61 +1095,162 @@ const IFACE_OVERRIDE_LANGS = new Set([ /** Depth cap for transitive-supertype BFS in interfaceOverrideEdges — real * extends/implements chains are ≤~6; the cap is a graph-corruption backstop. */ const MAX_SUPERTYPE_DEPTH = 8; +/** + * Whether a supertype edge out of a Go type is one of its embeddings (#2397): + * declared, never synthesized (go-implements writes those), into a struct, an + * interface or a defined type, and out of an interface only into an interface. + * `kindOf` answers those three kinds for a Go node id, null for anything else. + */ +function isGoEmbedding(e: Edge, kindOf: (id: string) => NodeKind | null): boolean { + if (e.provenance === 'heuristic') return false; + const to = kindOf(e.target); + return to !== null && (kindOf(e.source) !== 'interface' || to === 'interface'); +} /** * Go implicit interface satisfaction (#584). Go has no `implements` keyword — a - * struct satisfies an interface structurally when its method set covers the - * interface's. Synthesize the missing `implements` edge (struct → interface) by + * type satisfies an interface structurally when its method set covers the + * interface's. Synthesize the missing `implements` edge (type → interface) by * matching method-NAME sets, so impl-navigation works and the interface-dispatch * bridge ({@link interfaceOverrideEdges}, now 'go'-enabled) can link an interface - * method call to the concrete overrides. + * method call to the concrete overrides, and to the methods embedding promotes. + * + * The implementers are structs and defined types. A defined type declares + * methods as a struct does — gin's `type formSource map[string][]string` has + * `TrySet`, prometheus's `type staticDiscoverer []*targetgroup.Group` has `Run`, + * an adapter `type HandlerFunc func(…)` has `ServeHTTP` — and is extracted as a + * `type_alias` that owns them through `contains` edges. Go gives it none of + * the methods declared on the type it is written over, so it is matched by + * the methods it declares. A true alias (`type A = B`) is B, not a type of its + * own: its `type_alias` owns only methods written with it as the receiver + * (`func (c *KumaSDConfig) Name()`), which are B's, and stands in for B with + * those. One without any is no implementer. + * + * Both method sets include what embedding brings in, read off the declared + * `extends`/`implements` edge each embedded type is. An interface has the + * methods of the interfaces it embeds: etcd's `AuthReadTx` is `RLock` and + * `RUnlock` plus all of `UnsafeAuthReader`, so an `RWMutex` is not one. A struct + * has the methods promoted from the structs, interfaces and defined types it + * embeds, at any depth: counting embedded interfaces alone would drop every + * struct that satisfies one through promoted methods. Synthesized edges are + * never followed (this pass writes them), and an embedding that resolved to + * nothing (`io.Closer`) adds nothing. * - * Name-only matching (signatures ignored) — over-approximation accepted, in line - * with the other dispatch synthesizers; capped per interface. Empty interfaces - * (`any`) are skipped so they don't match every struct. + * Name-only matching (signatures ignored, and a promoted name counted even + * where Go finds it ambiguous or hidden by a field) — over-approximation + * accepted, in line with the other dispatch synthesizers; capped per + * interface. Empty interfaces (`any`) are skipped so they don't match every + * type, and a struct that embeds the interface keeps the edge its embedding + * already is. */ async function goImplementsEdges(queries: QueryBuilder, onYield: MaybeYield): Promise { let scanned255 = 0; const edges: Edge[] = []; const seen = new Set(); - const methodNameSet = (id: string): Set => - new Set( - queries - .getOutgoingEdges(id, ['contains']) - .map((e) => queries.getNodeById(e.target)) - .filter((n): n is Node => !!n && n.kind === 'method') - .map((n) => n.name), - ); - - // Materializes GO structs only (the pass is language-gated by the caller), - // never the whole struct kind — that array is O(nodes) on struct-heavy - // repos like the Linux kernel (#1212). - const goStructs: Node[] = []; - for (const s of queries.iterateNodesByKind('struct', { languages: ['go'] })) { + // Materializes GO types only (the pass is language-gated by the caller), + // never the whole struct kind — that array is O(nodes) on struct-heavy repos + // like the Linux kernel (#1212). Structs and defined types arrive + // interleaved in one canonical order, so the cap below takes implementers + // as the files declare them, whatever their kind. + const goImplementers: Node[] = []; + for (const n of queries.iterateNodesByKindIn(['struct', 'type_alias'], ['go'])) { if ((++scanned255 & 63) === 0) await onYield(); - goStructs.push(s); + goImplementers.push(n); } - const structMethods = new Map>(); - for (const s of goStructs) structMethods.set(s.id, methodNameSet(s.id)); + const goInterfaces: Node[] = []; + for (const i of queries.iterateNodesByKindIn('interface', ['go'])) { + if ((++scanned255 & 63) === 0) await onYield(); + goInterfaces.push(i); + } + + // The kinds of type whose methods embedding passes on: a struct can embed a + // defined type (`type HandlersChain []HandlerFunc`) as well. + const typeKind = new Map(); + for (const n of goImplementers) typeKind.set(n.id, n.kind); + for (const i of goInterfaces) typeKind.set(i.id, 'interface'); + const kindOf = (id: string): NodeKind | null => typeKind.get(id) ?? null; + + // Memoized: an embedded base is read once, however many types embed it. + const ownMemo = new Map>(); + const ownMethods = (id: string): Set => { + let names = ownMemo.get(id); + if (!names) { + names = new Set( + queries + .getOutgoingEdges(id, ['contains']) + .map((e) => queries.getNodeById(e.target)) + .filter((n): n is Node => !!n && n.kind === 'method') + .map((n) => n.name), + ); + ownMemo.set(id, names); + } + return names; + }; + + // The types one embeds: an interface embeds interfaces, a struct any of the + // three. Every type's are read up front in a few batched queries rather than + // one query per type. + const isEmbedding = (e: Edge): boolean => isGoEmbedding(e, kindOf); + const NO_EMBEDS: string[] = []; + const embedMemo = new Map(); + const typeIds = [...typeKind.keys()]; + for (let i = 0; i < typeIds.length; i += 2000) { + for (const e of queries.getOutgoingEdgesFrom(typeIds.slice(i, i + 2000), ['extends', 'implements'])) { + if (!isEmbedding(e)) continue; + const targets = embedMemo.get(e.source); + if (targets) targets.push(e.target); + else embedMemo.set(e.source, [e.target]); + } + await onYield(); + } + const embeds = (id: string): string[] => embedMemo.get(id) ?? NO_EMBEDS; + + // Own methods plus every embedded type's, down to the last level. Go allows + // a struct to embed a pointer to itself, or to one embedding it back. + const methodSet = (id: string): Set => { + if (embeds(id).length === 0) return ownMethods(id); + const names = new Set(); + const reached = new Set([id]); + const pending = [id]; + while (pending.length > 0) { + const at = pending.pop()!; + for (const m of ownMethods(at)) names.add(m); + for (const t of embeds(at)) { + if (reached.has(t)) continue; + reached.add(t); + pending.push(t); + } + } + return names; + }; - for (const iface of queries.iterateNodesByKind('interface', { languages: ['go'] })) { + const covers = (have: Set, want: Set): boolean => { + if (have.size < want.size) return false; + for (const m of want) { + if (!have.has(m)) return false; + } + return true; + }; + + // A type without a method satisfies no interface this pass looks at. + const candidates: Node[] = []; + const candidateMethods = new Map>(); + for (const s of goImplementers) { if ((++scanned255 & 63) === 0) await onYield(); + const have = methodSet(s.id); + if (have.size === 0) continue; + candidates.push(s); + candidateMethods.set(s.id, have); + } + for (const iface of goInterfaces) { if ((++scanned255 & 63) === 0) await onYield(); - const want = methodNameSet(iface.id); + const want = methodSet(iface.id); if (want.size === 0) continue; // empty interface (`any`) — would match everything let added = 0; - for (const s of goStructs) { - if (added >= MAX_CALLBACKS_PER_CHANNEL) break; - const have = structMethods.get(s.id); - if (!have || have.size < want.size) continue; - let all = true; - for (const m of want) { - if (!have.has(m)) { all = false; break; } - } - if (!all) continue; + const link = (s: Node): void => { const key = `${s.id}>${iface.id}`; - if (seen.has(key)) continue; + if (seen.has(key)) return; seen.add(key); edges.push({ source: s.id, @@ -1160,6 +1261,23 @@ async function goImplementsEdges(queries: QueryBuilder, onYield: MaybeYield): Pr metadata: { synthesizedBy: 'go-implements', via: iface.name, registeredAt: `${s.filePath}:${s.startLine}` }, }); added++; + }; + // A struct that needs promoted methods waits for the types declaring all + // of them, every defined type among them: under the cap, those are what + // the interface-dispatch bridge links a call through the interface to. + const throughEmbedding: Node[] = []; + for (const s of candidates) { + if (added >= MAX_CALLBACKS_PER_CHANNEL) break; + const have = candidateMethods.get(s.id)!; + if (!covers(have, want)) continue; + if (embeds(s.id).includes(iface.id)) continue; // declared by embedding it + const own = ownMethods(s.id); + if (own !== have && !covers(own, want)) throughEmbedding.push(s); + else link(s); + } + for (const s of throughEmbedding) { + if (added >= MAX_CALLBACKS_PER_CHANNEL) break; + link(s); } } return edges; @@ -1307,6 +1425,118 @@ async function kotlinExpectActualEdges(queries: QueryBuilder, onYield: MaybeYiel return edges; } +/** The methods a Go struct runs for a name it does not declare, and the line of the embedding they come through. */ +interface GoPromotion { + methods: Node[]; + line: number; +} + +/** + * Which embedded type's method a Go struct runs for a name it does not + * declare, picked the way Go's selector picks it: breadth-first through the + * struct's embeddings, the shallowest depth holding the name wins, and the + * name selects nothing when it occurs more than once at that depth, one type + * reached along two paths included. A type met at a shallower depth is not + * walked again. An embedded interface brings its whole method set at its own + * depth, and a method it provides is a dynamic call once more, so that is no + * promotion either. Struct fields are not in the graph: a field that hides a + * promoted method is not seen. + */ +function goPromotions( + queries: QueryBuilder, + methodsOf: (id: string) => Node[] +): (structId: string, name: string) => GoPromotion | null { + const kinds = new Map(); + const kindOf = (id: string): NodeKind | null => { + let kind = kinds.get(id); + if (kind === undefined) { + const n = queries.getNodeById(id); + kind = n?.language === 'go' && (n.kind === 'struct' || n.kind === 'interface' || n.kind === 'type_alias') + ? n.kind + : null; + kinds.set(id, kind); + } + return kind; + }; + const embedMemo = new Map(); + const embeds = (id: string): Edge[] => { + let out = embedMemo.get(id); + if (!out) { + const targets = new Set(); + out = queries.getOutgoingEdges(id, ['extends', 'implements']).filter((e) => { + if (!isGoEmbedding(e, kindOf) || targets.has(e.target)) return false; + targets.add(e.target); + return true; + }); + embedMemo.set(id, out); + } + return out; + }; + const ifaceMemo = new Map>(); + const ifaceMethods = (id: string): Set => { + let names = ifaceMemo.get(id); + if (!names) { + names = new Set(); + const reached = new Set([id]); + const pending = [id]; + while (pending.length > 0) { + const at = pending.pop()!; + for (const m of methodsOf(at)) names.add(m.name); + for (const e of embeds(at)) { + if (reached.has(e.target)) continue; + reached.add(e.target); + pending.push(e.target); + } + } + ifaceMemo.set(id, names); + } + return names; + }; + const provides = (id: string, name: string): boolean => + kindOf(id) === 'interface' ? ifaceMethods(id).has(name) : methodsOf(id).some((m) => m.name === name); + + const memo = new Map(); + return (structId, name) => { + const key = `${structId}>${name}`; + const hit = memo.get(key); + if (hit !== undefined) return hit; + let found: GoPromotion | null = null; + // Each type at the current depth, with how many paths reach it and the + // line of the struct's own embedding the first of them starts with. + let level = new Map([[structId, { paths: 1, line: 0 }]]); + const met = new Set([structId]); + for (let depth = 0; level.size > 0; depth++) { + const next = new Map(); + for (const [id, at] of level) { + if (kindOf(id) === 'interface') continue; // its embeddings are its method set + for (const e of embeds(id)) { + if (met.has(e.target)) continue; + const to = next.get(e.target); + if (to) to.paths += at.paths; + else next.set(e.target, { paths: at.paths, line: depth === 0 ? (e.line ?? 0) : at.line }); + } + } + let paths = 0; + let provider: string | undefined; + for (const [id, at] of next) { + met.add(id); + if (!provides(id, name)) continue; + paths += at.paths; + provider = id; + } + if (provider !== undefined) { + if (paths === 1 && kindOf(provider) !== 'interface') { + found = { methods: methodsOf(provider).filter((m) => m.name === name), line: next.get(provider)!.line }; + } + break; + } + level = next; + } + memo.set(key, found); + return found; + }; +} + async function interfaceOverrideEdges(queries: QueryBuilder, onYield: MaybeYield): Promise { let scanned255 = 0; const edges: Edge[] = []; @@ -1344,12 +1574,26 @@ async function interfaceOverrideEdges(queries: QueryBuilder, onYield: MaybeYield protocolMemo.set(base.id, methods); return methods; }; + // A Go struct also satisfies an interface with the methods its embedded + // types promote into it (goImplementsEdges counts them), and a call through + // the interface then runs the embedded type's method. Those links are made + // after the loop, from what the struct's cap has left, so every override + // keeps its edge, and a provider that implements the interface itself keeps + // its own. + const goPromotion = goPromotions(queries, methodsOf); + const promoted: { cls: Node; methods: Node[]; left: number }[] = []; // Concrete-side kinds vary by language: `class` covers Java / Kotlin / // C# / TS / Swift-classes / Scala-classes; `struct` covers Swift value - // types that conform to protocols. Iterate both. + // types that conform to protocols. Iterate both. A Go defined type + // (`type HandlerFunc func(…)`) holds its methods as a struct does, as a + // `type_alias`; that kind is walked for Go alone. const concreteKinds = ['class', 'struct', 'union'] as const; - for (const kind of concreteKinds) { - for (const cls of queries.iterateNodesByKind(kind)) { + const concrete = [ + ...concreteKinds.map((kind) => () => queries.iterateNodesByKind(kind)), + () => queries.iterateNodesByKindIn('type_alias', ['go']), + ]; + for (const nodesOfKind of concrete) { + for (const cls of nodesOfKind()) { if ((++scanned255 & 63) === 0) await onYield(); // A class can only emit here if it HAS a supertype edge — check that // (one edge query) before materializing its methods: most classes in a @@ -1360,7 +1604,8 @@ async function interfaceOverrideEdges(queries: QueryBuilder, onYield: MaybeYield IFACE_OVERRIDE_LANGS.has(n.language) && !(['javascript', 'typescript'].includes(n.language) && n.name === 'constructor') ); - if (implMethods.length === 0) continue; + const goStruct = cls.language === 'go' && (cls.kind === 'struct' || cls.kind === 'type_alias'); + if (implMethods.length === 0 && !goStruct) continue; // Group impl methods by name to handle OVERLOADS: an interface `list()` and // `list(params)` are distinct nodes and a call may resolve to either, so // link every base overload → every same-name impl overload (keying by name @@ -1391,10 +1636,18 @@ async function interfaceOverrideEdges(queries: QueryBuilder, onYield: MaybeYield } } for (const base of supertypes) { + const promotes = goStruct && base.kind === 'interface' && + sups.some((edge) => edge.target === base.id && edge.provenance === 'heuristic'); let added = 0; + const unmatched: Node[] = []; for (const bm of baseMethodsOf(base)) { if (added >= MAX_CALLBACKS_PER_CHANNEL) break; - for (const m of implByName.get(bm.name) ?? []) { + const impls = implByName.get(bm.name); + if (!impls) { + if (promotes) unmatched.push(bm); + continue; + } + for (const m of impls) { if (added >= MAX_CALLBACKS_PER_CHANNEL) break; if (bm.id === m.id) continue; const key = `${bm.id}>${m.id}`; @@ -1411,9 +1664,48 @@ async function interfaceOverrideEdges(queries: QueryBuilder, onYield: MaybeYield added++; } } + if (unmatched.length > 0 && added < MAX_CALLBACKS_PER_CHANNEL) { + promoted.push({ cls, methods: unmatched, left: MAX_CALLBACKS_PER_CHANNEL - added }); + } } } } + // The wiring site of a promoted method is the struct's embedding it comes + // through: `type blockSeriesSet struct{ blockBaseSeriesSet }`. + promoted.sort((a, b) => + (a.cls.filePath < b.cls.filePath ? -1 : a.cls.filePath > b.cls.filePath ? 1 : 0) + || a.cls.startLine - b.cls.startLine + || (a.cls.id < b.cls.id ? -1 : a.cls.id > b.cls.id ? 1 : 0), + ); + for (const { cls, methods, left } of promoted) { + if ((++scanned255 & 63) === 0) await onYield(); + let budget = left; + for (const bm of methods) { + if (budget <= 0) break; + const promotion = goPromotion(cls.id, bm.name); + if (!promotion) continue; + for (const m of promotion.methods) { + if (budget <= 0) break; + const key = `${bm.id}>${m.id}`; + if (seen.has(key)) continue; + seen.add(key); + edges.push({ + source: bm.id, + target: m.id, + kind: 'calls', + line: bm.startLine, + provenance: 'heuristic', + metadata: { + synthesizedBy: 'interface-impl', + via: m.name, + promotedInto: cls.name, + registeredAt: `${cls.filePath}:${promotion.line || cls.startLine}`, + }, + }); + budget--; + } + } + } return edges; } @@ -1662,13 +1954,28 @@ async function reactJsxChildEdges(ctx: ResolutionContext, onYield: MaybeYield): let imports: Map | null = null; const importsOf = () => (imports ??= importedFrom(ctx, file, parents[0]!.language)); + // A function renders the tags its own lines hold, so functions that span + // the same lines share them: every function of a minified bundle spans + // its one line, and reading that line again per function took 9 to 25 s + // over go-ethereum's graphiql.min.js (2,234 functions on 980 KB). The + // file is split once, not once per function. + let lines: string[] | null = null; + const tagsBySpan = new Map>(); for (const parent of parents) { - const src = sliceLines(content, parent.startLine, parent.endLine); - if (!src || (!src.includes(''))) continue; - const names = new Set(); - JSX_TAG_RE.lastIndex = 0; - let m: RegExpExecArray | null; - while ((m = JSX_TAG_RE.exec(src))) names.add(m[1]!); + if (!parent.startLine || !parent.endLine) continue; + const span = `${parent.startLine}:${parent.endLine}`; + let names = tagsBySpan.get(span); + if (!names) { + names = new Set(); + lines ??= content.split('\n'); + const src = lines.slice(parent.startLine - 1, parent.endLine).join('\n'); + if (src.includes('')) { + JSX_TAG_RE.lastIndex = 0; + let m: RegExpExecArray | null; + while ((m = JSX_TAG_RE.exec(src))) names.add(m[1]!); + } + tagsBySpan.set(span, names); + } let added = 0; for (const name of names) { if (added >= MAX_JSX_CHILDREN) break; diff --git a/src/resolution/frameworks/angular-router.ts b/src/resolution/frameworks/angular-router.ts index cd81f85a6b..bdcf8858de 100644 --- a/src/resolution/frameworks/angular-router.ts +++ b/src/resolution/frameworks/angular-router.ts @@ -758,6 +758,11 @@ function angularAppRoot(filePath: string, context: ResolutionContext): string { export type AngularRouteTable = RootedRouteTable; +/** True for a route the table holds: this resolver's, at an absolute path. */ +function inAngularTable(node: Node): boolean { + return isAngularRoute(node) && node.name.startsWith('/'); +} + const tables = new WeakMap(); export function angularRouteTable(context: ResolutionContext): AngularRouteTable { @@ -767,7 +772,7 @@ export function angularRouteTable(context: ResolutionContext): AngularRouteTable const byRoot = new Map(); const byFile = new Map(); for (const node of all) { - if (!isAngularRoute(node) || !node.name.startsWith('/')) continue; + if (!inAngularTable(node)) continue; const root = angularAppRoot(node.filePath, context); let t = byRoot.get(root); if (!t) byRoot.set(root, (t = { source: all, exact: new Map(), dynamic: [] })); @@ -1035,6 +1040,14 @@ export const angularRouterResolver: FrameworkResolver = { return NAV_CALL.test(name) || LAZY_COMPONENT_REF.test(name) || LAYOUT_REF.test(name); }, + navigation: { + tails: ['navigate', 'navigateByUrl', 'createUrlTree', 'parseUrl'], + // Shared libraries can use the sole-app fallback. Adding or removing an + // app changes that eligibility, so retry across the workspace; resolution + // still enforces each call's own app and refuses ambiguous shared calls. + scope: (route) => inAngularTable(route) ? [''] : null, + }, + extract(filePath: string, content: string): FrameworkExtractionResult { if (!/\.[cm]?ts$/.test(filePath) || filePath.endsWith('.d.ts')) return { nodes: [], references: [] }; const { routes } = parseAngularRoutes(content); diff --git a/src/resolution/frameworks/csharp.ts b/src/resolution/frameworks/csharp.ts index 14b84d0ee6..0a3e2fbb39 100644 --- a/src/resolution/frameworks/csharp.ts +++ b/src/resolution/frameworks/csharp.ts @@ -12,6 +12,9 @@ import { pickByNameAndKind } from './name-heuristic'; export const aspnetResolver: FrameworkResolver = { name: 'aspnet', languages: ['csharp'], + // A Razor page's `@model CheckoutModel` names the PageModel in the + // code-behind beside it (`Checkout.cshtml.cs`); these rules look there first. + resolveLanguages: ['csharp', 'razor'], detect(context: ResolutionContext): boolean { // Check for .csproj files with ASP.NET references diff --git a/src/resolution/frameworks/expo-router.ts b/src/resolution/frameworks/expo-router.ts index ef6f44d81c..e6641a4608 100644 --- a/src/resolution/frameworks/expo-router.ts +++ b/src/resolution/frameworks/expo-router.ts @@ -610,6 +610,15 @@ export interface RouteTable { const tables = new Map(); +/** + * True for this framework's own route nodes: the ones whose name IS the path + * derived from their file. Express/SvelteKit routes in the same project name + * themselves differently and never match. + */ +function isScreenRoute(node: Node): boolean { + return routePathForFile(node.filePath) === node.name; +} + export function routeTable(context: ResolutionContext): RouteTable { const all = context.getNodesByKind('route'); const key = context.getProjectRoot(); @@ -618,10 +627,7 @@ export function routeTable(context: ResolutionContext): RouteTable { const exact = new Map(); const dynamic: RouteEntry[] = []; for (const node of all) { - // Only this framework's own route nodes: the ones whose name IS the path - // derived from their file. Express/SvelteKit routes in the same project - // name themselves differently and never match. - if (routePathForFile(node.filePath) !== node.name) continue; + if (!isScreenRoute(node)) continue; exact.set(node.name, node); if (node.name.includes('[')) dynamic.push({ node, segs: node.name.split('/').slice(1) }); } @@ -749,6 +755,14 @@ export const expoRouterResolver: FrameworkResolver = { return NAV_METHOD.test(name); }, + navigation: { + // A project's own wrapper (`safePush`) is left out: its tail is its own + // name, which the failed-tail index cannot find by suffix. + tails: ['push', 'replace', 'navigate', 'dismissTo'], + // One table for the whole project, and any file's call matches against it. + scope: (route) => (isScreenRoute(route) ? [''] : null), + }, + extract(filePath: string, content: string) { const apiPath = apiRoutePathForFile(filePath); if (apiPath !== null) { diff --git a/src/resolution/frameworks/go.ts b/src/resolution/frameworks/go.ts index 68e28fd0ca..f8b18bc2cd 100644 --- a/src/resolution/frameworks/go.ts +++ b/src/resolution/frameworks/go.ts @@ -7,6 +7,7 @@ import { Node } from '../../types'; import { FrameworkResolver, UnresolvedRef, ResolvedRef, ResolutionContext } from '../types'; import { stripCommentsForRegex } from '../strip-comments'; +import { GO_TYPE_KINDS, isGoBareName } from '../go-type-visibility'; import { pickByNameAndKind } from './name-heuristic'; export const goResolver: FrameworkResolver = { @@ -26,18 +27,24 @@ export const goResolver: FrameworkResolver = { }, resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null { - // An embedded type is the one its package declares under that name: the - // same package's for `Node`, the import's for `pkg.Node`, which name - // matching and the import resolver work out. The patterns below pick among - // every same-named node by folder convention, and Pattern 4 sees structs - // only: `Node` embedded beside promql/parser's `type Node interface` went - // to discovery/kubernetes's struct `Node`, etcd's `Client` embedding its - // own `Lease` interface to the server's `Lease` struct. + // The patterns below guess a declaration from the shape of a name, but Go + // reads a name from one package only: one written bare from the + // reference's own, `pkg.Node` from that import's (the import resolver's + // job), `x[i].Error()` from whatever type `x[i]` has. So they guess only + // for a bare name, only in its own package, and never for an embedded + // type, which name matching and the import resolver work out. Past that, + // promql/parser's `Node` parameters and embeddings went to + // discovery/kubernetes's struct `Node` beside the package's own `Node` + // interface, `apiv1.Node` to the struct in the same file, etcd's `Client` + // embedding its own `Lease` interface to the server's `Lease` struct, and + // every `.String()` called through an expression to the struct + // `promql.String`, as an instantiation. if (ref.referenceKind === 'extends' || ref.referenceKind === 'implements') return null; + if (!isGoBareName(ref, context)) return null; // Pattern 1: Handler references if (ref.referenceName.endsWith('Handler') || ref.referenceName.startsWith('Handle')) { - const result = resolveByNameAndKind(ref, 'function', HANDLER_DIRS, context); + const result = resolveInOwnPackage(ref, FUNCTION_KINDS, context); if (result) { return { original: ref, @@ -50,7 +57,7 @@ export const goResolver: FrameworkResolver = { // Pattern 2: Service/Repository references if (ref.referenceName.endsWith('Service') || ref.referenceName.endsWith('Repository') || ref.referenceName.endsWith('Store')) { - const result = resolveByNameAndKind(ref, null, SERVICE_DIRS, context, SERVICE_KINDS); + const result = resolveInOwnPackage(ref, SERVICE_KINDS, context); if (result) { return { original: ref, @@ -63,7 +70,7 @@ export const goResolver: FrameworkResolver = { // Pattern 3: Middleware references if (ref.referenceName.endsWith('Middleware') || ref.referenceName.startsWith('Auth') || ref.referenceName.startsWith('Log')) { - const result = resolveByNameAndKind(ref, 'function', MIDDLEWARE_DIRS, context); + const result = resolveInOwnPackage(ref, FUNCTION_KINDS, context); if (result) { return { original: ref, @@ -74,9 +81,12 @@ export const goResolver: FrameworkResolver = { } } - // Pattern 4: Model/Entity references (typically PascalCase structs) + // Pattern 4: Model/Entity references — a PascalCase type of any kind: the + // package's `Node` interface or `Entry` type, not only its structs. Left + // to name matching, the result type of `func (f *fanout) Appender(…) + // Appender` linked to the method the line declares. if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) { - const result = resolveByNameAndKind(ref, 'struct', MODEL_DIRS, context); + const result = resolveInOwnPackage(ref, GO_TYPE_KINDS, context); if (result) { return { original: ref, @@ -273,24 +283,17 @@ function extractGoTailIdent(expr: string): string | null { return m ? m[1]! : null; } -// Directory patterns for framework resolution -const HANDLER_DIRS = ['handler', 'handlers', 'api', 'routes', 'controller', 'controllers']; -const SERVICE_DIRS = ['service', 'services', 'repository', 'store', 'pkg']; -const MIDDLEWARE_DIRS = ['middleware', 'middlewares']; -const MODEL_DIRS = ['model', 'models', 'entity', 'entities', 'domain', 'pkg']; -const SERVICE_KINDS = new Set(['struct', 'interface']); +const FUNCTION_KINDS: ReadonlySet = new Set(['function']); +const SERVICE_KINDS: ReadonlySet = new Set(['struct', 'interface']); -/** A framework name heuristic's pick (see name-heuristic.ts), preferring these folders. */ -function resolveByNameAndKind( - ref: UnresolvedRef, - kind: string | null, - preferredDirs: string[], - context: ResolutionContext, - kinds?: Set -): string | null { - const allowed: ReadonlySet = kinds ?? (kind ? new Set([kind]) : GO_NAMED_KINDS); - return pickByNameAndKind(ref, allowed, (f) => preferredDirs.some((d) => f.includes(`/${d}/`)), context); +/** + * A framework name heuristic's pick (see name-heuristic.ts) among the + * declarations of the reference's own package — its directory — its own + * file's first. No folder convention applies: Go never reads a bare name from + * another package, a dot import aside. + */ +function resolveInOwnPackage(ref: UnresolvedRef, kinds: ReadonlySet, context: ResolutionContext): string | null { + const dir = ref.filePath.slice(0, ref.filePath.lastIndexOf('/') + 1); + const inPackage = (n: Node) => n.filePath.startsWith(dir) && !n.filePath.slice(dir.length).includes('/'); + return pickByNameAndKind(ref, kinds, () => false, context, { accept: inPackage }); } - -/** Any declaration a name can be (the heuristic with no kind). */ -const GO_NAMED_KINDS: ReadonlySet = new Set(['function', 'method', 'struct', 'interface', 'type_alias', 'variable', 'constant']); diff --git a/src/resolution/frameworks/index.ts b/src/resolution/frameworks/index.ts index 5f4b2fc0ec..36cba92cc3 100644 --- a/src/resolution/frameworks/index.ts +++ b/src/resolution/frameworks/index.ts @@ -205,6 +205,27 @@ export function getApplicableFrameworks( ); } +/** + * Filter a list of detected frameworks down to ones whose `resolve()` reads a + * reference written in `language`: its `resolveLanguages`, else its + * `languages`. Frameworks that declare neither are treated as universal. + * + * A framework's conventions are its languages': Express's `logger` middleware + * rule took a Go method's `*zap.Logger` result type for the method itself. + * Bridges list the language their references are written in — the React + * Native bridge JS, the Swift ↔ Objective-C bridge both, Drupal the + * `routing.yml` it reads controllers from. + */ +export function getResolvingFrameworks( + detected: FrameworkResolver[], + language: Language +): FrameworkResolver[] { + return detected.filter((fw) => { + const languages = fw.resolveLanguages ?? fw.languages; + return !languages || languages.includes(language); + }); +} + /** * Register a custom framework resolver */ diff --git a/src/resolution/frameworks/nextjs.ts b/src/resolution/frameworks/nextjs.ts index 22b85f7466..a2aba3d5dd 100644 --- a/src/resolution/frameworks/nextjs.ts +++ b/src/resolution/frameworks/nextjs.ts @@ -125,6 +125,12 @@ interface NextTable extends RouteTable { const tables = new WeakMap(); +/** The page a route node is, or null for a node the table leaves out (a handler, another framework's route). */ +function tablePage(node: Node): NextRouteFile | null { + const file = nextRouteForFile(node.filePath); + return file && file.kind === 'page' && file.path === node.name ? file : null; +} + export function nextRouteTable(context: ResolutionContext): NextTable { const all = context.getNodesByKind('route'); const cached = tables.get(context); @@ -133,8 +139,8 @@ export function nextRouteTable(context: ResolutionContext): NextTable { const dynamic: RouteTable['dynamic'] = []; const roots = new Set(); for (const node of all) { - const file = nextRouteForFile(node.filePath); - if (!file || file.kind !== 'page' || file.path !== node.name) continue; + const file = tablePage(node); + if (!file) continue; exact.set(node.name, node); if (node.name.includes(':')) dynamic.push({ node, segs: node.name.split('/').slice(1) }); roots.add(file.root); @@ -235,6 +241,13 @@ export const nextjsResolver: FrameworkResolver = { return NAV_CALL.test(name); }, + navigation: { + tails: ['push', 'replace', 'prefetch', 'redirect', 'permanentRedirect'], + // One table holds every Next app's pages, and a call under any of them + // matches against all of it. + scope: (route, context) => (tablePage(route) ? nextRouteTable(context).roots : null), + }, + extract(filePath: string, content: string) { const file = nextRouteForFile(filePath); if (!file) return { nodes: [], references: [] }; diff --git a/src/resolution/frameworks/react-router.ts b/src/resolution/frameworks/react-router.ts index e9d66ce25a..2d397aede8 100644 --- a/src/resolution/frameworks/react-router.ts +++ b/src/resolution/frameworks/react-router.ts @@ -5,8 +5,11 @@ * `` (v5), * `}/>` (v6) and * `createBrowserRouter([{ path, element }])` (v6.4+) each become a `route` - * node named by its path, bound to the component that renders it. That is - * half of what "how does this app flow" means. This file is the other half. + * node named by its path, bound to the component that renders it — and so + * does a table another file hands the router (`AppRoutes.map(… )`, `useRoutes(routes)` with `routes` imported), read once every + * file is indexed. That is half of what "how does this app flow" means. This + * file is the other half. * * **Navigation is a string.** `history.push('/placeorder')` (v5, and the * `useHistory` hook), `navigate('/placeorder')` (v6's `useNavigate`), @@ -32,8 +35,14 @@ * inside ``, a data router's `children`); the markup * scan composes that tree, a constant path (`paths.app.root.path`) included, * and a `lazy: () => import('./routes/x')` route renders that module's default - * export (`frameworks/react.ts`). Known limit, deliberate: a splat - * (`/admin/*`) matches anything, so it is never the answer to a concrete href. + * export (`frameworks/react.ts`). An index route (`index: true`, ``) is the page at its parent's address, and a route around others — + * an object with `children`, a `` with ``s inside — is + * the layout they render inside (a `references` edge marked `layout: true`). + * Known limits, deliberate: a splat (`/admin/*`) matches anything, so it is + * never the answer to a concrete href; and an index route at the top of a + * component's own `` is at wherever another route mounts that + * component, which its file does not say, so it is no route at all. */ import type { Language, Node } from '../../types'; @@ -60,7 +69,7 @@ import { // two arms). It lives in `nextjs.ts` because that is where it was first // needed; duplicating it here would be a second derivation of the same rule. import { destinationsForHref } from './nextjs'; -import { configHrefExpression } from './react'; +import { configHrefExpression, isTableRoute } from './react'; const ROUTE_LANGUAGES: readonly Language[] = ['typescript', 'javascript', 'tsx', 'jsx']; @@ -309,12 +318,14 @@ export const reactRouterRoot = appRootFor; * Its id is a verbatim reconstruction of the node's own fields, which no * other framework's route id is: a server route carries its METHOD * (`route:file:12:POST:/login`), a file-based page carries no line. A path - * built from a constant keeps the id it was extracted with. + * built from a constant keeps the id it was extracted with, and a route read + * from a table another file hands the router says so (`…:12:table:/login`). */ function isReactRouterRoute(node: Node): boolean { if (node.id.startsWith(`route:react-router:${node.filePath}:`)) return true; const prefix = `route:${node.filePath}:${node.startLine}:`; if ((node.language !== 'tsx' && node.language !== 'jsx') || !node.id.startsWith(prefix)) return false; + if (isTableRoute(node)) return true; // Its name, or — for a path built from a constant, renamed after extraction // — the path as the file wrote it; never a server route's `METHOD:`. const rest = node.id.slice(prefix.length); @@ -336,6 +347,15 @@ function optionalRoutePaths(path: string): string[] { return [...unique.values()]; } +/** + * True for a route the table holds. A nested route's path is relative to its + * parent; without the tree it is not a destination. A splat matches + * everything, so it answers nothing. + */ +function isDestination(node: Node): boolean { + return isReactRouterRoute(node) && node.name.startsWith('/') && !node.name.endsWith('*'); +} + const tables = new WeakMap(); export function reactRouterTable(context: ResolutionContext): ReactRouterTable { @@ -350,10 +370,7 @@ export function reactRouterTable(context: ResolutionContext): ReactRouterTable { return t; }; for (const node of all) { - if (!isReactRouterRoute(node)) continue; - // A nested route's path is relative to its parent; without the tree it is - // not a destination. A splat matches everything, so it answers nothing. - if (!node.name.startsWith('/') || node.name.endsWith('*')) continue; + if (!isDestination(node)) continue; const root = reactRouterRoot(node.filePath); const path = node.name.length > 1 && node.name.endsWith('/') ? node.name.slice(0, -1) : node.name; @@ -418,6 +435,11 @@ export const reactRouterResolver: FrameworkResolver = { }, extract: extractReactRouterConfig, + navigation: { + tails: ['push', 'replace', 'navigate', 'redirect'], + // A call matches against the table of the app its file is in. + scope: (route) => (isDestination(route) ? [reactRouterRoot(route.filePath)] : null), + }, resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null { if (ref.referenceName.startsWith('react-router-module:')) { diff --git a/src/resolution/frameworks/react.ts b/src/resolution/frameworks/react.ts index ac4453c309..bf0e7a03f6 100644 --- a/src/resolution/frameworks/react.ts +++ b/src/resolution/frameworks/react.ts @@ -5,13 +5,17 @@ * Next.js pages, route handlers and navigation are `nextjs.ts`'s. */ -import { Node } from '../../types'; -import { FrameworkResolver, UnresolvedRef, ResolvedRef, ResolutionContext } from '../types'; +import { Language, Node } from '../../types'; +import { FrameworkExtractionResult, FrameworkResolver, UnresolvedRef, ResolvedRef, ResolutionContext } from '../types'; import { dependsOn } from './package-deps'; import { detectLanguage } from '../../extraction/grammars'; import { parseSourceTreeSync } from '../../extraction/parse-tree'; import { resolveImportPath } from '../import-resolver'; import { innermostBinding } from '../gates'; +import { stripCommentsForRegex } from '../strip-comments'; +import { makeLineAt } from '../synth-utils'; +import { isTestPath } from '../../search/query-utils'; +import { matchBracket, skipString } from './object-literal'; /** The languages React components, hooks and contexts are written and used in. */ const REACT_SCRIPT_LANGUAGES: ReadonlySet = new Set(['typescript', 'javascript', 'tsx', 'jsx']); @@ -21,8 +25,6 @@ export const reactResolver: FrameworkResolver = { // Includes 'tsx'/'jsx' so route extraction runs on JSX files (where // `}>` routes live) — without them the .tsx/.jsx grammars // were filtered out of the extract pass and those routes were never indexed. - // (resolve() is unaffected — it runs for every detected framework regardless - // of language; only the extract pass filters on `languages`.) languages: ['javascript', 'typescript', 'tsx', 'jsx'], detect(context: ResolutionContext): boolean { @@ -34,9 +36,10 @@ export const reactResolver: FrameworkResolver = { return allFiles.some((f) => f.endsWith('.jsx') || f.endsWith('.tsx')); }, - // A data-router `lazy: () => import('./routes/x')` route names a module, not a symbol. + // A data-router `lazy: () => import('./routes/x')` route names a module, not + // a symbol; a route's `layout:MainLayout` names the component around it. claimsReference(name: string): boolean { - return name.startsWith(LAZY_ROUTE_PREFIX); + return name.startsWith(LAZY_ROUTE_PREFIX) || name.startsWith(LAYOUT_PREFIX); }, resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null { @@ -47,6 +50,13 @@ export const reactResolver: FrameworkResolver = { const target = lazyRouteComponent(ref.referenceName.slice(LAZY_ROUTE_PREFIX.length), ref.filePath, context); return target ? { original: ref, targetNodeId: target, confidence: 0.9, resolvedBy: 'framework' } : null; } + // The layout a route renders inside: what happens in it — its header's + // links, its logout — happens on the route's screen too. + if (ref.referenceName.startsWith(LAYOUT_PREFIX)) { + if (!ref.fromNodeId.startsWith('route:') || ref.fromNodeId.endsWith(':vue') || ref.fromNodeId.endsWith(':angular')) return null; + const target = layoutComponent(ref.referenceName.slice(LAYOUT_PREFIX.length), ref, context); + return target ? { original: ref, targetNodeId: target, confidence: 0.9, resolvedBy: 'framework', metadata: { layout: true } } : null; + } // A component, hook or context the file IMPORTS is the import's: the // package's (`useQuery` from `@tanstack/react-query`, `