diff --git a/CHANGELOG.md b/CHANGELOG.md index 57ea805b51..33dcf82f57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,10 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes +- Kotlin receiver types carry through declared factory returns, compatible property extensions and verified receiver-preserving fluent methods. Same-name methods on incompatible types are rejected. + +- Kotlin nested and multiline return chains keep the callee position and declared return type. Imported return-type hypotheses with an unknown receiver are capped before candidate ranking, so a stronger framework candidate can win. Re-index to refresh call positions. + - Calls through a typed receiver link to the right class in Python, PHP, Ruby and C#. In Python, `self.cache.fetch()` now links to `Store.fetch` when `__init__` assigns `self.cache = Store()`, in the class and its subclasses. A field is typed only when every method that assigns it, in the class and its base classes, agrees on the class (a subclass may narrow a base's declared type), a conditional value like `Store() if flag else Other()` names no class, and a field assigned only by a constructor call does not fall back to a subclass's method. On flask this adds 8 correct call edges and removes none; on cpython it adds 1,222 typed call edges and drops 91 wrong name guesses. A nested function that uses its outer function's annotated parameter, like `obj` in `def outer(obj: Data)`, now reads that annotation instead of linking to any method of that name. In PHP, a typed property whose class is imported as `use Lib\Store as Cache` links to `Lib\Store`'s methods, including grouped `use Lib\{Store, Other as Alt};` and comma-separated imports, and a qualified type like `Other\Sub` means that class, not an imported `Sub`. A class name in a file with no namespace means the global class, not a same-named class in another namespace, and an inherited method is looked up on the parent the class's own declaration names. Ruby resolves a constant like `Sub` from the class or module around it outward, then through the enclosing class's superclasses, and reads a qualified `Other::Sub` as written, including for a receiver like `x = Other::Sub.new`. Inside `class Other::App`, a bare `Sub` is not looked up in `Other`. A C# or Ruby receiver whose class lacks the method no longer links to a same-named method on an unrelated class, though a C# extension method whose `this` type fits the receiver still matches. Re-index affected projects after upgrading. - Five more JavaScript and TypeScript cases link correctly. Member calls in Svelte and Astro files (`count.set(1)` on an imported store) now go through the same receiver checks as TypeScript, so they no longer link to the store constant or to an unrelated class's `set`. A variable holding `$state(new Model())`, `$state.raw(…)` or `$derived(…)` now has `Model`'s type, in components and in `.svelte.ts` files. A call through a field typed `Conn | null` or `Conn | undefined` on a typed variable (`s.conn.query()`) links to `Conn`'s method, as `this.conn.query()` already did. An import from a monorepo package that only a `package.json` names, in a workspace layout CodeGraph does not map (Rush, nested or negated workspace globs), no longer links to a same-named function elsewhere at trusted confidence. A store selector captured by a closure still links its calls when a closed block or sibling function declares the same name with `const`, `let`, `class` or `function`; a `var` still hides it. And a circular default re-export, which ESM rejects, no longer resolves to the first function exported from a file in the cycle. - Kotlin, Java and Angular code maps more accurately. A Kotlin assignment to a member of a call result (`res().status = code`) no longer ends the class it sits in: in javalin, every `Context` member after the first such line had been extracted as a top-level function, which other packages could not reach. A `fun interface` now spans its body, so its method sits inside it, and one with type parameters is no longer dropped. A Kotlin `it` is typed as the receiver of `let` or `also` only when the call is the standard-library one, not an extension `fun Widget.let(…)` from any file. A project's own function named `run`, `apply` or `with` now binds `it` like any other function instead of passing through an outer lambda's `it`, when the call can see it (same package or imported) and it takes a lambda. A Java `(Foo f) -> f.bar()` links to `Foo.bar` even when the class has a field named `f`. An Angular route mounted through an NgModule whose routing module, in the module's own folder, imports its routes from another file, or sits behind an `export *` barrel there, is named under its lazy path instead of `/`. `$`-prefixed names such as `RouterModule as $router` and `$paths.cart` are also read. @@ -284,6 +288,23 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Calls in Vue, Svelte and Astro components now follow the same rules as JavaScript and TypeScript. A `t('…')` from `useI18n()` or a `ref(…)` from `vue` used to link to an interface property or a local variable of the same name elsewhere in the project, so on halo two unrelated symbols had over a thousand made-up callers each. A call imported from a package is also no longer mistaken for a project symbol when the package is declared in a nested app's `package.json` or a Deno import map. An import through an alias CodeGraph can't follow, like SvelteKit's `$lib/…` or a nested Nuxt app's `~/…`, is still treated as project code, and so is an import of the package's own `package.json` name or of a `workspace:` dependency. A function the file declares itself keeps its calls even when it shares a built-in's name, like a component's `function process(…)` or a module's own `fetch`. Re-index Vue, Svelte, Astro and JavaScript/TypeScript projects after upgrading. - A bare Java call like `verify(mock)`, `assertThat(x)` or `hashCode()` now links only to a method of the class it's in, of that class's parents, or one the file imports statically. It used to link to any class's method of that name: in halo, Mockito's `verify` and `eq` gave one service's `verify` over a thousand made-up callers, and in retrofit Truth's `assertThat` did the same to a test helper. Calls to inherited methods and static imports from the project now link to the right one, like jsoup's `attr(…)` through `LeafNode` or halo's `and(…)` / `equal(…)` through `import static …Queries.*`. A call inside an anonymous class, like `new PetType() { … }`, also reaches the methods of the type it instantiates. Re-index Java projects after upgrading. - Python calls like `User.objects.get(…)`, `self.client.login(…)` or `request.POST.get(…)` no longer link to whichever class in the project has a method of that name. Such a call now links only to a member of what its receiver names, like `helpers.slugify()` to `slugify` in `helpers.py`, and a bare `get(1)` never links to a method. A name the file imports from a package outside the project, like `from django.shortcuts import render`, no longer links to a same-named project function. In Django projects these made-up links had given single test or serializer methods thousands of callers, for example netbox's `.all()` calls on one `UserConfig.all`. Re-index Python projects after upgrading. +- In Kotlin, a call with no receiver now reaches a method of another class only when that class is in reach. That means a class, an anonymous `object : Base()` or an extension function's receiver around the call, what those inherit, or the receiver of a DSL lambda (koin's `single { get() }` runs on a `Scope`, Exposed's `withTables { … }` on a transaction). An object member the file imports is in reach too. Kotlin's own `error(…)`, `require(x >= 0) { … }` and `check(…)` no longer land on a project class's method of that name. On koin, `module { … }` in one test used to land on another test class's private `module` over 260 times, and inside a lambda `get()` landed on the wrong class's `get`. Re-index after upgrading. +- In C#, Java and Kotlin, a call through a field, property or parameter now lands on the method of the type it is declared with. That includes a type inherited from a base class with its type argument filled in, a generic or `for`-each declaration, a C# `using` alias, and a type parameter's bound. A call no longer falls back to a guess at some project class's same-named method, and a type from outside the project gives no link. On Newtonsoft.Json, `_innerWriter.WriteValue(…)` inside a writer wrapper used to land on the wrapper's own `WriteValue`, and a `TextWriter`'s `Write(…)` on a test writer's. On commons-lang, a `DateFormat` field's `parse(…)` used to land on the project's `DateParser` 284 times. +- C# members of an interface are now public and types declared without a modifier are now internal, as the language defines. A call on an interface-typed field now reaches the interface's method, where it used to find nothing. A guess from a receiver's name now weighs only the owner type's own name, not its namespace. Standard .NET names like `ToString`, `Add` and `Write` need a receiver named after the owner. In every language, a guess from production code no longer lands on a mock or helper defined in a test suite. Re-index after upgrading. +- In Swift, a call written with no receiver, or through `self.` or `super.`, now reaches only a member of the type around it or of what that type inherits or conforms to, and the nearest override wins. A standard-library name later in a chain (`axis.entries.removeAll()`, `names.contains(x)`) no longer lands on a project type's method, unless the receiver or the call's argument label points to it. On Charts, `min(a, b)` and `max(a, b)` used to land on a range type's `min` field and a `Sequence` extension, and `super.init(…)` on an Objective-C demo's `init` over 170 times. A global in a playground or a test target is no longer linked from the main code. Re-index after upgrading. +- A call on a variable declared with a type from outside the project (`List list`, `var sb = new StringBuilder()`, `signer = TimestampSigner()`) no longer lands on a project type's method with the same name. On gson, `list.add(…)` used to land on a project list wrapper; on commons-lang, `s.length()` and `buffer.append(…)` on project classes. In Python, a keyword argument inside a call (`prefix=IPNetwork(…)`) is no longer taken for an assignment to that name. +- Kotlin classes whose primary constructor sits on the line after the class name, before a supertype list (`expect open class ByteString` / `internal constructor(…) : Comparable`, common in Kotlin Multiplatform code), no longer lose their members to loose functions. A call later in a chain to a Kotlin standard-library function (`apply`, `let`, `toString`, `map`, `contains`…) no longer lands on a project type's same-named method. On okhttp, `.apply { … }` used to land on `ConnectionSpec.apply`. Re-index after upgrading. +- In Rust and Go, a call later in a chain, like `sym.filename().map(…)` or `c.Flags().String(…)`, no longer lands on the project's own method of that name when the name is a standard-library one (`unwrap`, `clone`, `map`, `iter`, Go's `String` or `Error`). A plain call like `get(handler)` or `drop(x)` never lands on a method. On tokio, `.map(…)` on an `Option` used to land on `MutexGuard::map`; on axum, the router's `get(handler)` on a cookie jar's `get`; on ripgrep, `.clone()` on an error type. +- A call on a type from outside the project, like `Integer.valueOf(…)`, `Arrays.asList(…)`, `Object.assign(…)` or Delphi's `Exception.Create(…)`, no longer lands on the project's own method with that name. On commons-lang, `Integer.valueOf` used to land on `StringUtils.valueOf` over 800 times. Go's exported variables and Pascal's capitalized parameters and locals are not mistaken for type names. +- Imports through a tsconfig or jsconfig path alias now use the config nearest the importing file, so each app in a monorepo resolves its own `@/…` or `~/…` alias. On bulletproof-react and trpc's examples, those imports used to fail or land in a different app. +- React Router data-router routes are named by their full path: nested `children` paths are joined to their parent's, `path: paths.app.root.path` constants are read, and `lazy: () => import('./routes/x')` routes link to the module's component. A guard wrapper like `` no longer stands in for the page. On bulletproof-react, every route now has its real path and page. +- In JavaScript, TypeScript and Vue, a call through a name destructured from a composable or custom hook, like `const { getDefaultActivityRoute } = useDefaultActivity()` or `const { t } = useI18n()`, now reaches the function the hook returns. These calls used to resolve to nothing. mealie, elk, halo and excalidraw each gained dozens to over a hundred such links. +- In JavaScript and TypeScript, a module's top-level function, variable or type that the module doesn't export is no longer linked from other files. Every form of export still counts: the `export` keyword, `export { … }` lists, `export default`, and a default-exported object. On typeorm, test fixtures' local `ColumnMetadata` used to capture `import { ColumnMetadata } from "typeorm"`; on sveltekit, an unexported `resolve` collected other files' `resolve(…)` calls; on zod, a local `z` collected the `zod` package imports. +- Flow-typed JavaScript files (those with `@flow` in their header comment) are now parsed like TypeScript, so type annotations like `render(): React.Node` no longer cut classes short. This mostly affects React Native code: on React Native's own libraries the graph gained over 8,000 links, and component classes keep their methods. Files without the pragma are unchanged. Re-index after upgrading. +- In Scala, a call reached by its name alone now stays in reach. A name that the enclosing function binds as a parameter or local (`f(true)` for `f: A => B`) is that local. A call later in a chain, like `fa.iterator.map(…)`, reaches only a member of what its receiver is named after. A plain call reaches only the enclosing class and its supertypes (an anonymous subclass's base included), a companion, or an imported object. On cats, parameter calls like `f(a)` used to land on a case class's field `f` over 400 times, and chained `.map(…)` on one ops class's `map`. +- A link made by a framework or bridge resolver now records which one made it (`metadata.framework`, such as `swift-objc-bridge` or `react-native-bridge`), as the README describes. Before, only synthesized event and view channels were named. +- ASP.NET apps built with FastEndpoints now have their routes: each endpoint class's `Get(…)` / `Post(…)` in `Configure()` becomes a route linked to its own `HandleAsync` / `ExecuteAsync`, including paths kept in a request class's `Route` constant. ardalis/CleanArchitecture went from no routes to 24. Minimal API routes written without a leading slash (`app.MapGet("api/todos", …)`) are now named `/api/todos`. +- Vapor routes whose handler is a trailing closure (`app.get("hello") { req in … }`), WebSocket routes (`app.webSocket("chat") { … }`) and `routes.on(.POST, "x", use: handler)` registrations are now found. Only routes with a `use:` handler were read before: the Swift Package Index server went from 13 routes to 34, and a WebSocket-only chat server from none to one. - Play projects kept in subdirectories are recognized, so each one's `conf/routes` is read. A repository with no Play build at its root, like playframework's samples, went from no routes to over 130. - Rails routes are now read with their nesting: `namespace` and `scope` add their path and controller module, nested `resources` sit under their parent's `:id`, and `member` / `collection` blocks add their actions. A namespaced route now links to its own module's controller. Routes in a Rails engine's `config/routes.rb` are found too: solidus went from no routes to over 600, and mastodon's route-to-action links nearly tripled. - Flask routes registered with `add_url_rule(…)` are now found and linked to their view function or class-based view. So are routes registered through a project's own helper that passes a list of paths and a `view_func=`. flaskbb, which registers every view that way, went from no routes to over 100. diff --git a/README.md b/README.md index 3e5694cc0e..4ca34f9cab 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 all of upstream `main` (last merged: [`a5b0160`](https://github.com/colbymchenry/codegraph/commit/a5b0160e), after v1.6.1, 2026-09-30) 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 all of upstream `main` (last merged: [`2e2b98c`](https://github.com/colbymchenry/codegraph/commit/2e2b98c9), after v1.6.1, 2026-09-30) 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). @@ -114,6 +114,8 @@ Dispatch and framework coverage the fork adds, by kind: | React Native `NativeModules[key]` | Computed native-module calls to the native method | | `window.postMessage` | Posted messages to their listeners | +Flow-annotated JavaScript is parsed through the TSX grammar. Java and C# calls through declared fields or properties use their declared types; unresolved external types remain unresolved. Rust, Go, Scala, Swift and Kotlin calls also use the receiver and lexical scope at the call site. Kotlin receiver inference follows bounded chains of declared returns and verified receiver-preserving methods. Properties initialized by typed factory calls retain compatible imported extensions. Kotlin chains use the callee position and declared return type, including nested and multiline calls; imported return-type hypotheses for standard method names keep confidence at most 0.7 when the receiver type is unknown. + **Server endpoints** | Addition | What it links | @@ -500,8 +502,8 @@ CodeGraph detects web-framework routing files and emits `route` nodes linked by | **Play** | `GET`/`POST`/… verb routes in `conf/routes` → `Controller.method` actions (Scala + Java), including projects kept in subdirectories | | **Gin / chi / gorilla / mux** | `r.GET(...)`, `router.HandleFunc(...)` | | **Axum / actix / Rocket** | `.route("/x", get(handler))` | -| **ASP.NET** | `[HttpGet("/x")]` attributes on action methods | -| **Vapor** | `app.get("x", use: handler)` | +| **ASP.NET** | `[HttpGet("/x")]` attributes on action methods and FastEndpoints `Configure()` verb calls | +| **Vapor** | `app.get("x", use: handler)` and closure handlers | | **Analog** | `src/app/pages/**/*.page.ts` files (`index`, dot segments, `[param]`, `[...rest]` and `(group)` names) bound to the page's default component class; a page with a same-named folder is a layout, not a route | — | | **Astro** | `src/pages/` file-based routes (`.astro` pages + `.ts` endpoints, `[param]`/`[...rest]` syntax); each page links to its component, exported `GET`/`POST`/… endpoint methods link to their handlers, and `` / `Astro.redirect` link to the page they name | | **RedwoodSDK** | Literal `defineApp([...])` trees with `route`, `index`, `render`, `layout` and `prefix`, plus `{ get, post, … }` method tables; each route links to its final handler and becomes a page once that handler is shown to return JSX | — | @@ -514,7 +516,7 @@ These frameworks additionally emit **`navigates`** edges: the function that send |---|---|---| | **Expo Router** | Every screen file under `app/` (`app/item/[id].tsx` → `/item/[id]`, groups stripped), bound to its default-export component; `+api` files are endpoints (`GET /hello`) bound to their handlers | `router.push` / `replace` / `navigate`, template hrefs, `{ pathname }` objects, and a helper's returned href | | **Next.js** | App Router `app/**/page.tsx` and Pages Router pages (`(group)` stripped, `[slug]` → `:slug`); `app/api/**/route.ts` exports and `pages/api/*` are endpoints, not screens | `router.push` / `replace` / `prefetch`, `redirect()` / `permanentRedirect()` in a server action or page, `NextResponse.redirect(new URL(…))` in middleware, `` and internal `` | -| **React Router** | `` (v5 and v6), `createBrowserRouter([{ path, element }])`, framework mode's `app/routes.ts` (`route`, `index`, `layout`, `prefix`), and the default file convention under `app/routes/` (Remix, or React Router with `flatRoutes()`: dot nesting, index and pathless segments, `$param`, optional `($segment)` and `$` splats), each bound to its module's default component | `history.push` / `replace`, `useNavigate`'s `navigate`, a loader's `redirect`, `` / `` / `` / v5's `` / react-router-bootstrap's ``, and a `styled(Link)` wrapper | +| **React Router** | `` (v5 and v6), `createBrowserRouter` / `createHashRouter` / `createMemoryRouter` arrays with nested `children`, constant paths, `Component` and lazy module exports, framework mode's `app/routes.ts` (`route`, `index`, `layout`, `prefix`), and the default file convention under `app/routes/` (Remix, or React Router with `flatRoutes()`: dot nesting, index and pathless segments, `$param`, optional `($segment)` and `$` splats), each bound to its module's default component | `history.push` / `replace`, `useNavigate`'s `navigate`, a loader's `redirect`, `` / `` / `` / v5's `` / react-router-bootstrap's ``, and a `styled(Link)` wrapper | | **TanStack Router** | `createFileRoute('/posts/$postId')` (file-based) and `createRoute({ path, getParentRoute })` composed up its parent chain (code-based); `_pathless` segments, `(group)` folders, `__root` and `` layouts are not addresses; TanStack Start `server.handlers` (and `createHandlers`) in those files become method-qualified endpoints (`GET /api/users`) | `navigate({ to })`, a thrown `redirect({ to })`, `` / `` — where `to` is the route PATTERN and the values ride beside it in `params` | | **Vue Router** / **Nuxt** | `createRouter({ routes: [...] })` / `new Router(...)` and the route tables it's given (`export const constantRoutes = [...]`, per-module route files), with the view each entry names — a lazy `() => import(…)` bound to its file — and `children` joined onto their parent's path, the parent being the layout around them; plus, in a Nuxt app, `pages/` file-based routes, each linked to its page component (`index` folders, root index pages, Nuxt 4 route groups), `server/api/` and `server/routes/` endpoints (method suffixes such as `.get.ts`, catch-alls) and route middleware | `router.push` / `replace`, `$router.push` / `this.$router.push`, Nuxt's `navigateTo`, `` / `` / `` — **by route name** (`push({ name: 'profile' })`) as well as by path | | **Solid Router** | Imported `Router`/`Route` JSX and route-config arrays (`path`, `component`, `children`) with static `lazy(() => import(...))` components. A table exported from another file as `RouteDefinition[]` (the official template's `routes.ts`) is read too, prefixed by where it is registered | — | @@ -554,7 +556,7 @@ Real iOS and React Native codebases live across multiple languages — a Swift c | Expo Modules | expo-haptics | expo-camera | expo SDK sweep (7 packages) | | Fabric / Paper views | [react-native-segmented-control](https://github.com/react-native-segmented-control/segmented-control) | [react-native-screens](https://github.com/software-mansion/react-native-screens) | [react-native-skia](https://github.com/Shopify/react-native-skia) | -Each bridge emits edges tagged `provenance:'heuristic'` with `metadata.synthesizedBy:` set to a stable channel name (e.g. `swift-objc-bridge`, `rn-event-channel`, `fabric-native-impl`, `expo-module-extract`), so the agent can tell at a glance how a hop got into the graph. +Every bridge hop says how it got into the graph. A hop matched by a bridge resolver carries `metadata.resolvedBy: 'framework'` and `metadata.framework` naming the resolver (`swift-objc-bridge`, `react-native-bridge`, `expo-modules-js`, `fabric-view`). A synthesized channel is tagged `provenance:'heuristic'` with `metadata.synthesizedBy` (`rn-event-channel`, `fabric-native-impl`). --- ## Quick Start diff --git a/__tests__/cross-language-gate.test.ts b/__tests__/cross-language-gate.test.ts index dc20ee3ce0..58d13b370c 100644 --- a/__tests__/cross-language-gate.test.ts +++ b/__tests__/cross-language-gate.test.ts @@ -63,7 +63,7 @@ describe('cross-language name matches', () => { it('keeps a Kotlin call onto a Java method (one JVM)', async () => { project({ - 'src/Main.kt': 'fun main() {\n shout()\n}\n', + 'src/Main.kt': 'import Loud.shout\n\nfun main() {\n shout()\n}\n', 'src/Loud.java': 'public class Loud {\n public static int shout() { return 1; }\n}\n', }); expect(await targetsOf('main')).toContain('src/Loud.java:shout'); diff --git a/__tests__/declared-member-receiver.test.ts b/__tests__/declared-member-receiver.test.ts new file mode 100644 index 0000000000..4aad7c8ca8 --- /dev/null +++ b/__tests__/declared-member-receiver.test.ts @@ -0,0 +1,217 @@ +/** + * A C# / Java call through a field, property or parameter is a call on the + * type it is declared with — read from the class's own member declarations, + * those it inherits (with the type arguments the subclass gives), a using + * alias, or a generic or enhanced-for declaration — never a guess at a + * same-named method of some project class: + * + * - Newtonsoft's `_innerWriter.WriteValue(…)` inside TraceJsonWriter went to + * TraceJsonWriter's own `WriteValue`, and `_textWriter.Write(…)` on a + * `TextWriter` to a test's `ThrowingWriter.Write`; + * - a C# interface's members are public, so a call on an interface-typed + * field reaches the interface's method (eShop's `_fixUriService.Fix…()`); + * - `using Assert = …XUnitAssert;` makes `Assert.AreEqual` XUnitAssert's; + * - a standard .NET name on an untyped receiver (`table.Columns.Add(…)`) is + * not a project class's same-named method unless the receiver names it. + */ +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-member-receiver-')); + const files: Record = { + 'src/KnownClinic.java': `package app; +public class KnownClinic { + private app.Repository owners; + public void check() { this.owners.find(); } +} +`, + 'src/ExternalClinic.java': `package app; +public class ExternalClinic { + private outside.Repository owners; + public void check() { this.owners.find(); } +} +`, + 'src/Repository.java': `package app; +public interface Repository { + java.util.List find(); +} +`, + 'src/Clinic.java': `package app; +public class Clinic { + protected Repository owners; + public void check() { + java.util.List owners = this.owners.find(); + } +} +`, + 'src/Json/JsonWriter.cs': `namespace App.Json; + +public abstract class JsonWriter +{ + public virtual void WriteValue(string value) { } +} +`, + 'src/Json/TraceJsonWriter.cs': `using System.IO; + +namespace App.Json; + +internal class TraceJsonWriter : JsonWriter +{ + private readonly JsonWriter _innerWriter; + /* a block comment naming the _textWriter field */ + private readonly TextWriter _textWriter; + + public override void WriteValue(string value) + { + _innerWriter.WriteValue(value); + _textWriter.Write(value); + } +} +`, + 'tests/ThrowingWriter.cs': `using System.IO; + +namespace App.Tests; + +public class ThrowingWriter : TextWriter +{ + public override void Write(string value) { } +} +`, + 'tests/XUnitAssert.cs': `namespace App.Tests; + +public static class XUnitAssert +{ + public static void AreEqual(object a, object b) { } +} +`, + 'src/DefaultJsonNameTable.cs': `namespace App; + +public class DefaultJsonNameTable +{ + public void Add(string key) { } +} +`, + 'src/Services/IFixUriService.cs': `namespace App.Services; + +public interface IFixUriService +{ + void FixBasketItemPictureUri(string uri); +} +`, + 'src/Services/FixUriService.cs': `namespace App.Services; + +public class FixUriService : IFixUriService +{ + public void FixBasketItemPictureUri(string uri) { } +} +`, + 'src/Services/BasketService.cs': `namespace App.Services; + +public class BasketService +{ + private readonly IFixUriService _fixUriService; + + public void Load(string uri) + { + _fixUriService.FixBasketItemPictureUri(uri); + } +} +`, + 'tests/Integration/IntegrationTest.cs': `namespace App.Tests.Integration; + +public abstract class IntegrationTest where TFixture : new() +{ + protected TFixture Fixture { get; private set; } +} + +public class DropCreateDatabaseAlways +{ + public object CreateContext() => null; +} +`, + 'tests/Integration/QueryTests.cs': `using Assert = App.Tests.XUnitAssert; + +namespace App.Tests.Integration; + +public class QueryTests : IntegrationTest +{ + public class DatabaseInitializer : DropCreateDatabaseAlways { } + + public void Runs() + { + var context = Fixture.CreateContext(); + Assert.AreEqual(1, 1); + var table = MakeTable(); + table.Columns.Add("price"); + } +} +`, + }; + 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 }); +}); + +/** `Owner::member` of every call edge out of a file. */ +function callsFrom(file: string): string[] { + const ids = cg.getNodesInFile(file).map((n) => n.id); + return cg + .getOutgoingEdgesFrom(ids) + .filter((e) => e.kind === 'calls') + .map((e) => cg.getNode(e.target)!.qualifiedName) + .sort(); +} + +describe('C# calls through declared members', () => { + it('reach the field’s declared type, and nothing for an outside one', () => { + expect(callsFrom('src/Json/TraceJsonWriter.cs')).toEqual(['App.Json::JsonWriter::WriteValue']); + }); + + it('reach an interface’s method: interface members are public', () => { + expect(callsFrom('src/Services/BasketService.cs')).toEqual([ + 'App.Services::IFixUriService::FixBasketItemPictureUri', + ]); + }); + + it('follow an inherited property typed by the type argument, a using alias, and leave a .NET name alone', () => { + expect(callsFrom('tests/Integration/QueryTests.cs')).toEqual([ + 'App.Tests.Integration::DropCreateDatabaseAlways::CreateContext', + 'App.Tests::XUnitAssert::AreEqual', + ]); + }); +}); + +describe('C# default visibility', () => { + it('is public in an interface and internal for a type in a namespace', () => { + const method = cg.getNodesInFile('src/Services/IFixUriService.cs').find((n) => n.kind === 'method')!; + expect(method.visibility).toBe('public'); + const cls = cg.getNodesInFile('src/Json/TraceJsonWriter.cs').find((n) => n.kind === 'class')!; + expect(cls.visibility).toBe('internal'); + }); +}); + +it('keeps an explicit Java field receiver when a local shares its name', () => { + expect(callsFrom('src/Clinic.java')).toContain('app::Repository::find'); +}); + +it('leaves a qualified external field type outside the project', () => { + expect(callsFrom('src/ExternalClinic.java')).not.toContain('app::Repository::find'); +}); + +it('keeps the exact qualified project field type', () => { + expect(callsFrom('src/KnownClinic.java')).toContain('app::Repository::find'); +}); diff --git a/__tests__/destructured-call-result.test.ts b/__tests__/destructured-call-result.test.ts new file mode 100644 index 0000000000..e11e2e6149 --- /dev/null +++ b/__tests__/destructured-call-result.test.ts @@ -0,0 +1,88 @@ +/** + * A call through a name destructured from a call's result — a composable or + * a custom hook — reaches the function that callee returns under that key: + * `const { getDefaultActivityRoute } = useDefaultActivity()` (mealie, where the + * function is a module-level one the composable returns), `const { t } = + * useI18n()`, `const { login } = useAuth()` (declared in the hook's body). + * The local binding used to rule out every cross-file candidate. + */ +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-destructured-')); + const files: Record = { + 'src/hooks/useNested.ts': `export function useNested() { + function hiddenAction() { } + function nested() { return { hiddenAction }; } + return undefined; +} +`, + 'src/pages/nested.ts': `import { useNested } from '../hooks/useNested'; +export function runNested() { + const { hiddenAction } = useNested(); + hiddenAction(); +} +`, + 'package.json': JSON.stringify({ name: 'app', dependencies: { vue: '^3' } }), + 'src/composables/use-default-activity.ts': `function getDefaultActivityRoute(key?: string): string { + return key ?? '/'; +} +export default function useDefaultActivity() { + return { getDefaultActivityRoute }; +} +`, + 'src/hooks/useAuth.ts': `export function useAuth() { + function login(user: string) { + return user; + } + const logout = () => null; + return { login, signOut: logout }; +} +`, + 'src/pages/index.ts': `import useDefaultActivity from '../composables/use-default-activity'; +import { useAuth } from '../hooks/useAuth'; +export function go() { + const { getDefaultActivityRoute } = useDefaultActivity(); + const { login, signOut: leave } = useAuth(); + login('ada'); + leave(); + return getDefaultActivityRoute('x'); +} +`, + }; + 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 }); +}); + +describe('names destructured from a call', () => { + it('reach what the callee returns under that key', () => { + const go = cg.getNodesInFile('src/pages/index.ts').find((n) => n.name === 'go')!; + const targets = cg + .getOutgoingEdges(go.id) + .filter((e) => e.kind === 'calls') + .map((e) => `${cg.getNode(e.target)!.filePath}:${cg.getNode(e.target)!.name}`); + expect(targets).toContain('src/composables/use-default-activity.ts:getDefaultActivityRoute'); + expect(targets).toContain('src/hooks/useAuth.ts:login'); + }); +}); + +it('ignores actions returned only by a nested function', () => { + const run = cg.getNodesInFile('src/pages/nested.ts').find((n) => n.name === 'runNested')!; + const targets = cg.getOutgoingEdges(run.id).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.name); + expect(targets).not.toContain('hiddenAction'); +}); diff --git a/__tests__/esm-unexported-binding.test.ts b/__tests__/esm-unexported-binding.test.ts new file mode 100644 index 0000000000..05573340e7 --- /dev/null +++ b/__tests__/esm-unexported-binding.test.ts @@ -0,0 +1,77 @@ +/** + * A top-level binding an ES module doesn't export can't be named from another + * file. sveltekit's `generate_manifest.js` keeps an unexported `resolve` that + * other files' `resolve(…)` calls went to; typeorm's test inputs' local + * `ColumnMetadata` took `import { ColumnMetadata } from "typeorm"`. Exported + * forms all still count: the `export` keyword, `export { a as b }`, + * `export default x`, `export default { a, b }`; so do classic scripts, + * CommonJS files and functions assigned onto a prototype. + */ +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-esm-unexported-')); + const files: Record = { + 'package.json': JSON.stringify({ name: 'app', type: 'module' }), + 'src/manifest.js': `import { join } from 'node:path'; +function resolve(event) { return join(event); } +export function manifest() { return resolve('x'); } +`, + 'src/adapters.js': `import { a } from './a.js'; +function getAdapter(name) { return name; } +export default { getAdapter }; +`, + 'src/listed.js': `import { a } from './a.js'; +function helper() { return 1; } +export { helper }; +`, + 'src/proto.js': `import { a } from './a.js'; +function Params() {} +const prototype = Params.prototype; +prototype.render = function render() { return ''; }; +export default Params; +`, + 'src/a.js': `export const a = 1; +`, + 'src/use.js': `import { helper } from './listed.js'; +import Params from './proto.js'; +export async function handle(event) { + await resolve(event); + getAdapter('xhr'); + helper(); + new Params().render(); +} +`, + }; + 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 }); +}); + +describe('unexported ES module bindings', () => { + it('are not reached from another file; every export form still is', () => { + const handle = cg.getNodesInFile('src/use.js').find((n) => n.name === 'handle')!; + const targets = cg + .getOutgoingEdges(handle.id) + .filter((e) => e.kind === 'calls') + .map((e) => `${cg.getNode(e.target)!.filePath}:${cg.getNode(e.target)!.name}`); + expect(targets).not.toContain('src/manifest.js:resolve'); + expect(targets).toContain('src/adapters.js:getAdapter'); + expect(targets).toContain('src/listed.js:helper'); + expect(targets).toContain('src/proto.js:render'); + }); +}); diff --git a/__tests__/evaluation/edge-cases.ts b/__tests__/evaluation/edge-cases.ts index a325896074..faeb15a052 100644 --- a/__tests__/evaluation/edge-cases.ts +++ b/__tests__/evaluation/edge-cases.ts @@ -92,6 +92,62 @@ export const PRECISION_CORPORA: Record = { }; export const edgeCases: EdgeCase[] = [ + { + id: 'exposed-literal-trim-jdbc', corpus: 'exposed', kind: 'calls', + from: { file: 'exposed-tests/src/test/kotlin/org/jetbrains/exposed/v1/tests/shared/dml/ReplaceTests.kt', name: 'testReplaceWithExpression' }, + to: { file: 'exposed-core/src/main/kotlin/org/jetbrains/exposed/v1/core/FunctionBuilder.kt', name: 'trim' }, expect: 'present', + source: 'upstream reconcile #345', why: 'The imported literal factory declares a receiver whose ancestry accepts the SQL trim extension.', + }, + + { + id: 'exposed-literal-trim-r2dbc', corpus: 'exposed', kind: 'calls', + from: { file: 'exposed-r2dbc-tests/src/test/kotlin/org/jetbrains/exposed/v1/r2dbc/sql/tests/shared/dml/ReplaceTests.kt', name: 'testReplaceWithExpression' }, + to: { file: 'exposed-core/src/main/kotlin/org/jetbrains/exposed/v1/core/FunctionBuilder.kt', name: 'trim' }, expect: 'present', + source: 'upstream reconcile #345', why: 'The imported literal factory declares a receiver whose ancestry accepts the SQL trim extension.', + }, + + { + id: 'javalin-fluent-validator-nullable', corpus: 'javalin', kind: 'calls', + from: { file: 'javalin/src/test/java/io/javalin/TestValidation.kt', name: '`custom converter works for null when nullable`' }, + to: { file: 'javalin/src/main/java/io/javalin/validation/Validator.kt', name: 'getOrNull' }, expect: 'present', + source: 'upstream reconcile #345', why: 'The imported queryParamAsClass return type survives the receiver-preserving check call.', + }, + + { + id: 'javalin-fluent-validator-optional', corpus: 'javalin', kind: 'calls', + from: { file: 'javalin/src/test/java/io/javalin/TestValidation.kt', name: '`optional query param value with check works`' }, + to: { file: 'javalin/src/main/java/io/javalin/validation/Validator.kt', name: 'getOrNull' }, expect: 'present', + source: 'upstream reconcile #345', why: 'The imported queryParamAsClass return type survives the receiver-preserving check call.', + }, + + { + id: 'exposed-column-count-extension', corpus: 'exposed', kind: 'calls', + from: { file: 'documentation-website/Writerside/snippets/exposed-dsl/src/main/kotlin/org/example/examples/JoinExamples.kt', name: 'joinAndCount' }, + to: { file: 'exposed-core/src/main/kotlin/org/jetbrains/exposed/v1/core/FunctionBuilder.kt', name: 'count' }, expect: 'present', + source: 'upstream reconcile #345', why: 'The column factory declares Column, whose ancestry accepts the imported SQL extension.', + }, + + { + id: 'exposed-column-substring-extension', corpus: 'exposed', kind: 'calls', + from: { file: 'documentation-website/Writerside/snippets/exposed-dsl/src/main/kotlin/org/example/examples/InsertSelectExamples.kt', name: 'insertSelect' }, + to: { file: 'exposed-core/src/main/kotlin/org/jetbrains/exposed/v1/core/FunctionBuilder.kt', name: 'substring' }, expect: 'present', + source: 'upstream reconcile #345', why: 'The column factory declares Column, whose ancestry accepts the imported SQL extension.', + }, + + { + id: 'exposed-column-contains-extension', corpus: 'exposed', kind: 'calls', + from: { file: 'documentation-website/Writerside/snippets/exposed-data-types/src/main/kotlin/org/example/examples/JSONandJSONBExamples.kt', name: 'useContains' }, + to: { file: 'exposed-json/src/main/kotlin/org/jetbrains/exposed/v1/json/JsonConditions.kt', name: 'contains' }, expect: 'present', + source: 'upstream reconcile #345', why: 'The column factory declares Column, whose ancestry accepts the imported SQL extension.', + }, + + { + id: 'exposed-column-not-query-count', corpus: 'exposed', kind: 'calls', + from: { file: 'documentation-website/Writerside/snippets/exposed-dsl/src/main/kotlin/org/example/examples/JoinExamples.kt', name: 'joinAndCount' }, + to: { file: 'exposed-core/src/main/kotlin/org/jetbrains/exposed/v1/core/AbstractQuery.kt', name: 'count' }, expect: 'absent', + source: 'upstream reconcile #345', why: 'Counting a declared Column calls its imported extension, not AbstractQuery.count.', + }, + // --- javalin: a Kotlin file importing a Java class's static member --- { id: 'javalin-kotlin-imports-java-member', corpus: 'javalin', kind: 'calls', diff --git a/__tests__/evaluation/results/precision-exposed-2155404-2e0b2a0b-chain-confidence.json b/__tests__/evaluation/results/precision-exposed-2155404-2e0b2a0b-chain-confidence.json new file mode 100644 index 0000000000..3773793e4a --- /dev/null +++ b/__tests__/evaluation/results/precision-exposed-2155404-2e0b2a0b-chain-confidence.json @@ -0,0 +1,48 @@ +{ + "timestamp": "2026-09-30T20:22:29.751Z", + "corpus": "exposed", + "repo": "https://github.com/JetBrains/Exposed.git", + "commit": "2155404", + "codegraphSha": "2e0b2a0b", + "summary": { + "absentCases": 1, + "absentHeld": 1, + "presentCases": 1, + "presentHeld": 1, + "unjudgeable": 0 + }, + "resolvedBy": { + "exact-match": 34422, + "(none)": 26363, + "import": 7194, + "instance-method": 2779, + "function-ref": 2481, + "framework": 2277, + "file-path": 229, + "qualified-name": 175 + }, + "edges": 75920, + "results": [ + { + "caseId": "exposed-wildcard-package-receiver", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:f40372af7132a6726971d7163deef334", + "to": "method:71e425ce8fd2d25d06c04cf318012e1a", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "exposed-wildcard-not-other-package", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + } + ], + "sourceState": "2e0b2a0b plus final chain positioning, heuristic confidence and Unicode return-line fixes" +} diff --git a/__tests__/evaluation/results/precision-exposed-2155404-4870612d-ast-chain-review.json b/__tests__/evaluation/results/precision-exposed-2155404-4870612d-ast-chain-review.json new file mode 100644 index 0000000000..48a0c894b4 --- /dev/null +++ b/__tests__/evaluation/results/precision-exposed-2155404-4870612d-ast-chain-review.json @@ -0,0 +1,48 @@ +{ + "timestamp": "2026-09-30T20:53:56.685Z", + "corpus": "exposed", + "repo": "https://github.com/JetBrains/Exposed.git", + "commit": "2155404", + "codegraphSha": "4870612d", + "summary": { + "absentCases": 1, + "absentHeld": 1, + "presentCases": 1, + "presentHeld": 1, + "unjudgeable": 0 + }, + "resolvedBy": { + "exact-match": 34356, + "(none)": 26363, + "import": 7194, + "instance-method": 2779, + "function-ref": 2481, + "framework": 2277, + "file-path": 229, + "qualified-name": 175 + }, + "edges": 75854, + "results": [ + { + "caseId": "exposed-wildcard-package-receiver", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:f40372af7132a6726971d7163deef334", + "to": "method:71e425ce8fd2d25d06c04cf318012e1a", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "exposed-wildcard-not-other-package", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + } + ], + "sourceState": "4870612d plus AST callee positions, imported return-chain confidence ordering and multiline receiver evidence" +} diff --git a/__tests__/evaluation/results/precision-exposed-2155404-57a6c3de-review-fixes.json b/__tests__/evaluation/results/precision-exposed-2155404-57a6c3de-review-fixes.json new file mode 100644 index 0000000000..9d706a3d6b --- /dev/null +++ b/__tests__/evaluation/results/precision-exposed-2155404-57a6c3de-review-fixes.json @@ -0,0 +1,48 @@ +{ + "timestamp": "2026-09-30T19:49:47.725Z", + "corpus": "exposed", + "repo": "https://github.com/JetBrains/Exposed.git", + "commit": "2155404", + "codegraphSha": "57a6c3de", + "summary": { + "absentCases": 1, + "absentHeld": 1, + "presentCases": 1, + "presentHeld": 1, + "unjudgeable": 0 + }, + "resolvedBy": { + "exact-match": 34429, + "(none)": 26363, + "import": 7194, + "instance-method": 2779, + "function-ref": 2481, + "framework": 2277, + "file-path": 229, + "qualified-name": 175 + }, + "edges": 75927, + "results": [ + { + "caseId": "exposed-wildcard-package-receiver", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:f40372af7132a6726971d7163deef334", + "to": "method:71e425ce8fd2d25d06c04cf318012e1a", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "exposed-wildcard-not-other-package", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + } + ], + "sourceState": "57a6c3de plus PR345 review fixes; isolated clean native build" +} diff --git a/__tests__/evaluation/results/precision-exposed-2155404-b04f15c8-reconcile-2172.json b/__tests__/evaluation/results/precision-exposed-2155404-b04f15c8-reconcile-2172.json new file mode 100644 index 0000000000..e4bd25b911 --- /dev/null +++ b/__tests__/evaluation/results/precision-exposed-2155404-b04f15c8-reconcile-2172.json @@ -0,0 +1,48 @@ +{ + "timestamp": "2026-09-30T18:51:52.832Z", + "corpus": "exposed", + "repo": "https://github.com/JetBrains/Exposed.git", + "commit": "2155404", + "codegraphSha": "b04f15c8+reconcile-2172", + "summary": { + "absentCases": 1, + "absentHeld": 1, + "presentCases": 1, + "presentHeld": 1, + "unjudgeable": 0 + }, + "resolvedBy": { + "exact-match": 32932, + "(none)": 26363, + "import": 7194, + "instance-method": 2773, + "function-ref": 2481, + "framework": 2277, + "file-path": 229, + "qualified-name": 175 + }, + "edges": 74424, + "results": [ + { + "caseId": "exposed-wildcard-package-receiver", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:f40372af7132a6726971d7163deef334", + "to": "method:71e425ce8fd2d25d06c04cf318012e1a", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "exposed-wildcard-not-other-package", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + } + ], + "sourceState": "Uncommitted upstream #2157-#2172 reconciliation based on b04f15c8, including native resolver ports." +} diff --git a/__tests__/evaluation/results/precision-exposed-2155404-ce9f2814-receiver-review.json b/__tests__/evaluation/results/precision-exposed-2155404-ce9f2814-receiver-review.json new file mode 100644 index 0000000000..99ce1dc08f --- /dev/null +++ b/__tests__/evaluation/results/precision-exposed-2155404-ce9f2814-receiver-review.json @@ -0,0 +1,120 @@ +{ + "timestamp": "2026-09-30T21:40:54.967Z", + "corpus": "exposed", + "repo": "https://github.com/JetBrains/Exposed.git", + "commit": "2155404", + "codegraphSha": "ce9f2814", + "summary": { + "absentCases": 2, + "absentHeld": 2, + "presentCases": 6, + "presentHeld": 6, + "unjudgeable": 0 + }, + "resolvedBy": { + "exact-match": 34365, + "(none)": 26363, + "import": 7194, + "instance-method": 2779, + "function-ref": 2481, + "framework": 2277, + "file-path": 229, + "qualified-name": 175 + }, + "edges": 75863, + "results": [ + { + "caseId": "exposed-literal-trim-jdbc", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:b50c90376f77d8d0aca5d309ac09f69c", + "to": "method:9d77982d7d95880e2a54c4893b86a838", + "resolvedBy": "exact-match" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "exposed-literal-trim-r2dbc", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:05217ebf2e2d2da1556291c5c369839a", + "to": "method:9d77982d7d95880e2a54c4893b86a838", + "resolvedBy": "exact-match" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "exposed-column-count-extension", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:941657a44efa96b0b33ffbd2175bb09b", + "to": "method:495855c626bbbf0a54e3d191aa2fae92", + "resolvedBy": "exact-match" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "exposed-column-substring-extension", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:33263165fe66809c5f378e014e92784d", + "to": "method:8d9c8fca9a37a0223dc1a4ea8cdaccaa", + "resolvedBy": "exact-match" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "exposed-column-contains-extension", + "expect": "present", + "pass": true, + "found": [ + { + "from": "function:401d37a830cdf0a2075a04679534d5d6", + "to": "method:0d2884b38da4f463426a16b42399df20", + "resolvedBy": "exact-match" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "exposed-column-not-query-count", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "exposed-wildcard-package-receiver", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:f40372af7132a6726971d7163deef334", + "to": "method:71e425ce8fd2d25d06c04cf318012e1a", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "exposed-wildcard-not-other-package", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + } + ], + "sourceState": "ce9f2814 plus bounded Kotlin receiver inference, lexical scope guards, exact imported factory identity and hypothesis confidence propagation" +} diff --git a/__tests__/evaluation/results/precision-exposed-2155404-ef6262fd-scope-followups.json b/__tests__/evaluation/results/precision-exposed-2155404-ef6262fd-scope-followups.json new file mode 100644 index 0000000000..760fe4872f --- /dev/null +++ b/__tests__/evaluation/results/precision-exposed-2155404-ef6262fd-scope-followups.json @@ -0,0 +1,48 @@ +{ + "timestamp": "2026-09-30T19:00:53.485Z", + "corpus": "exposed", + "repo": "https://github.com/JetBrains/Exposed.git", + "commit": "2155404", + "codegraphSha": "ef6262fd+scope-followups", + "summary": { + "absentCases": 1, + "absentHeld": 1, + "presentCases": 1, + "presentHeld": 1, + "unjudgeable": 0 + }, + "resolvedBy": { + "exact-match": 34429, + "(none)": 26363, + "import": 7194, + "instance-method": 2773, + "function-ref": 2481, + "framework": 2277, + "file-path": 229, + "qualified-name": 175 + }, + "edges": 75921, + "results": [ + { + "caseId": "exposed-wildcard-package-receiver", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:f40372af7132a6726971d7163deef334", + "to": "method:71e425ce8fd2d25d06c04cf318012e1a", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "exposed-wildcard-not-other-package", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + } + ], + "sourceState": "Uncommitted Kotlin bulk DSL, receiver SAM and source-validated Companion fixes based on ef6262fd." +} diff --git a/__tests__/evaluation/results/precision-javalin-48bf31c-2e0b2a0b-chain-confidence.json b/__tests__/evaluation/results/precision-javalin-48bf31c-2e0b2a0b-chain-confidence.json new file mode 100644 index 0000000000..ec48d26825 --- /dev/null +++ b/__tests__/evaluation/results/precision-javalin-48bf31c-2e0b2a0b-chain-confidence.json @@ -0,0 +1,47 @@ +{ + "timestamp": "2026-09-30T20:22:05.123Z", + "corpus": "javalin", + "repo": "https://github.com/javalin/javalin.git", + "commit": "48bf31c", + "codegraphSha": "2e0b2a0b", + "summary": { + "absentCases": 1, + "absentHeld": 1, + "presentCases": 1, + "presentHeld": 1, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 8159, + "exact-match": 4508, + "import": 2970, + "instance-method": 1807, + "function-ref": 19, + "file-path": 8, + "fuzzy": 1 + }, + "edges": 17472, + "results": [ + { + "caseId": "javalin-kotlin-imports-java-member", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:ef8b70a9b486ddf107a0a782595685f5", + "to": "method:8eb17ccb9967512be8c4aed8f145f350", + "resolvedBy": "import" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "javalin-bare-get-not-test-client", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + } + ], + "sourceState": "2e0b2a0b plus final chain positioning, heuristic confidence and Unicode return-line fixes" +} diff --git a/__tests__/evaluation/results/precision-javalin-48bf31c-4870612d-ast-chain-review.json b/__tests__/evaluation/results/precision-javalin-48bf31c-4870612d-ast-chain-review.json new file mode 100644 index 0000000000..2e56ff6b6d --- /dev/null +++ b/__tests__/evaluation/results/precision-javalin-48bf31c-4870612d-ast-chain-review.json @@ -0,0 +1,47 @@ +{ + "timestamp": "2026-09-30T20:53:32.321Z", + "corpus": "javalin", + "repo": "https://github.com/javalin/javalin.git", + "commit": "48bf31c", + "codegraphSha": "4870612d", + "summary": { + "absentCases": 1, + "absentHeld": 1, + "presentCases": 1, + "presentHeld": 1, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 8159, + "exact-match": 4515, + "import": 2970, + "instance-method": 1807, + "function-ref": 19, + "file-path": 8, + "fuzzy": 1 + }, + "edges": 17479, + "results": [ + { + "caseId": "javalin-kotlin-imports-java-member", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:ef8b70a9b486ddf107a0a782595685f5", + "to": "method:8eb17ccb9967512be8c4aed8f145f350", + "resolvedBy": "import" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "javalin-bare-get-not-test-client", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + } + ], + "sourceState": "4870612d plus AST callee positions, imported return-chain confidence ordering and multiline receiver evidence" +} diff --git a/__tests__/evaluation/results/precision-javalin-48bf31c-57a6c3de-review-fixes.json b/__tests__/evaluation/results/precision-javalin-48bf31c-57a6c3de-review-fixes.json new file mode 100644 index 0000000000..55ae7c502b --- /dev/null +++ b/__tests__/evaluation/results/precision-javalin-48bf31c-57a6c3de-review-fixes.json @@ -0,0 +1,47 @@ +{ + "timestamp": "2026-09-30T19:49:23.838Z", + "corpus": "javalin", + "repo": "https://github.com/javalin/javalin.git", + "commit": "48bf31c", + "codegraphSha": "57a6c3de", + "summary": { + "absentCases": 1, + "absentHeld": 1, + "presentCases": 1, + "presentHeld": 1, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 8159, + "exact-match": 4508, + "import": 2970, + "instance-method": 1807, + "function-ref": 19, + "file-path": 8, + "fuzzy": 1 + }, + "edges": 17472, + "results": [ + { + "caseId": "javalin-kotlin-imports-java-member", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:ef8b70a9b486ddf107a0a782595685f5", + "to": "method:8eb17ccb9967512be8c4aed8f145f350", + "resolvedBy": "import" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "javalin-bare-get-not-test-client", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + } + ], + "sourceState": "57a6c3de plus PR345 review fixes; isolated clean native build" +} diff --git a/__tests__/evaluation/results/precision-javalin-48bf31c-b04f15c8-reconcile-2172.json b/__tests__/evaluation/results/precision-javalin-48bf31c-b04f15c8-reconcile-2172.json new file mode 100644 index 0000000000..87ae94733a --- /dev/null +++ b/__tests__/evaluation/results/precision-javalin-48bf31c-b04f15c8-reconcile-2172.json @@ -0,0 +1,47 @@ +{ + "timestamp": "2026-09-30T18:51:29.271Z", + "corpus": "javalin", + "repo": "https://github.com/javalin/javalin.git", + "commit": "48bf31c", + "codegraphSha": "b04f15c8+reconcile-2172", + "summary": { + "absentCases": 1, + "absentHeld": 1, + "presentCases": 1, + "presentHeld": 1, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 8159, + "exact-match": 4500, + "import": 2970, + "instance-method": 1805, + "function-ref": 19, + "file-path": 8, + "fuzzy": 1 + }, + "edges": 17462, + "results": [ + { + "caseId": "javalin-kotlin-imports-java-member", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:ef8b70a9b486ddf107a0a782595685f5", + "to": "method:8eb17ccb9967512be8c4aed8f145f350", + "resolvedBy": "import" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "javalin-bare-get-not-test-client", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + } + ], + "sourceState": "Uncommitted upstream #2157-#2172 reconciliation based on b04f15c8, including native resolver ports." +} diff --git a/__tests__/evaluation/results/precision-javalin-48bf31c-ce9f2814-receiver-review.json b/__tests__/evaluation/results/precision-javalin-48bf31c-ce9f2814-receiver-review.json new file mode 100644 index 0000000000..e065214973 --- /dev/null +++ b/__tests__/evaluation/results/precision-javalin-48bf31c-ce9f2814-receiver-review.json @@ -0,0 +1,73 @@ +{ + "timestamp": "2026-09-30T21:40:29.396Z", + "corpus": "javalin", + "repo": "https://github.com/javalin/javalin.git", + "commit": "48bf31c", + "codegraphSha": "ce9f2814", + "summary": { + "absentCases": 1, + "absentHeld": 1, + "presentCases": 3, + "presentHeld": 3, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 8159, + "exact-match": 4517, + "import": 2970, + "instance-method": 1807, + "function-ref": 19, + "file-path": 8, + "fuzzy": 1 + }, + "edges": 17481, + "results": [ + { + "caseId": "javalin-fluent-validator-nullable", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:bf4ae208fbd820c105bee964c7b27a6a", + "to": "method:fe7a51f0d2260237d9a5d95284e0b366", + "resolvedBy": "exact-match" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "javalin-fluent-validator-optional", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:7bdb347a57c4e38e31e33bfb6f6c6c54", + "to": "method:fe7a51f0d2260237d9a5d95284e0b366", + "resolvedBy": "exact-match" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "javalin-kotlin-imports-java-member", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:ef8b70a9b486ddf107a0a782595685f5", + "to": "method:8eb17ccb9967512be8c4aed8f145f350", + "resolvedBy": "import" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "javalin-bare-get-not-test-client", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + } + ], + "sourceState": "ce9f2814 plus bounded Kotlin receiver inference, lexical scope guards, exact imported factory identity and hypothesis confidence propagation" +} diff --git a/__tests__/evaluation/results/precision-javalin-48bf31c-ef6262fd-scope-followups.json b/__tests__/evaluation/results/precision-javalin-48bf31c-ef6262fd-scope-followups.json new file mode 100644 index 0000000000..e629076d49 --- /dev/null +++ b/__tests__/evaluation/results/precision-javalin-48bf31c-ef6262fd-scope-followups.json @@ -0,0 +1,47 @@ +{ + "timestamp": "2026-09-30T19:00:29.792Z", + "corpus": "javalin", + "repo": "https://github.com/javalin/javalin.git", + "commit": "48bf31c", + "codegraphSha": "ef6262fd+scope-followups", + "summary": { + "absentCases": 1, + "absentHeld": 1, + "presentCases": 1, + "presentHeld": 1, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 8159, + "exact-match": 4502, + "import": 2970, + "instance-method": 1805, + "function-ref": 19, + "file-path": 8, + "fuzzy": 1 + }, + "edges": 17464, + "results": [ + { + "caseId": "javalin-kotlin-imports-java-member", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:ef8b70a9b486ddf107a0a782595685f5", + "to": "method:8eb17ccb9967512be8c4aed8f145f350", + "resolvedBy": "import" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "javalin-bare-get-not-test-client", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + } + ], + "sourceState": "Uncommitted Kotlin bulk DSL, receiver SAM and source-validated Companion fixes based on ef6262fd." +} diff --git a/__tests__/evaluation/results/precision-petclinic-818c413-2e0b2a0b-chain-confidence.json b/__tests__/evaluation/results/precision-petclinic-818c413-2e0b2a0b-chain-confidence.json new file mode 100644 index 0000000000..accaf75e7b --- /dev/null +++ b/__tests__/evaluation/results/precision-petclinic-818c413-2e0b2a0b-chain-confidence.json @@ -0,0 +1,45 @@ +{ + "timestamp": "2026-09-30T20:22:06.395Z", + "corpus": "petclinic", + "repo": "https://github.com/spring-projects/spring-petclinic.git", + "commit": "818c413", + "codegraphSha": "2e0b2a0b", + "summary": { + "absentCases": 0, + "absentHeld": 0, + "presentCases": 1, + "presentHeld": 1, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 986, + "instance-method": 270, + "exact-match": 169, + "import": 44, + "framework": 10, + "function-ref": 2, + "file-path": 1 + }, + "edges": 1482, + "results": [ + { + "caseId": "petclinic-bounded-type-parameter", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:2160e6191fa145dc29200531c30d1a5d", + "to": "method:efb8586338e2f81ea9d7cb6f694a2f0d", + "resolvedBy": "instance-method" + }, + { + "from": "method:2160e6191fa145dc29200531c30d1a5d", + "to": "method:efb8586338e2f81ea9d7cb6f694a2f0d", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + } + ], + "sourceState": "2e0b2a0b plus final chain positioning, heuristic confidence and Unicode return-line fixes" +} diff --git a/__tests__/evaluation/results/precision-petclinic-818c413-4870612d-ast-chain-review.json b/__tests__/evaluation/results/precision-petclinic-818c413-4870612d-ast-chain-review.json new file mode 100644 index 0000000000..da520ec1d9 --- /dev/null +++ b/__tests__/evaluation/results/precision-petclinic-818c413-4870612d-ast-chain-review.json @@ -0,0 +1,45 @@ +{ + "timestamp": "2026-09-30T20:53:33.601Z", + "corpus": "petclinic", + "repo": "https://github.com/spring-projects/spring-petclinic.git", + "commit": "818c413", + "codegraphSha": "4870612d", + "summary": { + "absentCases": 0, + "absentHeld": 0, + "presentCases": 1, + "presentHeld": 1, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 986, + "instance-method": 270, + "exact-match": 169, + "import": 44, + "framework": 10, + "function-ref": 2, + "file-path": 1 + }, + "edges": 1482, + "results": [ + { + "caseId": "petclinic-bounded-type-parameter", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:2160e6191fa145dc29200531c30d1a5d", + "to": "method:efb8586338e2f81ea9d7cb6f694a2f0d", + "resolvedBy": "instance-method" + }, + { + "from": "method:2160e6191fa145dc29200531c30d1a5d", + "to": "method:efb8586338e2f81ea9d7cb6f694a2f0d", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + } + ], + "sourceState": "4870612d plus AST callee positions, imported return-chain confidence ordering and multiline receiver evidence" +} diff --git a/__tests__/evaluation/results/precision-petclinic-818c413-57a6c3de-review-fixes.json b/__tests__/evaluation/results/precision-petclinic-818c413-57a6c3de-review-fixes.json new file mode 100644 index 0000000000..5ae008a72d --- /dev/null +++ b/__tests__/evaluation/results/precision-petclinic-818c413-57a6c3de-review-fixes.json @@ -0,0 +1,45 @@ +{ + "timestamp": "2026-09-30T19:49:25.105Z", + "corpus": "petclinic", + "repo": "https://github.com/spring-projects/spring-petclinic.git", + "commit": "818c413", + "codegraphSha": "57a6c3de", + "summary": { + "absentCases": 0, + "absentHeld": 0, + "presentCases": 1, + "presentHeld": 1, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 986, + "instance-method": 270, + "exact-match": 169, + "import": 44, + "framework": 10, + "function-ref": 2, + "file-path": 1 + }, + "edges": 1482, + "results": [ + { + "caseId": "petclinic-bounded-type-parameter", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:2160e6191fa145dc29200531c30d1a5d", + "to": "method:efb8586338e2f81ea9d7cb6f694a2f0d", + "resolvedBy": "instance-method" + }, + { + "from": "method:2160e6191fa145dc29200531c30d1a5d", + "to": "method:efb8586338e2f81ea9d7cb6f694a2f0d", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + } + ], + "sourceState": "57a6c3de plus PR345 review fixes; isolated clean native build" +} diff --git a/__tests__/evaluation/results/precision-petclinic-818c413-b04f15c8-reconcile-2172.json b/__tests__/evaluation/results/precision-petclinic-818c413-b04f15c8-reconcile-2172.json new file mode 100644 index 0000000000..cf89d15f87 --- /dev/null +++ b/__tests__/evaluation/results/precision-petclinic-818c413-b04f15c8-reconcile-2172.json @@ -0,0 +1,45 @@ +{ + "timestamp": "2026-09-30T18:51:30.524Z", + "corpus": "petclinic", + "repo": "https://github.com/spring-projects/spring-petclinic.git", + "commit": "818c413", + "codegraphSha": "b04f15c8+reconcile-2172", + "summary": { + "absentCases": 0, + "absentHeld": 0, + "presentCases": 1, + "presentHeld": 1, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 986, + "instance-method": 270, + "exact-match": 169, + "import": 44, + "framework": 10, + "function-ref": 2, + "file-path": 1 + }, + "edges": 1482, + "results": [ + { + "caseId": "petclinic-bounded-type-parameter", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:2160e6191fa145dc29200531c30d1a5d", + "to": "method:efb8586338e2f81ea9d7cb6f694a2f0d", + "resolvedBy": "instance-method" + }, + { + "from": "method:2160e6191fa145dc29200531c30d1a5d", + "to": "method:efb8586338e2f81ea9d7cb6f694a2f0d", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + } + ], + "sourceState": "Uncommitted upstream #2157-#2172 reconciliation based on b04f15c8, including native resolver ports." +} diff --git a/__tests__/evaluation/results/precision-petclinic-818c413-ce9f2814-receiver-review.json b/__tests__/evaluation/results/precision-petclinic-818c413-ce9f2814-receiver-review.json new file mode 100644 index 0000000000..5708fa387f --- /dev/null +++ b/__tests__/evaluation/results/precision-petclinic-818c413-ce9f2814-receiver-review.json @@ -0,0 +1,45 @@ +{ + "timestamp": "2026-09-30T21:40:30.751Z", + "corpus": "petclinic", + "repo": "https://github.com/spring-projects/spring-petclinic.git", + "commit": "818c413", + "codegraphSha": "ce9f2814", + "summary": { + "absentCases": 0, + "absentHeld": 0, + "presentCases": 1, + "presentHeld": 1, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 986, + "instance-method": 270, + "exact-match": 169, + "import": 44, + "framework": 10, + "function-ref": 2, + "file-path": 1 + }, + "edges": 1482, + "results": [ + { + "caseId": "petclinic-bounded-type-parameter", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:2160e6191fa145dc29200531c30d1a5d", + "to": "method:efb8586338e2f81ea9d7cb6f694a2f0d", + "resolvedBy": "instance-method" + }, + { + "from": "method:2160e6191fa145dc29200531c30d1a5d", + "to": "method:efb8586338e2f81ea9d7cb6f694a2f0d", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + } + ], + "sourceState": "ce9f2814 plus bounded Kotlin receiver inference, lexical scope guards, exact imported factory identity and hypothesis confidence propagation" +} diff --git a/__tests__/evaluation/results/precision-petclinic-818c413-ef6262fd-scope-followups.json b/__tests__/evaluation/results/precision-petclinic-818c413-ef6262fd-scope-followups.json new file mode 100644 index 0000000000..e59aab68e5 --- /dev/null +++ b/__tests__/evaluation/results/precision-petclinic-818c413-ef6262fd-scope-followups.json @@ -0,0 +1,45 @@ +{ + "timestamp": "2026-09-30T19:00:31.036Z", + "corpus": "petclinic", + "repo": "https://github.com/spring-projects/spring-petclinic.git", + "commit": "818c413", + "codegraphSha": "ef6262fd+scope-followups", + "summary": { + "absentCases": 0, + "absentHeld": 0, + "presentCases": 1, + "presentHeld": 1, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 986, + "instance-method": 270, + "exact-match": 169, + "import": 44, + "framework": 10, + "function-ref": 2, + "file-path": 1 + }, + "edges": 1482, + "results": [ + { + "caseId": "petclinic-bounded-type-parameter", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:2160e6191fa145dc29200531c30d1a5d", + "to": "method:efb8586338e2f81ea9d7cb6f694a2f0d", + "resolvedBy": "instance-method" + }, + { + "from": "method:2160e6191fa145dc29200531c30d1a5d", + "to": "method:efb8586338e2f81ea9d7cb6f694a2f0d", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + } + ], + "sourceState": "Uncommitted Kotlin bulk DSL, receiver SAM and source-validated Companion fixes based on ef6262fd." +} diff --git a/__tests__/evaluation/results/precision-slim-3675bf6-2e0b2a0b-chain-confidence.json b/__tests__/evaluation/results/precision-slim-3675bf6-2e0b2a0b-chain-confidence.json new file mode 100644 index 0000000000..a347aadafb --- /dev/null +++ b/__tests__/evaluation/results/precision-slim-3675bf6-2e0b2a0b-chain-confidence.json @@ -0,0 +1,40 @@ +{ + "timestamp": "2026-09-30T20:21:57.335Z", + "corpus": "slim", + "repo": "https://github.com/slimphp/Slim.git", + "commit": "3675bf6", + "codegraphSha": "2e0b2a0b", + "summary": { + "absentCases": 0, + "absentHeld": 0, + "presentCases": 1, + "presentHeld": 1, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 2173, + "exact-match": 1443, + "instance-method": 438, + "qualified-name": 306, + "import": 69, + "function-ref": 4, + "file-path": 4 + }, + "edges": 4437, + "results": [ + { + "caseId": "slim-guarded-exception-receiver", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:a6f7723645dbd37b31799db2b356325f", + "to": "method:ea555ef764392cc4779c55819b7860c5", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + } + ], + "sourceState": "2e0b2a0b plus final chain positioning, heuristic confidence and Unicode return-line fixes" +} diff --git a/__tests__/evaluation/results/precision-slim-3675bf6-4870612d-ast-chain-review.json b/__tests__/evaluation/results/precision-slim-3675bf6-4870612d-ast-chain-review.json new file mode 100644 index 0000000000..e41c2c2b35 --- /dev/null +++ b/__tests__/evaluation/results/precision-slim-3675bf6-4870612d-ast-chain-review.json @@ -0,0 +1,40 @@ +{ + "timestamp": "2026-09-30T20:53:24.535Z", + "corpus": "slim", + "repo": "https://github.com/slimphp/Slim.git", + "commit": "3675bf6", + "codegraphSha": "4870612d", + "summary": { + "absentCases": 0, + "absentHeld": 0, + "presentCases": 1, + "presentHeld": 1, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 2173, + "exact-match": 1443, + "instance-method": 438, + "qualified-name": 306, + "import": 69, + "function-ref": 4, + "file-path": 4 + }, + "edges": 4437, + "results": [ + { + "caseId": "slim-guarded-exception-receiver", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:a6f7723645dbd37b31799db2b356325f", + "to": "method:ea555ef764392cc4779c55819b7860c5", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + } + ], + "sourceState": "4870612d plus AST callee positions, imported return-chain confidence ordering and multiline receiver evidence" +} diff --git a/__tests__/evaluation/results/precision-slim-3675bf6-57a6c3de-review-fixes.json b/__tests__/evaluation/results/precision-slim-3675bf6-57a6c3de-review-fixes.json new file mode 100644 index 0000000000..166df0b036 --- /dev/null +++ b/__tests__/evaluation/results/precision-slim-3675bf6-57a6c3de-review-fixes.json @@ -0,0 +1,40 @@ +{ + "timestamp": "2026-09-30T19:49:16.018Z", + "corpus": "slim", + "repo": "https://github.com/slimphp/Slim.git", + "commit": "3675bf6", + "codegraphSha": "57a6c3de", + "summary": { + "absentCases": 0, + "absentHeld": 0, + "presentCases": 1, + "presentHeld": 1, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 2173, + "exact-match": 1443, + "instance-method": 438, + "qualified-name": 306, + "import": 69, + "function-ref": 4, + "file-path": 4 + }, + "edges": 4437, + "results": [ + { + "caseId": "slim-guarded-exception-receiver", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:a6f7723645dbd37b31799db2b356325f", + "to": "method:ea555ef764392cc4779c55819b7860c5", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + } + ], + "sourceState": "57a6c3de plus PR345 review fixes; isolated clean native build" +} diff --git a/__tests__/evaluation/results/precision-slim-3675bf6-b04f15c8-reconcile-2172.json b/__tests__/evaluation/results/precision-slim-3675bf6-b04f15c8-reconcile-2172.json new file mode 100644 index 0000000000..78c30d0224 --- /dev/null +++ b/__tests__/evaluation/results/precision-slim-3675bf6-b04f15c8-reconcile-2172.json @@ -0,0 +1,40 @@ +{ + "timestamp": "2026-09-30T18:51:21.558Z", + "corpus": "slim", + "repo": "https://github.com/slimphp/Slim.git", + "commit": "3675bf6", + "codegraphSha": "b04f15c8+reconcile-2172", + "summary": { + "absentCases": 0, + "absentHeld": 0, + "presentCases": 1, + "presentHeld": 1, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 2173, + "exact-match": 1443, + "instance-method": 438, + "qualified-name": 306, + "import": 69, + "function-ref": 4, + "file-path": 4 + }, + "edges": 4437, + "results": [ + { + "caseId": "slim-guarded-exception-receiver", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:a6f7723645dbd37b31799db2b356325f", + "to": "method:ea555ef764392cc4779c55819b7860c5", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + } + ], + "sourceState": "Uncommitted upstream #2157-#2172 reconciliation based on b04f15c8, including native resolver ports." +} diff --git a/__tests__/evaluation/results/precision-slim-3675bf6-ce9f2814-receiver-review.json b/__tests__/evaluation/results/precision-slim-3675bf6-ce9f2814-receiver-review.json new file mode 100644 index 0000000000..2bd9767600 --- /dev/null +++ b/__tests__/evaluation/results/precision-slim-3675bf6-ce9f2814-receiver-review.json @@ -0,0 +1,40 @@ +{ + "timestamp": "2026-09-30T21:40:21.206Z", + "corpus": "slim", + "repo": "https://github.com/slimphp/Slim.git", + "commit": "3675bf6", + "codegraphSha": "ce9f2814", + "summary": { + "absentCases": 0, + "absentHeld": 0, + "presentCases": 1, + "presentHeld": 1, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 2173, + "exact-match": 1443, + "instance-method": 438, + "qualified-name": 306, + "import": 69, + "function-ref": 4, + "file-path": 4 + }, + "edges": 4437, + "results": [ + { + "caseId": "slim-guarded-exception-receiver", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:a6f7723645dbd37b31799db2b356325f", + "to": "method:ea555ef764392cc4779c55819b7860c5", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + } + ], + "sourceState": "ce9f2814 plus bounded Kotlin receiver inference, lexical scope guards, exact imported factory identity and hypothesis confidence propagation" +} diff --git a/__tests__/evaluation/results/precision-slim-3675bf6-ef6262fd-scope-followups.json b/__tests__/evaluation/results/precision-slim-3675bf6-ef6262fd-scope-followups.json new file mode 100644 index 0000000000..4ee75c120f --- /dev/null +++ b/__tests__/evaluation/results/precision-slim-3675bf6-ef6262fd-scope-followups.json @@ -0,0 +1,40 @@ +{ + "timestamp": "2026-09-30T19:00:22.134Z", + "corpus": "slim", + "repo": "https://github.com/slimphp/Slim.git", + "commit": "3675bf6", + "codegraphSha": "ef6262fd+scope-followups", + "summary": { + "absentCases": 0, + "absentHeld": 0, + "presentCases": 1, + "presentHeld": 1, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 2173, + "exact-match": 1443, + "instance-method": 438, + "qualified-name": 306, + "import": 69, + "function-ref": 4, + "file-path": 4 + }, + "edges": 4437, + "results": [ + { + "caseId": "slim-guarded-exception-receiver", + "expect": "present", + "pass": true, + "found": [ + { + "from": "method:a6f7723645dbd37b31799db2b356325f", + "to": "method:ea555ef764392cc4779c55819b7860c5", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + } + ], + "sourceState": "Uncommitted Kotlin bulk DSL, receiver SAM and source-validated Companion fixes based on ef6262fd." +} diff --git a/__tests__/evaluation/results/precision-vite-8492422-2e0b2a0b-chain-confidence.json b/__tests__/evaluation/results/precision-vite-8492422-2e0b2a0b-chain-confidence.json new file mode 100644 index 0000000000..a760b27b42 --- /dev/null +++ b/__tests__/evaluation/results/precision-vite-8492422-2e0b2a0b-chain-confidence.json @@ -0,0 +1,97 @@ +{ + "timestamp": "2026-09-30T20:22:01.980Z", + "corpus": "vite", + "repo": "https://github.com/vitejs/vite.git", + "commit": "8492422", + "codegraphSha": "2e0b2a0b", + "summary": { + "absentCases": 6, + "absentHeld": 6, + "presentCases": 2, + "presentHeld": 2, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 13114, + "import": 8875, + "exact-match": 4904, + "instance-method": 405, + "qualified-name": 312, + "file-path": 201, + "function-ref": 200, + "fuzzy": 16, + "framework": 8 + }, + "edges": 28035, + "results": [ + { + "caseId": "vite-factory-receiver-control", + "expect": "present", + "pass": true, + "found": [ + { + "from": "function:d321dd128d90578bc69867835ec73ef8", + "to": "method:db73e5f5d9c7af61d0f22c5b9971ced3", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "vite-external-receiver-not-project-method", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-bare-import-self-edge", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-bare-import-getEnv", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-fuzzy-nested-resolveConfig-getEnv", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-fuzzy-nested-build-scan", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-sealed-module-defineConfig", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-control-relative-import", + "expect": "present", + "pass": true, + "found": [ + { + "from": "file:packages/vite/src/node/config.ts", + "to": "function:0c2f36c32c5742aee412978220fd5b23", + "resolvedBy": "import" + } + ], + "missingEndpoints": [] + } + ], + "sourceState": "2e0b2a0b plus final chain positioning, heuristic confidence and Unicode return-line fixes" +} diff --git a/__tests__/evaluation/results/precision-vite-8492422-4870612d-ast-chain-review.json b/__tests__/evaluation/results/precision-vite-8492422-4870612d-ast-chain-review.json new file mode 100644 index 0000000000..c00af64ead --- /dev/null +++ b/__tests__/evaluation/results/precision-vite-8492422-4870612d-ast-chain-review.json @@ -0,0 +1,97 @@ +{ + "timestamp": "2026-09-30T20:53:29.220Z", + "corpus": "vite", + "repo": "https://github.com/vitejs/vite.git", + "commit": "8492422", + "codegraphSha": "4870612d", + "summary": { + "absentCases": 6, + "absentHeld": 6, + "presentCases": 2, + "presentHeld": 2, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 13114, + "import": 8875, + "exact-match": 4904, + "instance-method": 405, + "qualified-name": 312, + "file-path": 201, + "function-ref": 200, + "fuzzy": 16, + "framework": 8 + }, + "edges": 28035, + "results": [ + { + "caseId": "vite-factory-receiver-control", + "expect": "present", + "pass": true, + "found": [ + { + "from": "function:d321dd128d90578bc69867835ec73ef8", + "to": "method:db73e5f5d9c7af61d0f22c5b9971ced3", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "vite-external-receiver-not-project-method", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-bare-import-self-edge", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-bare-import-getEnv", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-fuzzy-nested-resolveConfig-getEnv", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-fuzzy-nested-build-scan", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-sealed-module-defineConfig", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-control-relative-import", + "expect": "present", + "pass": true, + "found": [ + { + "from": "file:packages/vite/src/node/config.ts", + "to": "function:0c2f36c32c5742aee412978220fd5b23", + "resolvedBy": "import" + } + ], + "missingEndpoints": [] + } + ], + "sourceState": "4870612d plus AST callee positions, imported return-chain confidence ordering and multiline receiver evidence" +} diff --git a/__tests__/evaluation/results/precision-vite-8492422-57a6c3de-review-fixes.json b/__tests__/evaluation/results/precision-vite-8492422-57a6c3de-review-fixes.json new file mode 100644 index 0000000000..e355c8a5ff --- /dev/null +++ b/__tests__/evaluation/results/precision-vite-8492422-57a6c3de-review-fixes.json @@ -0,0 +1,97 @@ +{ + "timestamp": "2026-09-30T19:49:20.632Z", + "corpus": "vite", + "repo": "https://github.com/vitejs/vite.git", + "commit": "8492422", + "codegraphSha": "57a6c3de", + "summary": { + "absentCases": 6, + "absentHeld": 6, + "presentCases": 2, + "presentHeld": 2, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 13114, + "import": 8875, + "exact-match": 4904, + "instance-method": 405, + "qualified-name": 312, + "file-path": 201, + "function-ref": 200, + "fuzzy": 16, + "framework": 8 + }, + "edges": 28035, + "results": [ + { + "caseId": "vite-factory-receiver-control", + "expect": "present", + "pass": true, + "found": [ + { + "from": "function:d321dd128d90578bc69867835ec73ef8", + "to": "method:db73e5f5d9c7af61d0f22c5b9971ced3", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "vite-external-receiver-not-project-method", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-bare-import-self-edge", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-bare-import-getEnv", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-fuzzy-nested-resolveConfig-getEnv", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-fuzzy-nested-build-scan", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-sealed-module-defineConfig", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-control-relative-import", + "expect": "present", + "pass": true, + "found": [ + { + "from": "file:packages/vite/src/node/config.ts", + "to": "function:0c2f36c32c5742aee412978220fd5b23", + "resolvedBy": "import" + } + ], + "missingEndpoints": [] + } + ], + "sourceState": "57a6c3de plus PR345 review fixes; isolated clean native build" +} diff --git a/__tests__/evaluation/results/precision-vite-8492422-b04f15c8-reconcile-2172.json b/__tests__/evaluation/results/precision-vite-8492422-b04f15c8-reconcile-2172.json new file mode 100644 index 0000000000..4f928fdffc --- /dev/null +++ b/__tests__/evaluation/results/precision-vite-8492422-b04f15c8-reconcile-2172.json @@ -0,0 +1,97 @@ +{ + "timestamp": "2026-09-30T18:51:26.117Z", + "corpus": "vite", + "repo": "https://github.com/vitejs/vite.git", + "commit": "8492422", + "codegraphSha": "b04f15c8+reconcile-2172", + "summary": { + "absentCases": 6, + "absentHeld": 6, + "presentCases": 2, + "presentHeld": 2, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 13114, + "import": 8875, + "exact-match": 4904, + "instance-method": 405, + "qualified-name": 312, + "file-path": 201, + "function-ref": 200, + "fuzzy": 16, + "framework": 8 + }, + "edges": 28035, + "results": [ + { + "caseId": "vite-factory-receiver-control", + "expect": "present", + "pass": true, + "found": [ + { + "from": "function:d321dd128d90578bc69867835ec73ef8", + "to": "method:db73e5f5d9c7af61d0f22c5b9971ced3", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "vite-external-receiver-not-project-method", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-bare-import-self-edge", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-bare-import-getEnv", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-fuzzy-nested-resolveConfig-getEnv", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-fuzzy-nested-build-scan", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-sealed-module-defineConfig", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-control-relative-import", + "expect": "present", + "pass": true, + "found": [ + { + "from": "file:packages/vite/src/node/config.ts", + "to": "function:0c2f36c32c5742aee412978220fd5b23", + "resolvedBy": "import" + } + ], + "missingEndpoints": [] + } + ], + "sourceState": "Uncommitted upstream #2157-#2172 reconciliation based on b04f15c8, including native resolver ports." +} diff --git a/__tests__/evaluation/results/precision-vite-8492422-ce9f2814-receiver-review.json b/__tests__/evaluation/results/precision-vite-8492422-ce9f2814-receiver-review.json new file mode 100644 index 0000000000..1a3fa7b037 --- /dev/null +++ b/__tests__/evaluation/results/precision-vite-8492422-ce9f2814-receiver-review.json @@ -0,0 +1,97 @@ +{ + "timestamp": "2026-09-30T21:40:26.100Z", + "corpus": "vite", + "repo": "https://github.com/vitejs/vite.git", + "commit": "8492422", + "codegraphSha": "ce9f2814", + "summary": { + "absentCases": 6, + "absentHeld": 6, + "presentCases": 2, + "presentHeld": 2, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 13114, + "import": 8875, + "exact-match": 4904, + "instance-method": 405, + "qualified-name": 312, + "file-path": 201, + "function-ref": 200, + "fuzzy": 16, + "framework": 8 + }, + "edges": 28035, + "results": [ + { + "caseId": "vite-factory-receiver-control", + "expect": "present", + "pass": true, + "found": [ + { + "from": "function:d321dd128d90578bc69867835ec73ef8", + "to": "method:db73e5f5d9c7af61d0f22c5b9971ced3", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "vite-external-receiver-not-project-method", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-bare-import-self-edge", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-bare-import-getEnv", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-fuzzy-nested-resolveConfig-getEnv", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-fuzzy-nested-build-scan", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-sealed-module-defineConfig", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-control-relative-import", + "expect": "present", + "pass": true, + "found": [ + { + "from": "file:packages/vite/src/node/config.ts", + "to": "function:0c2f36c32c5742aee412978220fd5b23", + "resolvedBy": "import" + } + ], + "missingEndpoints": [] + } + ], + "sourceState": "ce9f2814 plus bounded Kotlin receiver inference, lexical scope guards, exact imported factory identity and hypothesis confidence propagation" +} diff --git a/__tests__/evaluation/results/precision-vite-8492422-ef6262fd-scope-followups.json b/__tests__/evaluation/results/precision-vite-8492422-ef6262fd-scope-followups.json new file mode 100644 index 0000000000..dbbe324f7f --- /dev/null +++ b/__tests__/evaluation/results/precision-vite-8492422-ef6262fd-scope-followups.json @@ -0,0 +1,97 @@ +{ + "timestamp": "2026-09-30T19:00:26.705Z", + "corpus": "vite", + "repo": "https://github.com/vitejs/vite.git", + "commit": "8492422", + "codegraphSha": "ef6262fd+scope-followups", + "summary": { + "absentCases": 6, + "absentHeld": 6, + "presentCases": 2, + "presentHeld": 2, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 13114, + "import": 8875, + "exact-match": 4904, + "instance-method": 405, + "qualified-name": 312, + "file-path": 201, + "function-ref": 200, + "fuzzy": 16, + "framework": 8 + }, + "edges": 28035, + "results": [ + { + "caseId": "vite-factory-receiver-control", + "expect": "present", + "pass": true, + "found": [ + { + "from": "function:d321dd128d90578bc69867835ec73ef8", + "to": "method:db73e5f5d9c7af61d0f22c5b9971ced3", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "vite-external-receiver-not-project-method", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-bare-import-self-edge", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-bare-import-getEnv", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-fuzzy-nested-resolveConfig-getEnv", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-fuzzy-nested-build-scan", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-sealed-module-defineConfig", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-control-relative-import", + "expect": "present", + "pass": true, + "found": [ + { + "from": "file:packages/vite/src/node/config.ts", + "to": "function:0c2f36c32c5742aee412978220fd5b23", + "resolvedBy": "import" + } + ], + "missingEndpoints": [] + } + ], + "sourceState": "Uncommitted Kotlin bulk DSL, receiver SAM and source-validated Companion fixes based on ef6262fd." +} diff --git a/__tests__/external-declared-receiver.test.ts b/__tests__/external-declared-receiver.test.ts new file mode 100644 index 0000000000..159ec2935f --- /dev/null +++ b/__tests__/external-declared-receiver.test.ts @@ -0,0 +1,93 @@ +/** + * A receiver whose declared type is one the project doesn't declare — + * `List list`, `var sb = new StringBuilder()` — calls that outside + * type's method, never a project type's same-named one: gson's `list.add(…)` + * went to a project list wrapper's `add`, commons-lang's `s.length()` to a + * writer's. A Python keyword argument (`prefix=IPNetwork(…),` inside a call) + * is not a binding of `prefix`. + */ +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-external-declared-')); + const files: Record = { + 'src/main/java/app/NonNullElementWrapperList.java': `package app; +public class NonNullElementWrapperList { + public boolean add(Object o) { return true; } +} +`, + 'src/main/java/app/Use.java': `package app; +import java.util.ArrayList; +import java.util.List; +public class Use { + public void run() { + List list = new ArrayList<>(); + list.add("x"); + } +} +`, + 'app/models.py': `class Prefix: + def get_available_ip_count(self): + return 0 +`, + 'app/main.py': `class API: + def run(self): + return self.dummyView.reverse_action('x') +`, + 'app/view_tests.py': `class DummyView: + def reverse_action(self, name): + return name + +class Harness: + def run(self): + return self.dummyView.reverse_action('x') +`, + 'app/tests.py': `from app.models import Prefix +from netaddr import IPNetwork + + +def test_count(): + prefix = Prefix( + prefix=IPNetwork('192.0.2.0/24'), + ) + return prefix.get_available_ip_count() +`, + }; + 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 callsIn = (file: string, fn: string): string[] => { + const node = cg.getNodesInFile(file).find((n) => n.name === fn)!; + return cg.getOutgoingEdges(node.id).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.qualifiedName); +}; + +describe('a receiver declared with an outside type', () => { + it('Java: `List list` calls List.add, not a project class’s add', () => { + expect(callsIn('src/main/java/app/Use.java', 'run').some((t) => t.endsWith('NonNullElementWrapperList::add'))).toBe(false); + }); + + it('Python: a production receiver guess excludes test methods; a test caller keeps them', () => { + expect(callsIn('app/main.py', 'run').some((t) => t.endsWith('DummyView::reverse_action'))).toBe(false); + expect(callsIn('app/view_tests.py', 'run').some((t) => t.endsWith('DummyView::reverse_action'))).toBe(true); + }); + + it('Python: a keyword argument is not the receiver’s binding', () => { + expect(callsIn('app/tests.py', 'test_count').some((t) => t.endsWith('Prefix::get_available_ip_count'))).toBe(true); + }); +}); diff --git a/__tests__/external-type-receiver.test.ts b/__tests__/external-type-receiver.test.ts new file mode 100644 index 0000000000..a14db4904b --- /dev/null +++ b/__tests__/external-type-receiver.test.ts @@ -0,0 +1,57 @@ +/** + * `Name.method()` where `Name` is a type the project doesn't declare — + * `Arrays.asList(…)`, `Object.assign(…)`, Delphi's `Exception.Create(…)` — is + * a call into that outside type, never the project's one same-named method. + * commons-lang's `Integer.valueOf(…)` went to `StringUtils.valueOf` 804 + * times; horse's `Exception.Create` to its `EHorseException.Create`. Go's + * exported variables (`FormPost.Bind`) and Pascal's capitalized locals and + * parameters (`LRequest.GetValue`) are not type names. + */ +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-external-type-')); + const files: Record = { + 'src/main/java/app/StringUtils.java': `package app; +public class StringUtils { + public static String valueOf(Object o) { return String.valueOf(o); } +} +`, + 'src/main/java/app/Use.java': `package app; +public class Use { + public int run() { + Integer.valueOf(1); + return StringUtils.valueOf(2).length(); + } +} +`, + }; + 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 }); +}); + +describe('a call on a type from outside the project', () => { + it('is not the project’s same-named method; the project’s own type still is', () => { + const run = cg.getNodesInFile('src/main/java/app/Use.java').find((n) => n.name === 'run')!; + const valueOf = cg + .getOutgoingEdges(run.id) + .filter((e) => e.kind === 'calls' && cg.getNode(e.target)!.name === 'valueOf'); + // Exactly one: `StringUtils.valueOf(2)`, not `Integer.valueOf(1)`. + expect(valueOf.map((e) => e.line)).toEqual([5]); + }); +}); diff --git a/__tests__/fastendpoints-routes.test.ts b/__tests__/fastendpoints-routes.test.ts new file mode 100644 index 0000000000..004b495522 --- /dev/null +++ b/__tests__/fastendpoints-routes.test.ts @@ -0,0 +1,105 @@ +/** + * FastEndpoints routes: an endpoint class declares its verb and path in + * `Configure()` — `Get("/Contributors")`, `Post(CreateContributorRequest.Route)` + * with the constant in the request's own file — and handles the request in + * its own `HandleAsync` / `ExecuteAsync`. ardalis/CleanArchitecture had no + * routes at all. A minimal API's `app.MapGet("api/todos", …)` serves + * `/api/todos`. + */ +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-fastendpoints-')); + const files: Record = { + 'src/Web/Contributors/Create.CreateContributorRequest.cs': `namespace Web.Contributors; + +public class CreateContributorRequest +{ + public const string Route = "/Contributors"; + public string Name { get; set; } = ""; +} +`, + 'src/Web/Contributors/Create.cs': `namespace Web.Contributors; + +public class Create : Endpoint +{ + public override void Configure() + { + Post(CreateContributorRequest.Route); + AllowAnonymous(); + } + + public override async Task ExecuteAsync(CreateContributorRequest request, CancellationToken ct) + { + } +} +`, + 'src/Web/Contributors/List.cs': `namespace Web.Contributors; + +public class List : EndpointWithoutRequest +{ + public override void Configure() + { + Get("/Contributors"); + AllowAnonymous(); + } + + public override async Task HandleAsync(CancellationToken ct) + { + Get("/not-a-route"); + } +} +`, + 'src/Web/Projects/List.cs': `namespace Web.Projects; + +public class List : EndpointWithoutRequest +{ + public override void Configure() + { + Get($"/{nameof(Project)}s"); + } + + public override async Task HandleAsync(CancellationToken ct) + { + } +} +`, + 'src/Api/Program.cs': `var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); +app.MapGet("api/todos", GetTodos); +`, + }; + 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 handlerOf = (file: string): string[] => { + const route = cg.getNodesByKind('route').find((r) => r.filePath === file)!; + return cg.getOutgoingEdges(route.id).map((e) => cg.getNode(e.target)!.qualifiedName); +}; + +describe('FastEndpoints and minimal API routes', () => { + it('names each endpoint by its path, constant paths included, and links its own handler', () => { + const names = cg.getNodesByKind('route').map((r) => r.name).sort(); + expect(names).toEqual(['GET /Contributors', 'GET /Projects', 'GET /api/todos', 'POST /Contributors']); + expect(handlerOf('src/Web/Contributors/Create.cs')).toEqual(['Web.Contributors::Create::ExecuteAsync']); + // Two classes named `List`: each route reaches its own class's handler. + expect(handlerOf('src/Web/Contributors/List.cs')).toEqual(['Web.Contributors::List::HandleAsync']); + expect(handlerOf('src/Web/Projects/List.cs')).toEqual(['Web.Projects::List::HandleAsync']); + }); +}); diff --git a/__tests__/fixtures/golden/payroll-go.dump b/__tests__/fixtures/golden/payroll-go.dump index ba8b0fd126..373b23ae40 100644 --- a/__tests__/fixtures/golden/payroll-go.dump +++ b/__tests__/fixtures/golden/payroll-go.dump @@ -587,13 +587,13 @@ {"source":"function:0ae098aebe4dbe9d71e324db178702ba","target":"struct:19321b921e8669d1052268136951cb96","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayrollCycleRow\"}","line":52,"col":100,"provenance":null} {"source":"function:0ae098aebe4dbe9d71e324db178702ba","target":"struct:a7849e2411e6cccc2a1da578922c7004","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayrollCycleUpdateRequest\"}","line":52,"col":72,"provenance":null} {"source":"function:0e6d6dd34d39c0ac54c9a7084b8d8ca8","target":"struct:44ba5408fc435805ad63b792fae78895","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"EmployeeRow\"}","line":84,"col":33,"provenance":null} -{"source":"function:0f4c51f6d4475eb21570beb83cf54ff4","target":"struct:33239c589ef83d64d5f6e73c2e021ee6","kind":"instantiates","metadata":"{\"confidence\":0.8,\"resolvedBy\":\"framework\",\"refName\":\"Store\"}","line":19,"col":43,"provenance":null} -{"source":"function:0f4c51f6d4475eb21570beb83cf54ff4","target":"struct:33239c589ef83d64d5f6e73c2e021ee6","kind":"references","metadata":"{\"confidence\":0.8,\"resolvedBy\":\"framework\",\"refName\":\"Store\"}","line":19,"col":27,"provenance":null} +{"source":"function:0f4c51f6d4475eb21570beb83cf54ff4","target":"struct:33239c589ef83d64d5f6e73c2e021ee6","kind":"instantiates","metadata":"{\"framework\":\"go\",\"confidence\":0.8,\"resolvedBy\":\"framework\",\"refName\":\"Store\"}","line":19,"col":43,"provenance":null} +{"source":"function:0f4c51f6d4475eb21570beb83cf54ff4","target":"struct:33239c589ef83d64d5f6e73c2e021ee6","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.8,\"resolvedBy\":\"framework\",\"refName\":\"Store\"}","line":19,"col":27,"provenance":null} {"source":"function:13c7c9632913a7b5c4cb3853ae46f74a","target":"function:d82ab6b26c7b41b587b8d940d1c57034","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"scanPayslip\"}","line":71,"col":8,"provenance":null} {"source":"function:13c7c9632913a7b5c4cb3853ae46f74a","target":"struct:6c93a0c367bc4b0f4392bc71266af6f6","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayslipUpdateRequest\"}","line":65,"col":67,"provenance":null} {"source":"function:13c7c9632913a7b5c4cb3853ae46f74a","target":"struct:eed7116b6261c410c79b06f6b3a3aebe","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayslipRow\"}","line":65,"col":90,"provenance":null} {"source":"function:1de2380afa5ea252aaabed82c8149f04","target":"interface:a2c04a6e9d1203f224eb044d73e38f03","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Clock\"}","line":50,"col":51,"provenance":null} -{"source":"function:1de2380afa5ea252aaabed82c8149f04","target":"struct:33239c589ef83d64d5f6e73c2e021ee6","kind":"references","metadata":"{\"confidence\":0.8,\"resolvedBy\":\"framework\",\"refName\":\"Store\"}","line":50,"col":36,"provenance":null} +{"source":"function:1de2380afa5ea252aaabed82c8149f04","target":"struct:33239c589ef83d64d5f6e73c2e021ee6","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.8,\"resolvedBy\":\"framework\",\"refName\":\"Store\"}","line":50,"col":36,"provenance":null} {"source":"function:1de2380afa5ea252aaabed82c8149f04","target":"struct:d40c8ed73128048826648f1ac8c1bc35","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Service\"}","line":51,"col":9,"provenance":null} {"source":"function:1de2380afa5ea252aaabed82c8149f04","target":"struct:d40c8ed73128048826648f1ac8c1bc35","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Service\"}","line":50,"col":59,"provenance":null} {"source":"function:209f5fe0d451b33db0d6395638a62fe8","target":"struct:19321b921e8669d1052268136951cb96","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayrollCycleRow\"}","line":111,"col":37,"provenance":null} @@ -633,7 +633,7 @@ {"source":"function:80943211ed2b432c66489bcda8e6d0a4","target":"function:2f7a92e2ac264db827c2eedf3fe7c448","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"earlierOf\"}","line":16,"col":66,"provenance":null} {"source":"function:80943211ed2b432c66489bcda8e6d0a4","target":"function:9ce32ae86fd8d01bb45dfcd56982a11b","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"calendarDays\"}","line":12,"col":11,"provenance":null} {"source":"function:80943211ed2b432c66489bcda8e6d0a4","target":"function:9ce32ae86fd8d01bb45dfcd56982a11b","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"calendarDays\"}","line":16,"col":12,"provenance":null} -{"source":"function:80943211ed2b432c66489bcda8e6d0a4","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":11,"col":77,"provenance":null} +{"source":"function:80943211ed2b432c66489bcda8e6d0a4","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":11,"col":77,"provenance":null} {"source":"function:80943211ed2b432c66489bcda8e6d0a4","target":"struct:b3960082884fafd52356f38f6551082f","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Contract\"}","line":11,"col":53,"provenance":null} {"source":"function:81328c12525c00ff7d9778f2e988eab3","target":"struct:44ba5408fc435805ad63b792fae78895","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"EmployeeRow\"}","line":65,"col":78,"provenance":null} {"source":"function:82bd498c77e3b6ab5b76ae283f737fde","target":"function:680f46cf762a57779b8a8dbe6de8c989","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"scanContract\"}","line":42,"col":8,"provenance":null} @@ -650,8 +650,8 @@ {"source":"function:97d5bc1460c9dc0379c4c2016964d601","target":"struct:141301eeb403e252d3f06decd0665c41","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"TimesheetRow\"}","line":71,"col":34,"provenance":null} {"source":"function:9a1275209a1a60e56b21de5aba04f222","target":"function:d82ab6b26c7b41b587b8d940d1c57034","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"scanPayslip\"}","line":61,"col":8,"provenance":null} {"source":"function:9a1275209a1a60e56b21de5aba04f222","target":"struct:eed7116b6261c410c79b06f6b3a3aebe","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayslipRow\"}","line":59,"col":61,"provenance":null} -{"source":"function:a2d9b9035f29c64480f1644da96bc32f","target":"struct:33239c589ef83d64d5f6e73c2e021ee6","kind":"instantiates","metadata":"{\"confidence\":0.8,\"resolvedBy\":\"framework\",\"refName\":\"Store\"}","line":23,"col":9,"provenance":null} -{"source":"function:a2d9b9035f29c64480f1644da96bc32f","target":"struct:33239c589ef83d64d5f6e73c2e021ee6","kind":"references","metadata":"{\"confidence\":0.8,\"resolvedBy\":\"framework\",\"refName\":\"Store\"}","line":22,"col":12,"provenance":null} +{"source":"function:a2d9b9035f29c64480f1644da96bc32f","target":"struct:33239c589ef83d64d5f6e73c2e021ee6","kind":"instantiates","metadata":"{\"framework\":\"go\",\"confidence\":0.8,\"resolvedBy\":\"framework\",\"refName\":\"Store\"}","line":23,"col":9,"provenance":null} +{"source":"function:a2d9b9035f29c64480f1644da96bc32f","target":"struct:33239c589ef83d64d5f6e73c2e021ee6","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.8,\"resolvedBy\":\"framework\",\"refName\":\"Store\"}","line":22,"col":12,"provenance":null} {"source":"function:b11ab260a99f087321b978dfbbcbccbb","target":"function:680f46cf762a57779b8a8dbe6de8c989","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"scanContract\"}","line":49,"col":8,"provenance":null} {"source":"function:b11ab260a99f087321b978dfbbcbccbb","target":"struct:8ae9f91523a30c90667e8f5ba09c3fc1","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"ContractRow\"}","line":47,"col":62,"provenance":null} {"source":"function:b3b725a56ce5377ac34ef157db4d1170","target":"struct:416d70a4ba3bd67a9d4b7114f2abd3a6","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayslipDTO\"}","line":40,"col":8,"provenance":null} @@ -666,7 +666,7 @@ {"source":"function:be35e9b1f03a5492d14e3bf3cc5dc87b","target":"struct:eed7116b6261c410c79b06f6b3a3aebe","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayslipRow\"}","line":113,"col":8,"provenance":null} {"source":"function:be35e9b1f03a5492d14e3bf3cc5dc87b","target":"struct:eed7116b6261c410c79b06f6b3a3aebe","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayslipRow\"}","line":112,"col":44,"provenance":null} {"source":"function:c304f28b41560a488fa25f4c0bdf4c46","target":"function:80943211ed2b432c66489bcda8e6d0a4","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"prorateSalary\"}","line":31,"col":8,"provenance":null} -{"source":"function:c304f28b41560a488fa25f4c0bdf4c46","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":27,"col":57,"provenance":null} +{"source":"function:c304f28b41560a488fa25f4c0bdf4c46","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":27,"col":57,"provenance":null} {"source":"function:c304f28b41560a488fa25f4c0bdf4c46","target":"struct:62563918fc7ed271ca7e2c7b11f2c17a","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Employee\"}","line":27,"col":74,"provenance":null} {"source":"function:c304f28b41560a488fa25f4c0bdf4c46","target":"struct:6ee2c8b983ba47afa67ded3413cb1af3","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Allowance\"}","line":27,"col":32,"provenance":null} {"source":"function:c6392c858f3b95686ef12895ec918b6e","target":"function:0e6d6dd34d39c0ac54c9a7084b8d8ca8","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"scanEmployee\"}","line":41,"col":8,"provenance":null} @@ -689,11 +689,11 @@ {"source":"interface:a2c04a6e9d1203f224eb044d73e38f03","target":"method:d73f595573e869571a14f1606df5c296","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"method:01c8a961c1fdfb54ea0c32614f905acc","target":"method:1b4f96e5a5a8b2784487acc4f7134ff7","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"instance-method\",\"refName\":\"e.UnpaidLeaveCoversWindow\"}","line":155,"col":5,"provenance":null} {"source":"method:01c8a961c1fdfb54ea0c32614f905acc","target":"method:75bdf0b5152e4d85c947a608505c15c1","kind":"calls","metadata":"{\"confidence\":0.85,\"resolvedBy\":\"instance-method\",\"refName\":\"s.store.EmployeesForCycle\"}","line":137,"col":13,"provenance":null} -{"source":"method:01c8a961c1fdfb54ea0c32614f905acc","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":136,"col":63,"provenance":null} +{"source":"method:01c8a961c1fdfb54ea0c32614f905acc","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":136,"col":63,"provenance":null} {"source":"method:01c8a961c1fdfb54ea0c32614f905acc","target":"struct:62563918fc7ed271ca7e2c7b11f2c17a","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Employee\"}","line":136,"col":98,"provenance":null} {"source":"method:01c8a961c1fdfb54ea0c32614f905acc","target":"struct:9448af57d2265ea84ce5114c823184ac","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"RunOptions\"}","line":136,"col":75,"provenance":null} -{"source":"method:069338e91f3b3bc0a6b434212386e59b","target":"function:0709805de829555a8b769b387496185d","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"DeletePayslip\"}","line":61,"col":8,"provenance":null} -{"source":"method:0f882862f60ecd98b79db60e64ec66eb","target":"function:2b593a9d67adcbcc2f8157621f1b08f4","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"CreatePayrollCycle\"}","line":71,"col":8,"provenance":null} +{"source":"method:069338e91f3b3bc0a6b434212386e59b","target":"function:0709805de829555a8b769b387496185d","kind":"calls","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"exact-match\",\"refName\":\"DeletePayslip\"}","line":61,"col":8,"provenance":null} +{"source":"method:0f882862f60ecd98b79db60e64ec66eb","target":"function:2b593a9d67adcbcc2f8157621f1b08f4","kind":"calls","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"exact-match\",\"refName\":\"CreatePayrollCycle\"}","line":71,"col":8,"provenance":null} {"source":"method:0f882862f60ecd98b79db60e64ec66eb","target":"struct:19321b921e8669d1052268136951cb96","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayrollCycleRow\"}","line":70,"col":88,"provenance":null} {"source":"method:0f882862f60ecd98b79db60e64ec66eb","target":"struct:3859374e7027b649f8650d5d9e1ac768","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayrollCycleCreateRequest\"}","line":70,"col":60,"provenance":null} {"source":"method:12243ae31857f370c8d5ea75c91b702f","target":"method:59c25996edd43745355454c918ca737d","kind":"calls","metadata":"{\"confidence\":0.85,\"resolvedBy\":\"instance-method\",\"refName\":\"s.store.SaveCycle\"}","line":226,"col":8,"provenance":null} @@ -701,13 +701,13 @@ {"source":"method:16a61da8990e9e8548eaba76a0d96d9f","target":"method:64fce125a13481bd9c53202505ebb26f","kind":"calls","metadata":"{\"synthesizedBy\":\"interface-impl\",\"via\":\"GetPayrollCycle\",\"registeredAt\":\"internal/gen/payrollpb/payroll_grpc.pb.go:79\"}","line":67,"col":null,"provenance":"heuristic"} {"source":"method:18a9696cad4179d634834650c0684f0c","target":"struct:4325013490f7f7a77604146f5e38c292","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"RunPayrollCycleRequest\"}","line":37,"col":72,"provenance":null} {"source":"method:18a9696cad4179d634834650c0684f0c","target":"struct:e66f442bcf28c3bd0a3f42fbacb69b2a","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"RunPayrollCycleResponse\"}","line":37,"col":123,"provenance":null} -{"source":"method:24e3292f2546abebcb9d371572f38e20","target":"struct:f1c05936f2b11591d1f90efa9d981fa7","kind":"references","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Payslip\"}","line":50,"col":76,"provenance":null} +{"source":"method:24e3292f2546abebcb9d371572f38e20","target":"struct:f1c05936f2b11591d1f90efa9d981fa7","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Payslip\"}","line":50,"col":76,"provenance":null} {"source":"method:2eb01c123ced28db3096d2168949061f","target":"method:412163f69abf5ae22adf9b9141ba79e2","kind":"calls","metadata":"{\"synthesizedBy\":\"interface-impl\",\"via\":\"mustEmbedUnimplementedPayrollServiceServer\",\"registeredAt\":\"internal/gen/payrollpb/payroll_grpc.pb.go:87\"}","line":69,"col":null,"provenance":"heuristic"} -{"source":"method:3367319bda79cdcf1fefc366ce7322ac","target":"struct:f1c05936f2b11591d1f90efa9d981fa7","kind":"instantiates","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Payslip\"}","line":102,"col":41,"provenance":null} +{"source":"method:3367319bda79cdcf1fefc366ce7322ac","target":"struct:f1c05936f2b11591d1f90efa9d981fa7","kind":"instantiates","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Payslip\"}","line":102,"col":41,"provenance":null} {"source":"method:3708105303f9a5feb2601fa8f3fe731e","target":"function:5e2796c1fbc4081e6b6f6a4307fe4bc0","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"key\"}","line":45,"col":12,"provenance":null} -{"source":"method:3708105303f9a5feb2601fa8f3fe731e","target":"struct:f1c05936f2b11591d1f90efa9d981fa7","kind":"references","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Payslip\"}","line":36,"col":57,"provenance":null} +{"source":"method:3708105303f9a5feb2601fa8f3fe731e","target":"struct:f1c05936f2b11591d1f90efa9d981fa7","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Payslip\"}","line":36,"col":57,"provenance":null} {"source":"method:3af34007f72fd40aebe0e4504e8228ae","target":"method:24e3292f2546abebcb9d371572f38e20","kind":"calls","metadata":"{\"confidence\":0.85,\"resolvedBy\":\"instance-method\",\"refName\":\"s.store.ListByCycle\"}","line":206,"col":15,"provenance":null} -{"source":"method:3af34007f72fd40aebe0e4504e8228ae","target":"struct:f1c05936f2b11591d1f90efa9d981fa7","kind":"references","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Payslip\"}","line":205,"col":83,"provenance":null} +{"source":"method:3af34007f72fd40aebe0e4504e8228ae","target":"struct:f1c05936f2b11591d1f90efa9d981fa7","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Payslip\"}","line":205,"col":83,"provenance":null} {"source":"method:3f9811f3a17dc661bfba3de069234279","target":"struct:e66f442bcf28c3bd0a3f42fbacb69b2a","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"RunPayrollCycleResponse\"}","line":69,"col":57,"provenance":null} {"source":"method:41899a297d23af6d3d2da3506b936eec","target":"function:852fbd5b1a3434558ad769f0855ecfae","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"sumKind\"}","line":59,"col":19,"provenance":null} {"source":"method:41899a297d23af6d3d2da3506b936eec","target":"function:852fbd5b1a3434558ad769f0855ecfae","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"sumKind\"}","line":79,"col":23,"provenance":null} @@ -716,28 +716,28 @@ {"source":"method:41899a297d23af6d3d2da3506b936eec","target":"method:83f3b7afb07d631d545c5f12a344383b","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"instance-method\",\"refName\":\"s.overtimeCents\"}","line":43,"col":16,"provenance":null} {"source":"method:41899a297d23af6d3d2da3506b936eec","target":"method:d4c880f819795f197c94de3201fb709e","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"instance-method\",\"refName\":\"s.basePayCents\"}","line":38,"col":9,"provenance":null} {"source":"method:41899a297d23af6d3d2da3506b936eec","target":"method:dd6580e3209a7f4efedb099a9bd5576d","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"instance-method\",\"refName\":\"d.AmountFor\"}","line":62,"col":12,"provenance":null} -{"source":"method:41899a297d23af6d3d2da3506b936eec","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":19,"col":15,"provenance":null} +{"source":"method:41899a297d23af6d3d2da3506b936eec","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":19,"col":15,"provenance":null} {"source":"method:41899a297d23af6d3d2da3506b936eec","target":"struct:51160f165aa24865c9ffd701638e08de","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"import\",\"refName\":\"payroll.Line\"}","line":39,"col":33,"provenance":null} {"source":"method:41899a297d23af6d3d2da3506b936eec","target":"struct:51160f165aa24865c9ffd701638e08de","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"import\",\"refName\":\"payroll.Line\"}","line":44,"col":34,"provenance":null} {"source":"method:41899a297d23af6d3d2da3506b936eec","target":"struct:51160f165aa24865c9ffd701638e08de","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"import\",\"refName\":\"payroll.Line\"}","line":54,"col":34,"provenance":null} {"source":"method:41899a297d23af6d3d2da3506b936eec","target":"struct:51160f165aa24865c9ffd701638e08de","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"import\",\"refName\":\"payroll.Line\"}","line":66,"col":34,"provenance":null} {"source":"method:41899a297d23af6d3d2da3506b936eec","target":"struct:51160f165aa24865c9ffd701638e08de","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"import\",\"refName\":\"payroll.Line\"}","line":75,"col":33,"provenance":null} {"source":"method:41899a297d23af6d3d2da3506b936eec","target":"struct:62563918fc7ed271ca7e2c7b11f2c17a","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Employee\"}","line":20,"col":18,"provenance":null} -{"source":"method:41899a297d23af6d3d2da3506b936eec","target":"struct:e0ae7fcd0d04a65b1a2a768c40e373d3","kind":"references","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Timesheet\"}","line":21,"col":19,"provenance":null} +{"source":"method:41899a297d23af6d3d2da3506b936eec","target":"struct:e0ae7fcd0d04a65b1a2a768c40e373d3","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Timesheet\"}","line":21,"col":19,"provenance":null} {"source":"method:41899a297d23af6d3d2da3506b936eec","target":"struct:f1c05936f2b11591d1f90efa9d981fa7","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"import\",\"refName\":\"payroll.Payslip\"}","line":24,"col":9,"provenance":null} {"source":"method:41899a297d23af6d3d2da3506b936eec","target":"struct:f1c05936f2b11591d1f90efa9d981fa7","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"import\",\"refName\":\"payroll.Payslip\"}","line":27,"col":9,"provenance":null} {"source":"method:41899a297d23af6d3d2da3506b936eec","target":"struct:f1c05936f2b11591d1f90efa9d981fa7","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"import\",\"refName\":\"payroll.Payslip\"}","line":30,"col":9,"provenance":null} {"source":"method:41899a297d23af6d3d2da3506b936eec","target":"struct:f1c05936f2b11591d1f90efa9d981fa7","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"import\",\"refName\":\"payroll.Payslip\"}","line":73,"col":9,"provenance":null} -{"source":"method:41899a297d23af6d3d2da3506b936eec","target":"struct:f1c05936f2b11591d1f90efa9d981fa7","kind":"references","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Payslip\"}","line":22,"col":11,"provenance":null} +{"source":"method:41899a297d23af6d3d2da3506b936eec","target":"struct:f1c05936f2b11591d1f90efa9d981fa7","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Payslip\"}","line":22,"col":11,"provenance":null} {"source":"method:4275e437d50514e430b25f9c11363ddf","target":"method:64fce125a13481bd9c53202505ebb26f","kind":"calls","metadata":"{\"synthesizedBy\":\"interface-impl\",\"via\":\"GetPayrollCycle\",\"registeredAt\":\"internal/gen/payrollpb/payroll_grpc.pb.go:79\"}","line":24,"col":null,"provenance":"heuristic"} {"source":"method:4275e437d50514e430b25f9c11363ddf","target":"method:e0490e3e69744ec0d57d332d3bda8c60","kind":"calls","metadata":"{\"synthesizedBy\":\"interface-impl\",\"via\":\"GetPayrollCycle\",\"registeredAt\":\"internal/gen/payrollpb/payroll_grpc.pb.go:46\"}","line":24,"col":null,"provenance":"heuristic"} {"source":"method:4a3dff65c0d488ecc09cf9edabe95d42","target":"struct:62563918fc7ed271ca7e2c7b11f2c17a","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Employee\"}","line":119,"col":37,"provenance":null} {"source":"method:4df364d0c37da3d10e39207b87a94ca8","target":"method:b90cd7088a55c9068e12a8120b256e13","kind":"calls","metadata":"{\"synthesizedBy\":\"interface-impl\",\"via\":\"RunPayrollCycle\",\"registeredAt\":\"internal/gen/payrollpb/payroll_grpc.pb.go:75\"}","line":66,"col":null,"provenance":"heuristic"} -{"source":"method:4f0ac2154c360ad1c55284c03d1f0c73","target":"struct:33239c589ef83d64d5f6e73c2e021ee6","kind":"references","metadata":"{\"confidence\":0.8,\"resolvedBy\":\"framework\",\"refName\":\"Store\"}","line":85,"col":49,"provenance":null} -{"source":"method:4f57884a5e57e13884848a959e466e9d","target":"function:63f3bd99966467470cbabcb097ad36dd","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"ListPayslipsByCycle\"}","line":66,"col":8,"provenance":null} +{"source":"method:4f0ac2154c360ad1c55284c03d1f0c73","target":"struct:33239c589ef83d64d5f6e73c2e021ee6","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.8,\"resolvedBy\":\"framework\",\"refName\":\"Store\"}","line":85,"col":49,"provenance":null} +{"source":"method:4f57884a5e57e13884848a959e466e9d","target":"function:63f3bd99966467470cbabcb097ad36dd","kind":"calls","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"exact-match\",\"refName\":\"ListPayslipsByCycle\"}","line":66,"col":8,"provenance":null} {"source":"method:4f57884a5e57e13884848a959e466e9d","target":"struct:eed7116b6261c410c79b06f6b3a3aebe","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayslipRow\"}","line":65,"col":76,"provenance":null} {"source":"method:578c07e761ce9e31fb80ec2ae5ee7713","target":"struct:4325013490f7f7a77604146f5e38c292","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"RunPayrollCycleRequest\"}","line":47,"col":56,"provenance":null} -{"source":"method:59c25996edd43745355454c918ca737d","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":78,"col":61,"provenance":null} +{"source":"method:59c25996edd43745355454c918ca737d","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":78,"col":61,"provenance":null} {"source":"method:64fce125a13481bd9c53202505ebb26f","target":"struct:3d4fd5cb833f90205ff1d82443698813","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayrollCycle\"}","line":79,"col":101,"provenance":null} {"source":"method:64fce125a13481bd9c53202505ebb26f","target":"struct:4325013490f7f7a77604146f5e38c292","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"RunPayrollCycleRequest\"}","line":79,"col":75,"provenance":null} {"source":"method:68acffbff0c2b5a4b3846a2cb887e3d0","target":"struct:4325013490f7f7a77604146f5e38c292","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"RunPayrollCycleRequest\"}","line":83,"col":72,"provenance":null} @@ -749,17 +749,17 @@ {"source":"method:6cf9498c0d766de9682085ddb6e0a655","target":"method:ed702f7226c3efff6b07f6f5afd73bf8","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"instance-method\",\"refName\":\"s.closeCycle\"}","line":121,"col":12,"provenance":null} {"source":"method:6cf9498c0d766de9682085ddb6e0a655","target":"struct:09a019d5ab5eefb1f1bc3b25ad95d81c","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"RunResult\"}","line":87,"col":11,"provenance":null} {"source":"method:6cf9498c0d766de9682085ddb6e0a655","target":"struct:09a019d5ab5eefb1f1bc3b25ad95d81c","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"RunResult\"}","line":86,"col":3,"provenance":null} -{"source":"method:6cf9498c0d766de9682085ddb6e0a655","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":83,"col":15,"provenance":null} +{"source":"method:6cf9498c0d766de9682085ddb6e0a655","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":83,"col":15,"provenance":null} {"source":"method:6cf9498c0d766de9682085ddb6e0a655","target":"struct:62563918fc7ed271ca7e2c7b11f2c17a","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Employee\"}","line":84,"col":18,"provenance":null} {"source":"method:6cf9498c0d766de9682085ddb6e0a655","target":"struct:9448af57d2265ea84ce5114c823184ac","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"RunOptions\"}","line":85,"col":6,"provenance":null} {"source":"method:6e0fe8d0272ee955601347cf37b5a383","target":"function:4ebe28bcc1b041576b79ad26309dd0b1","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"writeJSON\"}","line":85,"col":1,"provenance":null} {"source":"method:6e0fe8d0272ee955601347cf37b5a383","target":"function:ec95f80b7e074efca14308b96f0fae41","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"httpError\"}","line":82,"col":2,"provenance":null} {"source":"method:6e0fe8d0272ee955601347cf37b5a383","target":"method:3af34007f72fd40aebe0e4504e8228ae","kind":"calls","metadata":"{\"confidence\":0.85,\"resolvedBy\":\"instance-method\",\"refName\":\"h.svc.PayslipsForCycle\"}","line":80,"col":15,"provenance":null} -{"source":"method:72b795cb8a990681705170df3b9beacd","target":"function:b89f64f70418e754ec4cd1fef07bbf86","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"GetPayrollCycle\"}","line":76,"col":8,"provenance":null} +{"source":"method:72b795cb8a990681705170df3b9beacd","target":"function:b89f64f70418e754ec4cd1fef07bbf86","kind":"calls","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"exact-match\",\"refName\":\"GetPayrollCycle\"}","line":76,"col":8,"provenance":null} {"source":"method:72b795cb8a990681705170df3b9beacd","target":"struct:19321b921e8669d1052268136951cb96","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayrollCycleRow\"}","line":75,"col":65,"provenance":null} {"source":"method:75bdf0b5152e4d85c947a608505c15c1","target":"struct:62563918fc7ed271ca7e2c7b11f2c17a","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Employee\"}","line":88,"col":82,"provenance":null} {"source":"method:83f3b7afb07d631d545c5f12a344383b","target":"struct:62563918fc7ed271ca7e2c7b11f2c17a","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Employee\"}","line":106,"col":42,"provenance":null} -{"source":"method:83f3b7afb07d631d545c5f12a344383b","target":"struct:e0ae7fcd0d04a65b1a2a768c40e373d3","kind":"references","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Timesheet\"}","line":106,"col":63,"provenance":null} +{"source":"method:83f3b7afb07d631d545c5f12a344383b","target":"struct:e0ae7fcd0d04a65b1a2a768c40e373d3","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Timesheet\"}","line":106,"col":63,"provenance":null} {"source":"method:84b624c4861c4c33658499d7a4d48d3b","target":"function:4ebe28bcc1b041576b79ad26309dd0b1","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"writeJSON\"}","line":62,"col":1,"provenance":null} {"source":"method:84b624c4861c4c33658499d7a4d48d3b","target":"function:ec95f80b7e074efca14308b96f0fae41","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"httpError\"}","line":37,"col":2,"provenance":null} {"source":"method:84b624c4861c4c33658499d7a4d48d3b","target":"function:ec95f80b7e074efca14308b96f0fae41","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"httpError\"}","line":44,"col":3,"provenance":null} @@ -768,7 +768,7 @@ {"source":"method:84b624c4861c4c33658499d7a4d48d3b","target":"method:fd95c33dd58bbddc5221ee4ced0bbcd3","kind":"calls","metadata":"{\"confidence\":0.85,\"resolvedBy\":\"instance-method\",\"refName\":\"h.svc.RunCycle\"}","line":49,"col":16,"provenance":null} {"source":"method:84b624c4861c4c33658499d7a4d48d3b","target":"struct:9448af57d2265ea84ce5114c823184ac","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"import\",\"refName\":\"payroll.RunOptions\"}","line":49,"col":53,"provenance":null} {"source":"method:84b624c4861c4c33658499d7a4d48d3b","target":"struct:b927d9a5854646aa0a4030ad14e77688","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"runCycleResponse\"}","line":62,"col":29,"provenance":null} -{"source":"method:8904bbaa5fa8cab291d499d75ceccf2c","target":"function:371445b42f0b89d0e09b6c77c0cae1c9","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"CreatePayslip\"}","line":46,"col":8,"provenance":null} +{"source":"method:8904bbaa5fa8cab291d499d75ceccf2c","target":"function:371445b42f0b89d0e09b6c77c0cae1c9","kind":"calls","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"exact-match\",\"refName\":\"CreatePayslip\"}","line":46,"col":8,"provenance":null} {"source":"method:8904bbaa5fa8cab291d499d75ceccf2c","target":"struct:1d9ef6577e54226fc62ba69e632f38af","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayslipCreateRequest\"}","line":45,"col":55,"provenance":null} {"source":"method:8904bbaa5fa8cab291d499d75ceccf2c","target":"struct:eed7116b6261c410c79b06f6b3a3aebe","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayslipRow\"}","line":45,"col":78,"provenance":null} {"source":"method:8baae5c5fb59db7a88c19bb295635d18","target":"function:4ebe28bcc1b041576b79ad26309dd0b1","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"writeJSON\"}","line":76,"col":1,"provenance":null} @@ -779,7 +779,7 @@ {"source":"method:9b6984f4380172dd0fa8449ec4848fd9","target":"function:209f5fe0d451b33db0d6395638a62fe8","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"scanPayrollCycle\"}","line":41,"col":8,"provenance":null} {"source":"method:9b6984f4380172dd0fa8449ec4848fd9","target":"struct:19321b921e8669d1052268136951cb96","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayrollCycleRow\"}","line":36,"col":60,"provenance":null} {"source":"method:9b6984f4380172dd0fa8449ec4848fd9","target":"struct:19321b921e8669d1052268136951cb96","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayrollCycleRow\"}","line":36,"col":78,"provenance":null} -{"source":"method:9d5b1deafa95d4bdd73c8d7edebbd78e","target":"function:13c7c9632913a7b5c4cb3853ae46f74a","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"UpdatePayslip\"}","line":56,"col":8,"provenance":null} +{"source":"method:9d5b1deafa95d4bdd73c8d7edebbd78e","target":"function:13c7c9632913a7b5c4cb3853ae46f74a","kind":"calls","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"exact-match\",\"refName\":\"UpdatePayslip\"}","line":56,"col":8,"provenance":null} {"source":"method:9d5b1deafa95d4bdd73c8d7edebbd78e","target":"struct:6c93a0c367bc4b0f4392bc71266af6f6","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayslipUpdateRequest\"}","line":55,"col":66,"provenance":null} {"source":"method:9d5b1deafa95d4bdd73c8d7edebbd78e","target":"struct:eed7116b6261c410c79b06f6b3a3aebe","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayslipRow\"}","line":55,"col":89,"provenance":null} {"source":"method:9fb9bdbd7bbd9dc61cf38dda2c30809a","target":"function:d82ab6b26c7b41b587b8d940d1c57034","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"scanPayslip\"}","line":31,"col":8,"provenance":null} @@ -788,13 +788,13 @@ {"source":"method:a1cfcb03fa859746d05499b8e1b91979","target":"function:5e2796c1fbc4081e6b6f6a4307fe4bc0","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"key\"}","line":103,"col":20,"provenance":null} {"source":"method:a1cfcb03fa859746d05499b8e1b91979","target":"struct:e0ae7fcd0d04a65b1a2a768c40e373d3","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"import\",\"refName\":\"payroll.Timesheet\"}","line":105,"col":9,"provenance":null} {"source":"method:a1cfcb03fa859746d05499b8e1b91979","target":"struct:e0ae7fcd0d04a65b1a2a768c40e373d3","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"import\",\"refName\":\"payroll.Timesheet\"}","line":99,"col":9,"provenance":null} -{"source":"method:a1cfcb03fa859746d05499b8e1b91979","target":"struct:e0ae7fcd0d04a65b1a2a768c40e373d3","kind":"references","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Timesheet\"}","line":97,"col":84,"provenance":null} +{"source":"method:a1cfcb03fa859746d05499b8e1b91979","target":"struct:e0ae7fcd0d04a65b1a2a768c40e373d3","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Timesheet\"}","line":97,"col":84,"provenance":null} {"source":"method:a235478f8389c36b95d45a62664c4557","target":"method:68acffbff0c2b5a4b3846a2cb887e3d0","kind":"calls","metadata":"{\"synthesizedBy\":\"interface-impl\",\"via\":\"ListPayslips\",\"registeredAt\":\"internal/gen/payrollpb/payroll_grpc.pb.go:83\"}","line":68,"col":null,"provenance":"heuristic"} {"source":"method:a51e3df6f33aff769f591399d86eb5af","target":"method:68acffbff0c2b5a4b3846a2cb887e3d0","kind":"calls","metadata":"{\"synthesizedBy\":\"interface-impl\",\"via\":\"ListPayslips\",\"registeredAt\":\"internal/gen/payrollpb/payroll_grpc.pb.go:83\"}","line":25,"col":null,"provenance":"heuristic"} {"source":"method:a51e3df6f33aff769f591399d86eb5af","target":"method:c481d719be40f20f63e077521f87b89b","kind":"calls","metadata":"{\"synthesizedBy\":\"interface-impl\",\"via\":\"ListPayslips\",\"registeredAt\":\"internal/gen/payrollpb/payroll_grpc.pb.go:55\"}","line":25,"col":null,"provenance":"heuristic"} {"source":"method:abcc0b4bca6145184f692d4019641c7f","target":"method:d14d0b12675b711f5f90a392b7b297aa","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"instance-method\",\"refName\":\"s.loadCycle\"}","line":201,"col":8,"provenance":null} -{"source":"method:abcc0b4bca6145184f692d4019641c7f","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":200,"col":70,"provenance":null} -{"source":"method:b27d090751d2cb16edf0e5b79bf229a6","target":"function:9a1275209a1a60e56b21de5aba04f222","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"GetPayslip\"}","line":51,"col":8,"provenance":null} +{"source":"method:abcc0b4bca6145184f692d4019641c7f","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":200,"col":70,"provenance":null} +{"source":"method:b27d090751d2cb16edf0e5b79bf229a6","target":"function:9a1275209a1a60e56b21de5aba04f222","kind":"calls","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"exact-match\",\"refName\":\"GetPayslip\"}","line":51,"col":8,"provenance":null} {"source":"method:b27d090751d2cb16edf0e5b79bf229a6","target":"struct:eed7116b6261c410c79b06f6b3a3aebe","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayslipRow\"}","line":50,"col":60,"provenance":null} {"source":"method:b7b7504326d93c240db9e8bcb37454e5","target":"struct:3d4fd5cb833f90205ff1d82443698813","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayrollCycle\"}","line":124,"col":46,"provenance":null} {"source":"method:b90cd7088a55c9068e12a8120b256e13","target":"struct:4325013490f7f7a77604146f5e38c292","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"RunPayrollCycleRequest\"}","line":75,"col":75,"provenance":null} @@ -802,35 +802,35 @@ {"source":"method:c481d719be40f20f63e077521f87b89b","target":"struct:4325013490f7f7a77604146f5e38c292","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"RunPayrollCycleRequest\"}","line":55,"col":69,"provenance":null} {"source":"method:c481d719be40f20f63e077521f87b89b","target":"struct:e66f442bcf28c3bd0a3f42fbacb69b2a","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"RunPayrollCycleResponse\"}","line":55,"col":120,"provenance":null} {"source":"method:c51ef8827ea4d4e2fac8c8e0b7dce61d","target":"function:5e2796c1fbc4081e6b6f6a4307fe4bc0","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"key\"}","line":117,"col":11,"provenance":null} -{"source":"method:c51ef8827ea4d4e2fac8c8e0b7dce61d","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":111,"col":35,"provenance":null} +{"source":"method:c51ef8827ea4d4e2fac8c8e0b7dce61d","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":111,"col":35,"provenance":null} {"source":"method:c51ef8827ea4d4e2fac8c8e0b7dce61d","target":"struct:62563918fc7ed271ca7e2c7b11f2c17a","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Employee\"}","line":111,"col":62,"provenance":null} -{"source":"method:c51ef8827ea4d4e2fac8c8e0b7dce61d","target":"struct:e0ae7fcd0d04a65b1a2a768c40e373d3","kind":"references","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Timesheet\"}","line":111,"col":89,"provenance":null} +{"source":"method:c51ef8827ea4d4e2fac8c8e0b7dce61d","target":"struct:e0ae7fcd0d04a65b1a2a768c40e373d3","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Timesheet\"}","line":111,"col":89,"provenance":null} {"source":"method:cfa86e7d9aaf8dee7a94b8d5c7717017","target":"method:a1cfcb03fa859746d05499b8e1b91979","kind":"calls","metadata":"{\"confidence\":0.85,\"resolvedBy\":\"instance-method\",\"refName\":\"s.store.Timesheet\"}","line":166,"col":12,"provenance":null} {"source":"method:cfa86e7d9aaf8dee7a94b8d5c7717017","target":"method:b32c3975c878ce8570b17cb4638c5055","kind":"calls","metadata":"{\"confidence\":0.85,\"resolvedBy\":\"instance-method\",\"refName\":\"e.Contract.PeriodUnits\"}","line":180,"col":15,"provenance":null} -{"source":"method:cfa86e7d9aaf8dee7a94b8d5c7717017","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":165,"col":66,"provenance":null} +{"source":"method:cfa86e7d9aaf8dee7a94b8d5c7717017","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":165,"col":66,"provenance":null} {"source":"method:cfa86e7d9aaf8dee7a94b8d5c7717017","target":"struct:62563918fc7ed271ca7e2c7b11f2c17a","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Employee\"}","line":165,"col":83,"provenance":null} {"source":"method:cfa86e7d9aaf8dee7a94b8d5c7717017","target":"struct:e0ae7fcd0d04a65b1a2a768c40e373d3","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"import\",\"refName\":\"payroll.Timesheet\"}","line":168,"col":9,"provenance":null} {"source":"method:cfa86e7d9aaf8dee7a94b8d5c7717017","target":"struct:e0ae7fcd0d04a65b1a2a768c40e373d3","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"import\",\"refName\":\"payroll.Timesheet\"}","line":176,"col":9,"provenance":null} {"source":"method:cfa86e7d9aaf8dee7a94b8d5c7717017","target":"struct:e0ae7fcd0d04a65b1a2a768c40e373d3","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"import\",\"refName\":\"payroll.Timesheet\"}","line":183,"col":8,"provenance":null} -{"source":"method:cfa86e7d9aaf8dee7a94b8d5c7717017","target":"struct:e0ae7fcd0d04a65b1a2a768c40e373d3","kind":"references","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Timesheet\"}","line":165,"col":102,"provenance":null} +{"source":"method:cfa86e7d9aaf8dee7a94b8d5c7717017","target":"struct:e0ae7fcd0d04a65b1a2a768c40e373d3","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Timesheet\"}","line":165,"col":102,"provenance":null} {"source":"method:d14d0b12675b711f5f90a392b7b297aa","target":"method:e5d8405d7063593214cc4d16d2bbec81","kind":"calls","metadata":"{\"confidence\":0.85,\"resolvedBy\":\"instance-method\",\"refName\":\"s.store.Cycle\"}","line":190,"col":8,"provenance":null} {"source":"method:d14d0b12675b711f5f90a392b7b297aa","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"import\",\"refName\":\"payroll.Cycle\"}","line":188,"col":9,"provenance":null} -{"source":"method:d14d0b12675b711f5f90a392b7b297aa","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":186,"col":74,"provenance":null} +{"source":"method:d14d0b12675b711f5f90a392b7b297aa","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":186,"col":74,"provenance":null} {"source":"method:d4c880f819795f197c94de3201fb709e","target":"function:80943211ed2b432c66489bcda8e6d0a4","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"prorateSalary\"}","line":96,"col":9,"provenance":null} -{"source":"method:d4c880f819795f197c94de3201fb709e","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":92,"col":65,"provenance":null} +{"source":"method:d4c880f819795f197c94de3201fb709e","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":92,"col":65,"provenance":null} {"source":"method:d4c880f819795f197c94de3201fb709e","target":"struct:62563918fc7ed271ca7e2c7b11f2c17a","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Employee\"}","line":92,"col":41,"provenance":null} -{"source":"method:d4c880f819795f197c94de3201fb709e","target":"struct:e0ae7fcd0d04a65b1a2a768c40e373d3","kind":"references","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Timesheet\"}","line":92,"col":83,"provenance":null} +{"source":"method:d4c880f819795f197c94de3201fb709e","target":"struct:e0ae7fcd0d04a65b1a2a768c40e373d3","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Timesheet\"}","line":92,"col":83,"provenance":null} {"source":"method:d73f595573e869571a14f1606df5c296","target":"method:69ac55ca7f86919d7f4ed769540cd552","kind":"calls","metadata":"{\"synthesizedBy\":\"interface-impl\",\"via\":\"Now\",\"registeredAt\":\"internal/platform/clock/clock.go:13\"}","line":7,"col":null,"provenance":"heuristic"} {"source":"method:d73f595573e869571a14f1606df5c296","target":"method:a68d11d25279e217ec83342d95b172f6","kind":"calls","metadata":"{\"synthesizedBy\":\"interface-impl\",\"via\":\"Now\",\"registeredAt\":\"internal/platform/clock/clock.go:18\"}","line":7,"col":null,"provenance":"heuristic"} {"source":"method:e0490e3e69744ec0d57d332d3bda8c60","target":"struct:3d4fd5cb833f90205ff1d82443698813","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayrollCycle\"}","line":46,"col":123,"provenance":null} {"source":"method:e0490e3e69744ec0d57d332d3bda8c60","target":"struct:4325013490f7f7a77604146f5e38c292","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"RunPayrollCycleRequest\"}","line":46,"col":72,"provenance":null} {"source":"method:e5d8405d7063593214cc4d16d2bbec81","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"import\",\"refName\":\"payroll.Cycle\"}","line":67,"col":9,"provenance":null} {"source":"method:e5d8405d7063593214cc4d16d2bbec81","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"import\",\"refName\":\"payroll.Cycle\"}","line":73,"col":9,"provenance":null} -{"source":"method:e5d8405d7063593214cc4d16d2bbec81","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":65,"col":68,"provenance":null} -{"source":"method:eba91430ad280a0c070af5c64e79d2d0","target":"struct:f1c05936f2b11591d1f90efa9d981fa7","kind":"references","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Payslip\"}","line":62,"col":51,"provenance":null} +{"source":"method:e5d8405d7063593214cc4d16d2bbec81","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":65,"col":68,"provenance":null} +{"source":"method:eba91430ad280a0c070af5c64e79d2d0","target":"struct:f1c05936f2b11591d1f90efa9d981fa7","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Payslip\"}","line":62,"col":51,"provenance":null} {"source":"method:ed702f7226c3efff6b07f6f5afd73bf8","target":"method:59c25996edd43745355454c918ca737d","kind":"calls","metadata":"{\"confidence\":0.85,\"resolvedBy\":\"instance-method\",\"refName\":\"s.store.SaveCycle\"}","line":196,"col":8,"provenance":null} -{"source":"method:ed702f7226c3efff6b07f6f5afd73bf8","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":193,"col":64,"provenance":null} -{"source":"method:ee60f0f8c18e9fd703205c637e5b5f8d","target":"function:c68d01c236174f79631f8e8222523fa4","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"ListPayrollCycles\"}","line":81,"col":8,"provenance":null} +{"source":"method:ed702f7226c3efff6b07f6f5afd73bf8","target":"struct:4831ac24c172065fee6e46751021f7ba","kind":"references","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Cycle\"}","line":193,"col":64,"provenance":null} +{"source":"method:ee60f0f8c18e9fd703205c637e5b5f8d","target":"function:c68d01c236174f79631f8e8222523fa4","kind":"calls","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"exact-match\",\"refName\":\"ListPayrollCycles\"}","line":81,"col":8,"provenance":null} {"source":"method:ee60f0f8c18e9fd703205c637e5b5f8d","target":"struct:19321b921e8669d1052268136951cb96","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"PayrollCycleRow\"}","line":80,"col":58,"provenance":null} {"source":"method:fd95c33dd58bbddc5221ee4ced0bbcd3","target":"method:01c8a961c1fdfb54ea0c32614f905acc","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"instance-method\",\"refName\":\"s.rosterFor\"}","line":65,"col":16,"provenance":null} {"source":"method:fd95c33dd58bbddc5221ee4ced0bbcd3","target":"method:6cf9498c0d766de9682085ddb6e0a655","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"instance-method\",\"refName\":\"s.runPayrollCycleAll\"}","line":73,"col":8,"provenance":null} @@ -953,7 +953,7 @@ {"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:e66f442bcf28c3bd0a3f42fbacb69b2a","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"RunPayrollCycleResponse\",\"refKind\":\"calls\"}","line":134,"col":1,"provenance":null} -{"source":"variable:9c3a0c9681a69504be5d3af6ddb5378c","target":"struct:f1c05936f2b11591d1f90efa9d981fa7","kind":"instantiates","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Payslip\",\"refKind\":\"calls\"}","line":135,"col":1,"provenance":null} +{"source":"variable:9c3a0c9681a69504be5d3af6ddb5378c","target":"struct:f1c05936f2b11591d1f90efa9d981fa7","kind":"instantiates","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Payslip\",\"refKind\":\"calls\"}","line":135,"col":1,"provenance":null} == refs (516) {"from_node_id":"file:cmd/payrolld/main.go","reference_name":"log","reference_kind":"imports","line":4,"col":1,"candidates":null,"file_path":"cmd/payrolld/main.go","language":"go","status":"failed","name_tail":"log"} {"from_node_id":"file:cmd/payrolld/main.go","reference_name":"net/http","reference_kind":"imports","line":5,"col":1,"candidates":null,"file_path":"cmd/payrolld/main.go","language":"go","status":"failed","name_tail":"net/http"} diff --git a/__tests__/fixtures/golden/sfc-mix.dump b/__tests__/fixtures/golden/sfc-mix.dump index 07c2c762c5..3ee48d717c 100644 --- a/__tests__/fixtures/golden/sfc-mix.dump +++ b/__tests__/fixtures/golden/sfc-mix.dump @@ -34,7 +34,7 @@ {"source":"class:e0436e1c421fad6fb415ebf1250fc957","target":"method:0fb9589fa48847b470e36febaa20fd6a","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"component:3c652b3a8468352ce722877b478d750c","target":"class:9250e5f7a14d90af56522055875d3efc","kind":"instantiates","metadata":"{\"confidence\":0.5,\"resolvedBy\":\"exact-match\",\"refName\":\"GreetRequest\"}","line":13,"col":18,"provenance":null} {"source":"component:3c652b3a8468352ce722877b478d750c","target":"class:e0436e1c421fad6fb415ebf1250fc957","kind":"references","metadata":"{\"confidence\":0.5,\"resolvedBy\":\"exact-match\",\"refName\":\"GreetingService\"}","line":2,"col":0,"provenance":null} -{"source":"component:9ab44aceec99c2241ddd988791f90540","target":"component:f618e74c9653d19f14a4517ffbcc5a50","kind":"references","metadata":"{\"confidence\":0.8,\"resolvedBy\":\"framework\",\"refName\":\"Panel\"}","line":12,"col":5,"provenance":null} +{"source":"component:9ab44aceec99c2241ddd988791f90540","target":"component:f618e74c9653d19f14a4517ffbcc5a50","kind":"references","metadata":"{\"framework\":\"astro\",\"confidence\":0.8,\"resolvedBy\":\"framework\",\"refName\":\"Panel\"}","line":12,"col":5,"provenance":null} {"source":"component:9ab44aceec99c2241ddd988791f90540","target":"constant:065e374572e8fdbbdc1b3f23a2d4fae7","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"component:9ab44aceec99c2241ddd988791f90540","target":"constant:a603db0f72cd274c10bfb9baaf23a035","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"component:9ab44aceec99c2241ddd988791f90540","target":"file:src/pages/index.astro","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} @@ -90,7 +90,7 @@ {"source":"file:src/pages/index.astro","target":"import:7e12aa398bd31cc8ad3c376496090ad5","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"function:ed48ef0a7c92c2fe0641a87932a6e507","target":"function:0026a1cae8e888523240c893a10aa70d","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"import\",\"refName\":\"increment\"}","line":7,"col":4,"provenance":null} {"source":"method:0fb9589fa48847b470e36febaa20fd6a","target":"class:9250e5f7a14d90af56522055875d3efc","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"GreetRequest\"}","line":4,"col":24,"provenance":null} -{"source":"route:src/pages/index.astro:/:1","target":"component:9ab44aceec99c2241ddd988791f90540","kind":"references","metadata":"{\"confidence\":1,\"resolvedBy\":\"framework\",\"refName\":\"astro-page-component\"}","line":1,"col":0,"provenance":null} +{"source":"route:src/pages/index.astro:/:1","target":"component:9ab44aceec99c2241ddd988791f90540","kind":"references","metadata":"{\"framework\":\"astro\",\"confidence\":1,\"resolvedBy\":\"framework\",\"refName\":\"astro-page-component\"}","line":1,"col":0,"provenance":null} == refs (9) {"from_node_id":"component:3c652b3a8468352ce722877b478d750c","reference_name":"Greeter.Greet","reference_kind":"calls","line":14,"col":18,"candidates":null,"file_path":"Components/Greeter.razor","language":"razor","status":"failed","name_tail":"Greet"} {"from_node_id":"component:9ab44aceec99c2241ddd988791f90540","reference_name":"items.map","reference_kind":"calls","line":11,"col":8,"candidates":null,"file_path":"src/pages/index.astro","language":"astro","status":"failed","name_tail":"map","failure_reason":"unknown-receiver"} diff --git a/__tests__/fixtures/golden/tail-langs.dump b/__tests__/fixtures/golden/tail-langs.dump index c0dd98dc76..2e2f02b3a3 100644 --- a/__tests__/fixtures/golden/tail-langs.dump +++ b/__tests__/fixtures/golden/tail-langs.dump @@ -159,9 +159,9 @@ {"source":"class:85e303095ea33303cdff1b72c25fb6db","target":"method:1b92f98530b02ea1c9bfb4530bca5eb7","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"class:85e303095ea33303cdff1b72c25fb6db","target":"method:6371e3682b7f64f04708b1f642de336e","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"class:85e303095ea33303cdff1b72c25fb6db","target":"method:c94827fc2f9c117969234a98dadc0d05","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"class:8da3dc4378a8ad4becad38a01a3d4d63","target":"class:ed293d45f9099ac913df324c359e48c1","kind":"references","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"framework\",\"refName\":\"data.aws_ami.ubuntu\"}","line":8,"col":18,"provenance":null} -{"source":"class:8da3dc4378a8ad4becad38a01a3d4d63","target":"module:af152aef0ad0bd196ff5d219b36b58c1","kind":"references","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"framework\",\"refName\":\"module.network\"}","line":10,"col":18,"provenance":null} -{"source":"class:8da3dc4378a8ad4becad38a01a3d4d63","target":"variable:9c79774d08edc9a0bb27f472ee8a6a79","kind":"references","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"framework\",\"refName\":\"var.region\"}","line":11,"col":25,"provenance":null} +{"source":"class:8da3dc4378a8ad4becad38a01a3d4d63","target":"class:ed293d45f9099ac913df324c359e48c1","kind":"references","metadata":"{\"framework\":\"terraform\",\"confidence\":0.95,\"resolvedBy\":\"framework\",\"refName\":\"data.aws_ami.ubuntu\"}","line":8,"col":18,"provenance":null} +{"source":"class:8da3dc4378a8ad4becad38a01a3d4d63","target":"module:af152aef0ad0bd196ff5d219b36b58c1","kind":"references","metadata":"{\"framework\":\"terraform\",\"confidence\":0.95,\"resolvedBy\":\"framework\",\"refName\":\"module.network\"}","line":10,"col":18,"provenance":null} +{"source":"class:8da3dc4378a8ad4becad38a01a3d4d63","target":"variable:9c79774d08edc9a0bb27f472ee8a6a79","kind":"references","metadata":"{\"framework\":\"terraform\",\"confidence\":0.95,\"resolvedBy\":\"framework\",\"refName\":\"var.region\"}","line":11,"col":25,"provenance":null} {"source":"class:8f245e11bfe6a3d101a39ded206566ec","target":"method:2cffd29cd52f93d2ea6466306f4d7618","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"class:8f245e11bfe6a3d101a39ded206566ec","target":"method:8c9db773f13cb8c0e5a0498432ebc819","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"class:8f245e11bfe6a3d101a39ded206566ec","target":"method:bab15b615c718a99979e32483e78a2bd","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} @@ -272,7 +272,7 @@ {"source":"module:63f2cc0cb56a8e323b9dd6bdbb79713d","target":"function:f487868c7342813e789b96fab9df96ba","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"module:63f2cc0cb56a8e323b9dd6bdbb79713d","target":"import:55d60376df21c57ae15c3e8685a4ff52","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"module:63f2cc0cb56a8e323b9dd6bdbb79713d","target":"variable:bc323d601c470045a33f59b1b7c0dc11","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"module:af152aef0ad0bd196ff5d219b36b58c1","target":"variable:3da5d742eb007ccf578c71f0e64ad9c5","kind":"references","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"framework\",\"refName\":\"var.cidr\"}","line":5,"col":11,"provenance":null} +{"source":"module:af152aef0ad0bd196ff5d219b36b58c1","target":"variable:3da5d742eb007ccf578c71f0e64ad9c5","kind":"references","metadata":"{\"framework\":\"terraform\",\"confidence\":0.95,\"resolvedBy\":\"framework\",\"refName\":\"var.cidr\"}","line":5,"col":11,"provenance":null} {"source":"namespace:af63efb94c68875055ed19fc348aa906","target":"class:5ed1a9fbfc5fad495850f0cc6ac2970c","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"namespace:af63efb94c68875055ed19fc348aa906","target":"import:1b0247abd099fa4aef15950d5c54181d","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"namespace:af63efb94c68875055ed19fc348aa906","target":"import:62a37b7dc6cdff95138a84c92d60323c","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} @@ -293,7 +293,7 @@ {"source":"struct:00b863e6b6e7bd6569a36e94f72d3aaa","target":"property:1e7069012ccaabcb8a98d0790ea607ef","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"struct:00b863e6b6e7bd6569a36e94f72d3aaa","target":"property:81982deaee358e155a33af725d85bd97","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"struct:8050a2eb89168a19b5ac70b577047b96","target":"field:e2c04abad409b6092124d38ce58616bb","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"variable:62c285bb0e5c30272251db2b4966175f","target":"class:8da3dc4378a8ad4becad38a01a3d4d63","kind":"references","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"framework\",\"refName\":\"aws_instance.web\"}","line":14,"col":26,"provenance":null} +{"source":"variable:62c285bb0e5c30272251db2b4966175f","target":"class:8da3dc4378a8ad4becad38a01a3d4d63","kind":"references","metadata":"{\"framework\":\"terraform\",\"confidence\":0.95,\"resolvedBy\":\"framework\",\"refName\":\"aws_instance.web\"}","line":14,"col":26,"provenance":null} == refs (55) {"from_node_id":"class:85e303095ea33303cdff1b72c25fb6db","reference_name":"IGreeter","reference_kind":"implements","line":1,"col":43,"candidates":null,"file_path":"cfml/Greeter.cfc","language":"cfml","status":"failed","name_tail":"IGreeter"} {"from_node_id":"class:8da3dc4378a8ad4becad38a01a3d4d63","reference_name":"module.network:output.subnet_id","reference_kind":"references","line":10,"col":18,"candidates":null,"file_path":"terraform/main.tf","language":"terraform","status":"failed","name_tail":"subnet_id"} diff --git a/__tests__/fixtures/golden/torture-multilang.dump b/__tests__/fixtures/golden/torture-multilang.dump index 2a87bfb6dd..3ff16c701a 100644 --- a/__tests__/fixtures/golden/torture-multilang.dump +++ b/__tests__/fixtures/golden/torture-multilang.dump @@ -78,7 +78,7 @@ {"id":"class:866369875b5ab08194147a3ed7db495f","kind":"class","name":"Deep","qualified_name":"Outer::Inner::Deep","file_path":"torture.rb","language":"ruby","start_line":23,"end_line":26,"start_column":4,"end_column":7,"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:8748345347c1e156f8dd97f354442ad9","kind":"class","name":"Documented","qualified_name":"App\\Services::Documented","file_path":"torture.php","language":"php","start_line":30,"end_line":178,"start_column":0,"end_column":1,"docstring":"Doc for Documented.","signature":null,"visibility":"public","is_exported":1,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"class:8814f271e468f71dc14fc2f4afac7396","kind":"class","name":"InitWalk","qualified_name":"InitWalk","file_path":"torture.scala","language":"scala","start_line":194,"end_line":199,"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:8824753c22db659191de40733a67e526","kind":"class","name":"Program","qualified_name":"Program","file_path":"TortureTopLevel.cs","language":"csharp","start_line":10,"end_line":13,"start_column":0,"end_column":1,"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":"class:8824753c22db659191de40733a67e526","kind":"class","name":"Program","qualified_name":"Program","file_path":"TortureTopLevel.cs","language":"csharp","start_line":10,"end_line":13,"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":"class:89a6327dc2e1da4b990c740c891aaa96","kind":"class","name":"Session","qualified_name":"Session","file_path":"torture.swift","language":"swift","start_line":6,"end_line":37,"start_column":0,"end_column":1,"docstring":"Class doc line one\nClass doc line two","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:8c95080292bb3e5e19a1000efe9bd9e3","kind":"class","name":"MetersT","qualified_name":"MetersT","file_path":"torture.dart","language":"dart","start_line":212,"end_line":214,"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:90a8667278ecdb516d10fcf149e250b7","kind":"class","name":"LocalClass","qualified_name":"com.example.torture::caller::LocalClass","file_path":"torture.kt","language":"kotlin","start_line":224,"end_line":226,"start_column":4,"end_column":5,"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} @@ -1013,7 +1013,7 @@ {"id":"method:50795ff0a13bebd8320c09d5fe8e2698","kind":"method","name":"Compute","qualified_name":"Torture.Alpha::TortureClass::Compute","file_path":"Torture.cs","language":"csharp","start_line":71,"end_line":71,"start_column":8,"end_column":55,"docstring":null,"signature":null,"visibility":"private","is_exported":0,"is_async":0,"is_static":1,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"method:5155cb511053014dd8982984611ab0d9","kind":"method","name":"onChange","qualified_name":"com.example.torture::TortureService::Listener::onChange","file_path":"Torture.java","language":"java","start_line":83,"end_line":83,"start_column":4,"end_column":38,"docstring":null,"signature":"void (TortureService svc)","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"method:51799b32d3d9fffc601ddaa5e68cd56a","kind":"method","name":"cbTarget","qualified_name":"NamedArgs::cbTarget","file_path":"TortureGiven.scala","language":"scala","start_line":22,"end_line":22,"start_column":2,"end_column":27,"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:51810ede1fad470204b7d4bcfe7f1dca","kind":"method","name":"Get","qualified_name":"Torture.Alpha::IWidgetRepo::Get","file_path":"Torture.cs","language":"csharp","start_line":160,"end_line":160,"start_column":8,"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":"Task"} +{"id":"method:51810ede1fad470204b7d4bcfe7f1dca","kind":"method","name":"Get","qualified_name":"Torture.Alpha::IWidgetRepo::Get","file_path":"Torture.cs","language":"csharp","start_line":160,"end_line":160,"start_column":8,"end_column":28,"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":"Task"} {"id":"method:51d6ebbeee95e3c8b19527e13c10be19","kind":"method","name":"Reads","qualified_name":"Torture.Alpha::TortureClass::Reads","file_path":"Torture.cs","language":"csharp","start_line":104,"end_line":111,"start_column":8,"end_column":9,"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:53923a5148488879c3e388cbb0a9644a","kind":"method","name":"factory","qualified_name":"TraitFirst::factory","file_path":"ScalaCompanions.scala","language":"scala","start_line":11,"end_line":11,"start_column":2,"end_column":24,"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:554c9b2fd11df120a567061a2671400f","kind":"method","name":"readsConst","qualified_name":"WidgetT::readsConst","file_path":"torture.dart","language":"dart","start_line":148,"end_line":150,"start_column":2,"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} @@ -1134,7 +1134,7 @@ {"id":"method:b12f6a38b1232f74933b893a23efd390","kind":"method","name":"Main2","qualified_name":"Program::Main2","file_path":"TortureTopLevel.cs","language":"csharp","start_line":12,"end_line":12,"start_column":4,"end_column":52,"docstring":null,"signature":null,"visibility":"private","is_exported":0,"is_async":0,"is_static":1,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"method:b1ecb9f463b0e67d9ab538af3a90cd17","kind":"method","name":"documented","qualified_name":"DocClass::documented","file_path":"TortureDocs.scala","language":"scala","start_line":25,"end_line":25,"start_column":2,"end_column":27,"docstring":"member doc","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:b217696d4c9200c72b43c9cfd0aa2b03","kind":"method","name":"Widget","qualified_name":"app::Widget::Widget","file_path":"torture-macros-ctors.cpp","language":"cpp","start_line":19,"end_line":19,"start_column":2,"end_column":11,"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":"method:b428c051b0ed88967ff56fb804fe450a","kind":"method","name":"Compute","qualified_name":"Torture.Alpha::IWidgetRepo::Compute","file_path":"Torture.cs","language":"csharp","start_line":162,"end_line":162,"start_column":8,"end_column":36,"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:b428c051b0ed88967ff56fb804fe450a","kind":"method","name":"Compute","qualified_name":"Torture.Alpha::IWidgetRepo::Compute","file_path":"Torture.cs","language":"csharp","start_line":162,"end_line":162,"start_column":8,"end_column":36,"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:b45db8b9b1051e377f934b4f3880f721","kind":"method","name":"mixMethod","qualified_name":"MixA::mixMethod","file_path":"torture.dart","language":"dart","start_line":174,"end_line":176,"start_column":2,"end_column":18,"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:b4c76b45b0f8fe58f57f25c34f68d473","kind":"method","name":"handler","qualified_name":"Handlers::handler","file_path":"TortureFnref.scala","language":"scala","start_line":2,"end_line":2,"start_column":2,"end_column":26,"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:b5862db2c719b17231639bd84992b513","kind":"method","name":"reduce","qualified_name":"Action::reduce","file_path":"TortureCtors.dart","language":"dart","start_line":20,"end_line":20,"start_column":16,"end_column":24,"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} @@ -1264,7 +1264,7 @@ {"id":"property:35665af55cac4fa4fa3a8bbb40d4f10d","kind":"property","name":"#privateField","qualified_name":"Widget::#privateField","file_path":"torture.js","language":"javascript","start_line":17,"end_line":17,"start_column":2,"end_column":19,"docstring":null,"signature":"#privateField","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"property:36d0b68644ae5cbe11d8a493727a8a5a","kind":"property","name":"#items","qualified_name":"PrivateDelegator::#items","file_path":"torture.tsx","language":"tsx","start_line":239,"end_line":239,"start_column":2,"end_column":28,"docstring":null,"signature":"#items","visibility":null,"is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"property:3f76120a6d12e45f619241f30c648d15","kind":"property","name":"description","qualified_name":"AFError::description","file_path":"torture.swift","language":"swift","start_line":48,"end_line":48,"start_column":2,"end_column":49,"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":"property:41c90a4508dcf7006081c68981b6ae80","kind":"property","name":"Label","qualified_name":"Torture.Alpha::IWidgetRepo::Label","file_path":"Torture.cs","language":"csharp","start_line":161,"end_line":161,"start_column":8,"end_column":29,"docstring":null,"signature":"string Label","visibility":"private","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} +{"id":"property:41c90a4508dcf7006081c68981b6ae80","kind":"property","name":"Label","qualified_name":"Torture.Alpha::IWidgetRepo::Label","file_path":"Torture.cs","language":"csharp","start_line":161,"end_line":161,"start_column":8,"end_column":29,"docstring":null,"signature":"string Label","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"property:42cbcdbb0dffcc35a50326fe730508ab","kind":"property","name":"Made","qualified_name":"Torture.Alpha::TortureClass::Made","file_path":"Torture.cs","language":"csharp","start_line":38,"end_line":38,"start_column":8,"end_column":44,"docstring":null,"signature":"new() Made","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} {"id":"property:5adda8060392b1c87de026284998ff38","kind":"property","name":"isCloudProxy","qualified_name":"Session::isCloudProxy","file_path":"torture.swift","language":"swift","start_line":18,"end_line":18,"start_column":2,"end_column":53,"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":"property:65b1321e5f5780223e85efa4869b2dc1","kind":"property","name":"Name","qualified_name":"Torture.Alpha::TortureClass::Name","file_path":"Torture.cs","language":"csharp","start_line":34,"end_line":34,"start_column":8,"end_column":40,"docstring":null,"signature":"string Name","visibility":"public","is_exported":0,"is_async":0,"is_static":0,"is_abstract":0,"decorators":null,"type_parameters":null,"return_type":null} @@ -1470,7 +1470,7 @@ {"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 (1920) +== edges (1903) {"source":"class:0083dc18baa2b629e8572db20ba2b464","target":"class:25244cbdbe3e9bc720df9fc4d54fac0a","kind":"extends","metadata":"{\"confidence\":0.95,\"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} @@ -1738,7 +1738,6 @@ {"source":"class:c2dfee99ca5361d8de7ae7de6858d0bf","target":"field:4364abd6fe6a4ed95bc8cca1534464ec","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"class:c2dfee99ca5361d8de7ae7de6858d0bf","target":"field:852d4553f6ced53e767757f910aec935","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"class:c2dfee99ca5361d8de7ae7de6858d0bf","target":"field:bb16503247c8833e9d5ea8dc4ce7cb45","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"class:c2dfee99ca5361d8de7ae7de6858d0bf","target":"method:256f83a3935f0101fcb9728044b46196","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"register\"}","line":69,"col":8,"provenance":null} {"source":"class:c2dfee99ca5361d8de7ae7de6858d0bf","target":"method:96cd1708b4f116d8dcdcdc32127274c8","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"class:c2dfee99ca5361d8de7ae7de6858d0bf","target":"method:de718159205cba9b0d17647ac71f8f2a","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"class:c2dfee99ca5361d8de7ae7de6858d0bf","target":"method:e947ab8444253c3743504176397bb986","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} @@ -1799,7 +1798,6 @@ {"source":"class:cef28fda15c7702f8b1441177fbb8e99","target":"method:a83a51eab57270a522b9a2b7133e3e55","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"class:cef28fda15c7702f8b1441177fbb8e99","target":"method:bdff2ed091815c4e1268e7ac0a575bb0","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"class:cf8bad734e91e79fb7c91d19629af923","target":"field:1d9e3a840eac3d3d6b0d2607418da6e8","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"class:cf8bad734e91e79fb7c91d19629af923","target":"method:256f83a3935f0101fcb9728044b46196","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"register\"}","line":304,"col":8,"provenance":null} {"source":"class:d39afe12f0a538a2bcff3a3df26849f5","target":"field:95b7698226408405acc549f5ae9f30a0","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"class:d39afe12f0a538a2bcff3a3df26849f5","target":"method:b1ecb9f463b0e67d9ab538af3a90cd17","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"class:d4f24eb029ec8fa451f9439d45b3638b","target":"method:326a2b52ef1499055544729d4a4d3214","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} @@ -1939,10 +1937,8 @@ {"source":"enum:fcb9d383c69413c946db24cec4719f95","target":"enum_member:8341362c754fff305b25adf5c172b6e1","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"enum:fcb9d383c69413c946db24cec4719f95","target":"enum_member:bdd61861adf53750f330772a73a8196b","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"enum:fcb9d383c69413c946db24cec4719f95","target":"enum_member:c3e0d290164daa99da18461c242250b0","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"field:1d9e3a840eac3d3d6b0d2607418da6e8","target":"method:0f8219cd061509e88edd9e3dbc9bc5b8","kind":"calls","metadata":"{\"confidence\":0.4,\"resolvedBy\":\"exact-match\",\"refName\":\"draw\"}","line":300,"col":17,"provenance":null} {"source":"field:9b8892c2103e8d26a2d169b562508cad","target":"method:830203603e74b357be75b6353bc5377e","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"helper\"}","line":26,"col":45,"provenance":null} {"source":"field:aad418c7800fdc96bf64e4ae2e8fbbbf","target":"class:c4ed77533cd6f14e451fe7962f484e20","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"field:ff5d8f1e89169ffa0d4d1902eb2302b1","target":"method:0f8219cd061509e88edd9e3dbc9bc5b8","kind":"calls","metadata":"{\"confidence\":0.4,\"resolvedBy\":\"exact-match\",\"refName\":\"draw\"}","line":294,"col":17,"provenance":null} {"source":"file:ScalaCompanions.scala","target":"class:732f40a08bad7780ec21fd2cecf39a1f","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:ScalaCompanions.scala","target":"function:25ffa07c15bde4ba9c80922e52dae992","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:ScalaCompanions.scala","target":"module:2e6745c0540c4440974994e84f20e21f","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} @@ -2798,7 +2794,6 @@ {"source":"function:112bb390a54043c8ee6b3ee94ba6c956","target":"function:21ca45d9f9601839f9b8698f898ae7d9","kind":"calls","metadata":"{\"confidence\":0.4,\"resolvedBy\":\"exact-match\",\"refName\":\"localFn\"}","line":13,"col":9,"provenance":null} {"source":"function:112bb390a54043c8ee6b3ee94ba6c956","target":"function:21ca45d9f9601839f9b8698f898ae7d9","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"function:1144fd3aac9231a341249e5320f6e942","target":"method:dbc683708e8c0217a76edc711cab984f","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"instance-method\",\"refName\":\"Widget.create\"}","line":50,"col":9,"provenance":null} -{"source":"function:15790b08cab7e8d7cb5b8a474d94c359","target":"method:0f8219cd061509e88edd9e3dbc9bc5b8","kind":"calls","metadata":"{\"confidence\":0.4,\"resolvedBy\":\"exact-match\",\"refName\":\"draw\"}","line":253,"col":4,"provenance":null} {"source":"function:1932b46aef9b5a9c946962703eaf2c1c","target":"class:508cb01a02b2ec3d871fa137a11a8863","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Holder\"}","line":15,"col":14,"provenance":null} {"source":"function:1f9113c1936c681d582e2e8de8a47736","target":"class:fa355652331eee519e97b9ef15a61b2d","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"WidgetT\"}","line":57,"col":29,"provenance":null} {"source":"function:1ff8eb433358524f81c9fa301bb39b42","target":"function:df2774b45646f4acb1867fa96cb169c6","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"innermost\"}","line":36,"col":4,"provenance":null} @@ -2823,7 +2818,7 @@ {"source":"function:452af9ab50e94846870ef417047aa76b","target":"function:dd54d7d1bdb0ef2012631716e516c4a7","kind":"calls","metadata":"{\"synthesizedBy\":\"rtk-query\",\"via\":\"updateWidget\",\"registeredAt\":\"torture.tsx:184\"}","line":184,"col":null,"provenance":"heuristic"} {"source":"function:4c142f94bfc81f2dbaeb53322f651474","target":"function:a8c2c8f513bc437341958e1e409a8797","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"shadowed_reader\"}","line":128,"col":12,"provenance":null} {"source":"function:5a7d206b3379379dcd0e8c8ee7fbb2e6","target":"method:e20a7ba4db91a5635fcd83cfeb744be8","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"other\"}","line":51,"col":0,"provenance":null} -{"source":"function:5b434632abf9fbeb07e0a630ec66d9d3","target":"class:555fa7a8e0dcaf3765cd04fadb6e6b7d","kind":"references","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Registry\"}","line":186,"col":4,"provenance":null} +{"source":"function:5b434632abf9fbeb07e0a630ec66d9d3","target":"class:555fa7a8e0dcaf3765cd04fadb6e6b7d","kind":"references","metadata":"{\"framework\":\"spring\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Registry\"}","line":186,"col":4,"provenance":null} {"source":"function:5b434632abf9fbeb07e0a630ec66d9d3","target":"class:90a8667278ecdb516d10fcf149e250b7","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"function:5b434632abf9fbeb07e0a630ec66d9d3","target":"class:c2dfee99ca5361d8de7ae7de6858d0bf","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"WidgetK\",\"refKind\":\"calls\"}","line":181,"col":12,"provenance":null} {"source":"function:5b434632abf9fbeb07e0a630ec66d9d3","target":"enum:b3d793adc52e140cd3c453271f86633e","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Color\"}","line":187,"col":4,"provenance":null} @@ -2836,15 +2831,9 @@ {"source":"function:5b434632abf9fbeb07e0a630ec66d9d3","target":"function:b67d89cb05fb830ccc666734f2335062","kind":"calls","metadata":"{\"confidence\":0.4,\"resolvedBy\":\"exact-match\",\"refName\":\"draw\"}","line":221,"col":22,"provenance":null} {"source":"function:5b434632abf9fbeb07e0a630ec66d9d3","target":"function:b67d89cb05fb830ccc666734f2335062","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"function:5b434632abf9fbeb07e0a630ec66d9d3","target":"function:c062ca02de706ffe85e7dfb2f9586331","kind":"references","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"function-ref\",\"refName\":\"topLevel\",\"refKind\":\"function_ref\",\"fnRef\":true}","line":207,"col":15,"provenance":null} -{"source":"function:5b434632abf9fbeb07e0a630ec66d9d3","target":"method:23a3fe53759ac075ceb2da316b712eb7","kind":"calls","metadata":"{\"confidence\":0.3,\"resolvedBy\":\"fuzzy\",\"refName\":\"toString\"}","line":183,"col":4,"provenance":null} -{"source":"function:5b434632abf9fbeb07e0a630ec66d9d3","target":"method:256f83a3935f0101fcb9728044b46196","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"register\"}","line":207,"col":4,"provenance":null} -{"source":"function:5b434632abf9fbeb07e0a630ec66d9d3","target":"method:256f83a3935f0101fcb9728044b46196","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"register\"}","line":208,"col":4,"provenance":null} -{"source":"function:5b434632abf9fbeb07e0a630ec66d9d3","target":"method:256f83a3935f0101fcb9728044b46196","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"register\"}","line":209,"col":4,"provenance":null} -{"source":"function:5b434632abf9fbeb07e0a630ec66d9d3","target":"method:256f83a3935f0101fcb9728044b46196","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"register\"}","line":210,"col":4,"provenance":null} {"source":"function:5b434632abf9fbeb07e0a630ec66d9d3","target":"method:256f83a3935f0101fcb9728044b46196","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"instance-method\",\"refName\":\"Registry.register\"}","line":185,"col":4,"provenance":null} -{"source":"function:5b434632abf9fbeb07e0a630ec66d9d3","target":"method:80d4962209cb02f005f979978aea0d64","kind":"calls","metadata":"{\"confidence\":0.5,\"resolvedBy\":\"exact-match\",\"refName\":\"hashCode\"}","line":184,"col":4,"provenance":null} {"source":"function:5b434632abf9fbeb07e0a630ec66d9d3","target":"method:96cd1708b4f116d8dcdcdc32127274c8","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"instance-method\",\"refName\":\"w.chainInner\"}","line":192,"col":4,"provenance":null} -{"source":"function:5b434632abf9fbeb07e0a630ec66d9d3","target":"method:de718159205cba9b0d17647ac71f8f2a","kind":"calls","metadata":"{\"confidence\":0.4,\"resolvedBy\":\"exact-match\",\"refName\":\"render\"}","line":192,"col":4,"provenance":null} +{"source":"function:5b434632abf9fbeb07e0a630ec66d9d3","target":"method:de718159205cba9b0d17647ac71f8f2a","kind":"calls","metadata":"{\"confidence\":0.4,\"resolvedBy\":\"exact-match\",\"refName\":\"render\"}","line":192,"col":19,"provenance":null} {"source":"function:5b434632abf9fbeb07e0a630ec66d9d3","target":"method:de718159205cba9b0d17647ac71f8f2a","kind":"calls","metadata":"{\"confidence\":0.85,\"resolvedBy\":\"instance-method\",\"refName\":\"WidgetK.create().render\"}","line":189,"col":4,"provenance":null} {"source":"function:5b434632abf9fbeb07e0a630ec66d9d3","target":"method:de718159205cba9b0d17647ac71f8f2a","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"instance-method\",\"refName\":\"it.render\"}","line":205,"col":12,"provenance":null} {"source":"function:5b434632abf9fbeb07e0a630ec66d9d3","target":"method:de718159205cba9b0d17647ac71f8f2a","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"instance-method\",\"refName\":\"w.render\"}","line":182,"col":4,"provenance":null} @@ -2901,7 +2890,6 @@ {"source":"function:92e7a37a37fcdd1233f2060dcaa08d46","target":"function:262df8cc0bb3d2bff4ad563022b44561","kind":"references","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"function-ref\",\"refName\":\"onFire\",\"refKind\":\"function_ref\",\"fnRef\":true}","line":101,"col":7,"provenance":null} {"source":"function:92e7a37a37fcdd1233f2060dcaa08d46","target":"function:510392b0722e8898090560466c5a5ec5","kind":"references","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"function-ref\",\"refName\":\"handler\",\"refKind\":\"function_ref\",\"fnRef\":true}","line":100,"col":9,"provenance":null} {"source":"function:92e7a37a37fcdd1233f2060dcaa08d46","target":"function:5856f8137240d29ffd5ec6ced63378bc","kind":"references","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"function-ref\",\"refName\":\"cbB\",\"refKind\":\"function_ref\",\"fnRef\":true}","line":99,"col":18,"provenance":null} -{"source":"function:92e7a37a37fcdd1233f2060dcaa08d46","target":"method:84b1066798bf4890531cf6d4cd0b61b2","kind":"calls","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"instance-method\",\"refName\":\"Foo.make\"}","line":88,"col":2,"provenance":null} {"source":"function:92e7a37a37fcdd1233f2060dcaa08d46","target":"method:84b1066798bf4890531cf6d4cd0b61b2","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"make\"}","line":93,"col":2,"provenance":null} {"source":"function:973ef3de44975d37a59ae5993585dcb8","target":"enum:fa2b2411f27fe00e081bceff1312896a","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Flag\"}","line":31,"col":13,"provenance":null} {"source":"function:973ef3de44975d37a59ae5993585dcb8","target":"enum:fa2b2411f27fe00e081bceff1312896a","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Flag\"}","line":32,"col":12,"provenance":null} @@ -2939,7 +2927,7 @@ {"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"method:7e714128b5b3c81a4e4f571a142197ba","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"qualified-name\",\"refName\":\"Widget::Widget/0\"}","line":43,"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":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Box\"}","line":45,"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:dcb8897c9696246c318260fb9188d7fb","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Aggregate\"}","line":38,"col":2,"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} @@ -3032,8 +3020,6 @@ {"source":"method:38d0e81e9dc9d2296999d41cf9892c2a","target":"function:f4175a0ec088921527968e2183a05349","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"method:38d0e81e9dc9d2296999d41cf9892c2a","target":"function:fcb3c4babe7cfee4371d8e472c3bde0b","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"method:3b1cb571f833b38e29e2b07a5203ff27","target":"constant:b80ad042b76cc2c5a15bf2e1a5d743c8","kind":"references","metadata":"{\"valueRef\":true}","line":null,"col":null,"provenance":null} -{"source":"method:3c1ef2bb8932e71bb8cd70805321f3d9","target":"method:256f83a3935f0101fcb9728044b46196","kind":"calls","metadata":"{\"confidence\":0.5,\"resolvedBy\":\"exact-match\",\"refName\":\"register\"}","line":10,"col":4,"provenance":null} -{"source":"method:3c1ef2bb8932e71bb8cd70805321f3d9","target":"method:256f83a3935f0101fcb9728044b46196","kind":"calls","metadata":"{\"confidence\":0.5,\"resolvedBy\":\"exact-match\",\"refName\":\"register\"}","line":5,"col":4,"provenance":null} {"source":"method:3c1ef2bb8932e71bb8cd70805321f3d9","target":"method:3a24ef3bc85eda3ec9b2937c0945ca17","kind":"references","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"function-ref\",\"refName\":\"other\",\"refKind\":\"function_ref\",\"fnRef\":true}","line":8,"col":13,"provenance":null} {"source":"method:3c1ef2bb8932e71bb8cd70805321f3d9","target":"method:b4c76b45b0f8fe58f57f25c34f68d473","kind":"references","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"function-ref\",\"refName\":\"handler\",\"refKind\":\"function_ref\",\"fnRef\":true}","line":5,"col":13,"provenance":null} {"source":"method:3fd4d3ca878172f4f933189084dfb3f4","target":"class:fa355652331eee519e97b9ef15a61b2d","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"WidgetT\",\"refKind\":\"calls\"}","line":133,"col":34,"provenance":null} @@ -3051,9 +3037,8 @@ {"source":"method:4fcd52c7c720267b0a4cf6b9515e3e1f","target":"method:b5862db2c719b17231639bd84992b513","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"reduce\"}","line":25,"col":20,"provenance":null} {"source":"method:5066dc69ba3ae94e164d9fc751bbc008","target":"class:89a6327dc2e1da4b990c740c891aaa96","kind":"references","metadata":"{\"confidence\":0.4,\"resolvedBy\":\"exact-match\",\"refName\":\"Session\"}","line":25,"col":12,"provenance":null} {"source":"method:5066dc69ba3ae94e164d9fc751bbc008","target":"constant:48a4b90850e760164e1a2d8c20240c05","kind":"references","metadata":"{\"valueRef\":true}","line":null,"col":null,"provenance":null} -{"source":"method:5066dc69ba3ae94e164d9fc751bbc008","target":"method:84b1066798bf4890531cf6d4cd0b61b2","kind":"calls","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"instance-method\",\"refName\":\"DataRequest.make\"}","line":23,"col":14,"provenance":null} {"source":"method:5066dc69ba3ae94e164d9fc751bbc008","target":"struct:6d96b486579cf3fdd66718872215294c","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"HTTPMethod\"}","line":24,"col":12,"provenance":null} -{"source":"method:5155cb511053014dd8982984611ab0d9","target":"class:75b65f6c8276b034f5dd64a930bffc4d","kind":"references","metadata":"{\"confidence\":0.85,\"resolvedBy\":\"framework\",\"refName\":\"TortureService\"}","line":83,"col":18,"provenance":null} +{"source":"method:5155cb511053014dd8982984611ab0d9","target":"class:75b65f6c8276b034f5dd64a930bffc4d","kind":"references","metadata":"{\"framework\":\"spring\",\"confidence\":0.85,\"resolvedBy\":\"framework\",\"refName\":\"TortureService\"}","line":83,"col":18,"provenance":null} {"source":"method:51d6ebbeee95e3c8b19527e13c10be19","target":"enum:d04eb96f4adcd7f03628e1fe8ae2194d","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"ReadType\"}","line":107,"col":20,"provenance":null} {"source":"method:554c9b2fd11df120a567061a2671400f","target":"constant:7c3ccde0c9a76211b187a9b4d337eec3","kind":"references","metadata":"{\"valueRef\":true}","line":null,"col":null,"provenance":null} {"source":"method:57860865154ab9d9c7dc0dd14f5eaa79","target":"variable:6adfcec1106f14a610956b32cf60e621","kind":"references","metadata":"{\"valueRef\":true}","line":null,"col":null,"provenance":null} @@ -3128,7 +3113,6 @@ {"source":"method:dbc683708e8c0217a76edc711cab984f","target":"class:7b9fe84bf0a135fd43dd8e0a43329327","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Widget\"}","line":37,"col":11,"provenance":null} {"source":"method:dbedc27f10579fd7567f0cbda93fb625","target":"method:4ae22cd4d1a4129053ffee5c71219d2c","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"_check\"}","line":138,"col":28,"provenance":null} {"source":"method:dd439e569e5643b7eab01af84e7b86a4","target":"method:2bb5bc1d26d448e27c9c5b94d36a1051","kind":"references","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"function-ref\",\"refName\":\"this.onTick\",\"refKind\":\"function_ref\",\"fnRef\":true}","line":29,"col":18,"provenance":null} -{"source":"method:de718159205cba9b0d17647ac71f8f2a","target":"method:5c7ee5f7df4ea497aa603662d6664c48","kind":"calls","metadata":"{\"confidence\":0.4,\"resolvedBy\":\"exact-match\",\"refName\":\"draw\"}","line":77,"col":8,"provenance":null} {"source":"method:dfc92724bc8e56ce4fffe93fa68a9d43","target":"class:fa355652331eee519e97b9ef15a61b2d","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"WidgetT\",\"refKind\":\"calls\"}","line":143,"col":42,"provenance":null} {"source":"method:dfc92724bc8e56ce4fffe93fa68a9d43","target":"class:fa355652331eee519e97b9ef15a61b2d","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"WidgetT\"}","line":143,"col":2,"provenance":null} {"source":"method:dfc92724bc8e56ce4fffe93fa68a9d43","target":"class:fa355652331eee519e97b9ef15a61b2d","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"WidgetT\"}","line":143,"col":21,"provenance":null} @@ -3137,7 +3121,6 @@ {"source":"method:e349908111fb7121ad8228137a8e2647","target":"constant:7af11b36e8d4ddbbdc72a75e5d163a1f","kind":"references","metadata":"{\"valueRef\":true}","line":null,"col":null,"provenance":null} {"source":"method:e721803e0b4da8cdc52d9c909006f907","target":"class:fa355652331eee519e97b9ef15a61b2d","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"WidgetT\",\"refKind\":\"calls\"}","line":127,"col":37,"provenance":null} {"source":"method:e947ab8444253c3743504176397bb986","target":"class:c2dfee99ca5361d8de7ae7de6858d0bf","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"WidgetK\",\"refKind\":\"calls\"}","line":85,"col":32,"provenance":null} -{"source":"method:e9e838e0226a45eac1e61f723fb346d1","target":"method:3c1ef2bb8932e71bb8cd70805321f3d9","kind":"calls","metadata":"{\"confidence\":0.4,\"resolvedBy\":\"exact-match\",\"refName\":\"wire\"}","line":24,"col":4,"provenance":null} {"source":"method:e9e838e0226a45eac1e61f723fb346d1","target":"method:51799b32d3d9fffc601ddaa5e68cd56a","kind":"references","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"function-ref\",\"refName\":\"cbTarget\",\"refKind\":\"function_ref\",\"fnRef\":true}","line":24,"col":14,"provenance":null} {"source":"method:ea9d3d2b24e0b1e6559d12766186eb52","target":"variable:8fef25ea8abe486ee94190ed47c3644b","kind":"references","metadata":"{\"valueRef\":true}","line":null,"col":null,"provenance":null} {"source":"method:ec47a62d06708c1f02fc28a967649234","target":"class:63dc577067a8afeda1988d7d6845f698","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Table\"}","line":13,"col":8,"provenance":null} @@ -3361,7 +3344,7 @@ {"source":"struct:8af466b07dba0b708a3b1922902ab00a","target":"method:c19898b8b84cce6cc2b80007d1b6f588","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"struct:8af466b07dba0b708a3b1922902ab00a","target":"trait:7cbd29a1473d80a74da70be5024bc1d6","kind":"implements","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Source\"}","line":130,"col":5,"provenance":null} {"source":"struct:8dd22c4fc56baeba045e9bf82329d7eb","target":"trait:dcec472700533c7d65fd2f90ea3c6830","kind":"implements","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Base\"}","line":169,"col":5,"provenance":null} -{"source":"struct:c1ae7a1f5526f34c24c26eeaa97abd1b","target":"struct:c1ae7a1f5526f34c24c26eeaa97abd1b","kind":"extends","metadata":"{\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Counter\"}","line":29,"col":2,"provenance":null} +{"source":"struct:c1ae7a1f5526f34c24c26eeaa97abd1b","target":"struct:c1ae7a1f5526f34c24c26eeaa97abd1b","kind":"extends","metadata":"{\"framework\":\"go\",\"confidence\":0.7,\"resolvedBy\":\"framework\",\"refName\":\"Counter\"}","line":29,"col":2,"provenance":null} {"source":"struct:dc1fb066a6c08cc705216a045fa93a24","target":"method:36b2c35b96abd233c6449d02d9c4ef2f","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"struct:edcb8b15c67daf532c1057d9efe8ac3d","target":"method:0843076b0b3cc6a4287f1bbbb3ec931d","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"struct:edcb8b15c67daf532c1057d9efe8ac3d","target":"method:64996a1706443006da9f4a15b38abbe2","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} @@ -3391,7 +3374,7 @@ {"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:d8f9c30c4ee99a9302a150e8eed67acf","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 (1272) +== refs (1289) {"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:066eb79e83cf3105cebd14593747068f","reference_name":"BaseService","reference_kind":"extends","line":13,"col":14,"candidates":null,"file_path":"torture.py","language":"python","status":"failed","name_tail":"BaseService"} @@ -3479,6 +3462,7 @@ {"from_node_id":"class:addcf598d6e590538ddf583221f35a77","reference_name":"Registry","reference_kind":"decorates","line":23,"col":0,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"Registry"} {"from_node_id":"class:addcf598d6e590538ddf583221f35a77","reference_name":"Serializable","reference_kind":"implements","line":24,"col":77,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"Serializable"} {"from_node_id":"class:c2dfee99ca5361d8de7ae7de6858d0bf","reference_name":"log","reference_kind":"calls","line":73,"col":8,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"log"} +{"from_node_id":"class:c2dfee99ca5361d8de7ae7de6858d0bf","reference_name":"register","reference_kind":"calls","line":69,"col":8,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"register"} {"from_node_id":"class:c44cb299e35396bde78f29f0000a6649","reference_name":"BaseDto(Name)","reference_kind":"extends","line":8,"col":46,"candidates":null,"file_path":"TortureFileScoped.cs","language":"csharp","status":"failed","name_tail":"BaseDto(Name)"} {"from_node_id":"class:c44cb299e35396bde78f29f0000a6649","reference_name":"IThing","reference_kind":"extends","line":8,"col":61,"candidates":null,"file_path":"TortureFileScoped.cs","language":"csharp","status":"failed","name_tail":"IThing"} {"from_node_id":"class:c4ed77533cd6f14e451fe7962f484e20","reference_name":"Runnable","reference_kind":"extends","line":26,"col":46,"candidates":null,"file_path":"Torture.java","language":"java","status":"failed","name_tail":"Runnable"} @@ -3491,6 +3475,7 @@ {"from_node_id":"class:c5612d8a0e4cc01ff1ebb6299d21d6b3","reference_name":"Widget","reference_kind":"extends","line":19,"col":32,"candidates":null,"file_path":"Torture.cs","language":"csharp","status":"failed","name_tail":"Widget"} {"from_node_id":"class:cf8bad734e91e79fb7c91d19629af923","reference_name":"compute","reference_kind":"calls","line":303,"col":23,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"compute"} {"from_node_id":"class:cf8bad734e91e79fb7c91d19629af923","reference_name":"makePair","reference_kind":"calls","line":301,"col":27,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"makePair"} +{"from_node_id":"class:cf8bad734e91e79fb7c91d19629af923","reference_name":"register","reference_kind":"calls","line":304,"col":8,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"register"} {"from_node_id":"class:d849f5e76a1ac71848eaf9c9986f695c","reference_name":"decorator","reference_kind":"decorates","line":16,"col":0,"candidates":null,"file_path":"docstrings.py","language":"python","status":"failed","name_tail":"decorator"} {"from_node_id":"class:da855a76803320806ba08457a72b59fa","reference_name":"Forward","reference_kind":"extends","line":20,"col":31,"candidates":null,"file_path":"torture.hpp","language":"cpp","status":"failed","name_tail":"Forward"} {"from_node_id":"class:da855a76803320806ba08457a72b59fa","reference_name":"Forward","reference_kind":"extends","line":26,"col":2,"candidates":null,"file_path":"torture.hpp","language":"cpp","status":"failed","name_tail":"Forward"} @@ -3565,6 +3550,7 @@ {"from_node_id":"enum:f20e15cb271bf122063fc98641681a0b","reference_name":"HasColor","reference_kind":"implements","line":197,"col":29,"candidates":null,"file_path":"torture.php","language":"php","status":"failed","name_tail":"HasColor"} {"from_node_id":"field:1c2a880a9a9f3a515a296919af3f2481","reference_name":"IRepo","reference_kind":"references","line":28,"col":16,"candidates":null,"file_path":"Torture.cs","language":"csharp","status":"failed","name_tail":"IRepo"} {"from_node_id":"field:1d9e3a840eac3d3d6b0d2607418da6e8","reference_name":"compute","reference_kind":"calls","line":299,"col":16,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"compute"} +{"from_node_id":"field:1d9e3a840eac3d3d6b0d2607418da6e8","reference_name":"draw","reference_kind":"calls","line":300,"col":17,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"draw"} {"from_node_id":"field:2d9816236940ff530104ce7cd9e51f38","reference_name":"List","reference_kind":"references","line":23,"col":16,"candidates":null,"file_path":"Torture.java","language":"java","status":"failed","name_tail":"List"} {"from_node_id":"field:38e1637682007f7d1ba0a794fcf7f60b","reference_name":"helperCall","reference_kind":"calls","line":196,"col":19,"candidates":null,"file_path":"torture.scala","language":"scala","status":"failed","name_tail":"helperCall"} {"from_node_id":"field:4ad98fd51d9139d3ee2e9f06c7cef392","reference_name":"helperCall","reference_kind":"calls","line":195,"col":37,"candidates":null,"file_path":"torture.scala","language":"scala","status":"failed","name_tail":"helperCall"} @@ -3584,6 +3570,7 @@ {"from_node_id":"field:f55a20c13201bd32369db7de5edde8d0","reference_name":"Action","reference_kind":"references","line":31,"col":21,"candidates":null,"file_path":"Torture.cs","language":"csharp","status":"failed","name_tail":"Action"} {"from_node_id":"field:f55a20c13201bd32369db7de5edde8d0","reference_name":"List","reference_kind":"references","line":31,"col":16,"candidates":null,"file_path":"Torture.cs","language":"csharp","status":"failed","name_tail":"List"} {"from_node_id":"field:ff5d8f1e89169ffa0d4d1902eb2302b1","reference_name":"compute","reference_kind":"calls","line":293,"col":34,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"compute"} +{"from_node_id":"field:ff5d8f1e89169ffa0d4d1902eb2302b1","reference_name":"draw","reference_kind":"calls","line":294,"col":17,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"draw"} {"from_node_id":"file:TortureExt.scala","reference_name":"concat","reference_kind":"calls","line":2,"col":25,"candidates":null,"file_path":"TortureExt.scala","language":"scala","status":"failed","name_tail":"concat"} {"from_node_id":"file:TortureExt.scala","reference_name":"xs.drop","reference_kind":"calls","line":10,"col":54,"candidates":null,"file_path":"TortureExt.scala","language":"scala","status":"failed","name_tail":"drop"} {"from_node_id":"file:TortureGiven.scala","reference_name":"Ordering","reference_kind":"instantiates","line":8,"col":31,"candidates":null,"file_path":"TortureGiven.scala","language":"scala","status":"failed","name_tail":"Ordering"} @@ -3782,6 +3769,7 @@ {"from_node_id":"function:1116b32a328c7f5fd04a16f5e6445a4a","reference_name":"trailing()","reference_kind":"calls","line":244,"col":4,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"trailing()"} {"from_node_id":"function:112bb390a54043c8ee6b3ee94ba6c956","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: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: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":"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:178c6f5073dbe3085b9a39b8c2723861","reference_name":"standardGeneric","reference_kind":"calls","line":78,"col":37,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"standardGeneric"} @@ -3852,7 +3840,7 @@ {"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: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":"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":4,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"forEach"} +{"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"} {"from_node_id":"function:5a7d206b3379379dcd0e8c8ee7fbb2e6","reference_name":"OtherClass","reference_kind":"references","line":51,"col":6,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"OtherClass"} {"from_node_id":"function:5a7d206b3379379dcd0e8c8ee7fbb2e6","reference_name":"other.OtherClass","reference_kind":"calls","line":51,"col":50,"candidates":null,"file_path":"torture.dart","language":"dart","status":"failed","name_tail":"OtherClass"} @@ -3861,12 +3849,13 @@ {"from_node_id":"function:5b434632abf9fbeb07e0a630ec66d9d3","reference_name":"OtherClass::handle","reference_kind":"function_ref","line":208,"col":25,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"handle"} {"from_node_id":"function:5b434632abf9fbeb07e0a630ec66d9d3","reference_name":"Short.static","reference_kind":"calls","line":199,"col":4,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"static","failure_reason":"unknown-receiver"} {"from_node_id":"function:5b434632abf9fbeb07e0a630ec66d9d3","reference_name":"anon.draw","reference_kind":"calls","line":233,"col":4,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"draw","failure_reason":"unknown-receiver"} -{"from_node_id":"function:5b434632abf9fbeb07e0a630ec66d9d3","reference_name":"chain","reference_kind":"calls","line":191,"col":4,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"chain"} +{"from_node_id":"function:5b434632abf9fbeb07e0a630ec66d9d3","reference_name":"chain","reference_kind":"calls","line":191,"col":19,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"chain"} {"from_node_id":"function:5b434632abf9fbeb07e0a630ec66d9d3","reference_name":"fn","reference_kind":"calls","line":201,"col":4,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"fn"} {"from_node_id":"function:5b434632abf9fbeb07e0a630ec66d9d3","reference_name":"fn(3)","reference_kind":"calls","line":201,"col":4,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"fn(3)"} {"from_node_id":"function:5b434632abf9fbeb07e0a630ec66d9d3","reference_name":"fn(3)\n (fn)","reference_kind":"calls","line":201,"col":4,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"fn(3)\n (fn)"} -{"from_node_id":"function:5b434632abf9fbeb07e0a630ec66d9d3","reference_name":"forEach","reference_kind":"calls","line":204,"col":4,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"forEach"} +{"from_node_id":"function:5b434632abf9fbeb07e0a630ec66d9d3","reference_name":"forEach","reference_kind":"calls","line":204,"col":14,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"forEach"} {"from_node_id":"function:5b434632abf9fbeb07e0a630ec66d9d3","reference_name":"generic","reference_kind":"calls","line":206,"col":4,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"generic"} +{"from_node_id":"function:5b434632abf9fbeb07e0a630ec66d9d3","reference_name":"hashCode","reference_kind":"calls","line":184,"col":10,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"hashCode"} {"from_node_id":"function:5b434632abf9fbeb07e0a630ec66d9d3","reference_name":"helper","reference_kind":"calls","line":198,"col":4,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"helper"} {"from_node_id":"function:5b434632abf9fbeb07e0a630ec66d9d3","reference_name":"helper","reference_kind":"calls","line":203,"col":10,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"helper"} {"from_node_id":"function:5b434632abf9fbeb07e0a630ec66d9d3","reference_name":"helper","reference_kind":"calls","line":217,"col":13,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"helper"} @@ -3876,7 +3865,12 @@ {"from_node_id":"function:5b434632abf9fbeb07e0a630ec66d9d3","reference_name":"lowerFactory","reference_kind":"calls","line":191,"col":4,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"lowerFactory"} {"from_node_id":"function:5b434632abf9fbeb07e0a630ec66d9d3","reference_name":"obtain","reference_kind":"calls","line":211,"col":4,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"obtain"} {"from_node_id":"function:5b434632abf9fbeb07e0a630ec66d9d3","reference_name":"println","reference_kind":"calls","line":200,"col":29,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"println"} +{"from_node_id":"function:5b434632abf9fbeb07e0a630ec66d9d3","reference_name":"register","reference_kind":"calls","line":207,"col":4,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"register"} +{"from_node_id":"function:5b434632abf9fbeb07e0a630ec66d9d3","reference_name":"register","reference_kind":"calls","line":208,"col":4,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"register"} +{"from_node_id":"function:5b434632abf9fbeb07e0a630ec66d9d3","reference_name":"register","reference_kind":"calls","line":209,"col":4,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"register"} +{"from_node_id":"function:5b434632abf9fbeb07e0a630ec66d9d3","reference_name":"register","reference_kind":"calls","line":210,"col":4,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"register"} {"from_node_id":"function:5b434632abf9fbeb07e0a630ec66d9d3","reference_name":"this.caller","reference_kind":"function_ref","line":210,"col":19,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"caller"} +{"from_node_id":"function:5b434632abf9fbeb07e0a630ec66d9d3","reference_name":"toString","reference_kind":"calls","line":183,"col":9,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"toString"} {"from_node_id":"function:5b434632abf9fbeb07e0a630ec66d9d3","reference_name":"w.let","reference_kind":"calls","line":205,"col":4,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"let","failure_reason":"unknown-receiver"} {"from_node_id":"function:5dc6386ba18e3f1f04ce01163a8dd750","reference_name":"after_emoji","reference_kind":"calls","line":204,"col":27,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"after_emoji"} {"from_node_id":"function:5dc6386ba18e3f1f04ce01163a8dd750","reference_name":"target_fn","reference_kind":"calls","line":204,"col":47,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"target_fn"} @@ -3986,6 +3980,7 @@ {"from_node_id":"function:92e7a37a37fcdd1233f2060dcaa08d46","reference_name":"API.PackageController.GetRoute.query","reference_kind":"calls","line":90,"col":2,"candidates":null,"file_path":"torture.swift","language":"swift","status":"failed","name_tail":"query"} {"from_node_id":"function:92e7a37a37fcdd1233f2060dcaa08d46","reference_name":"App","reference_kind":"references","line":94,"col":2,"candidates":null,"file_path":"torture.swift","language":"swift","status":"failed","name_tail":"App"} {"from_node_id":"function:92e7a37a37fcdd1233f2060dcaa08d46","reference_name":"Foo","reference_kind":"calls","line":83,"col":2,"candidates":null,"file_path":"torture.swift","language":"swift","status":"failed","name_tail":"Foo"} +{"from_node_id":"function:92e7a37a37fcdd1233f2060dcaa08d46","reference_name":"Foo.make","reference_kind":"calls","line":88,"col":2,"candidates":null,"file_path":"torture.swift","language":"swift","status":"failed","name_tail":"make"} {"from_node_id":"function:92e7a37a37fcdd1233f2060dcaa08d46","reference_name":"Foo.make().draw","reference_kind":"calls","line":88,"col":2,"candidates":null,"file_path":"torture.swift","language":"swift","status":"failed","name_tail":"draw"} {"from_node_id":"function:92e7a37a37fcdd1233f2060dcaa08d46","reference_name":"Holder","reference_kind":"references","line":103,"col":22,"candidates":null,"file_path":"torture.swift","language":"swift","status":"failed","name_tail":"Holder"} {"from_node_id":"function:92e7a37a37fcdd1233f2060dcaa08d46","reference_name":"Self","reference_kind":"references","line":93,"col":2,"candidates":null,"file_path":"torture.swift","language":"swift","status":"failed","name_tail":"Self"} @@ -4254,6 +4249,8 @@ {"from_node_id":"method:3ac9d03b2b42036d5064669684b96d97","reference_name":"staticmethod","reference_kind":"decorates","line":21,"col":4,"candidates":null,"file_path":"torture.py","language":"python","status":"failed","name_tail":"staticmethod"} {"from_node_id":"method:3ac9d03b2b42036d5064669684b96d97","reference_name":"transform","reference_kind":"calls","line":23,"col":15,"candidates":null,"file_path":"torture.py","language":"python","status":"failed","name_tail":"transform"} {"from_node_id":"method:3b1cb571f833b38e29e2b07a5203ff27","reference_name":"DefaultName.Trim","reference_kind":"calls","line":150,"col":35,"candidates":null,"file_path":"Torture.cs","language":"csharp","status":"failed","name_tail":"Trim"} +{"from_node_id":"method:3c1ef2bb8932e71bb8cd70805321f3d9","reference_name":"register","reference_kind":"calls","line":10,"col":4,"candidates":null,"file_path":"TortureFnref.scala","language":"scala","status":"failed","name_tail":"register"} +{"from_node_id":"method:3c1ef2bb8932e71bb8cd70805321f3d9","reference_name":"register","reference_kind":"calls","line":5,"col":4,"candidates":null,"file_path":"TortureFnref.scala","language":"scala","status":"failed","name_tail":"register"} {"from_node_id":"method:3c1ef2bb8932e71bb8cd70805321f3d9","reference_name":"registerEta","reference_kind":"calls","line":6,"col":4,"candidates":null,"file_path":"TortureFnref.scala","language":"scala","status":"failed","name_tail":"registerEta"} {"from_node_id":"method:3d1787adb831782b4a90413dd941412b","reference_name":"d.draw","reference_kind":"calls","line":93,"col":21,"candidates":null,"file_path":"torture.scala","language":"scala","status":"failed","name_tail":"draw"} {"from_node_id":"method:3e0eaf75fa55d6bb243377f25e7b18b2","reference_name":"length","reference_kind":"calls","line":108,"col":22,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"length"} @@ -4286,6 +4283,7 @@ {"from_node_id":"method:4fcd52c7c720267b0a4cf6b9515e3e1f","reference_name":"own","reference_kind":"calls","line":22,"col":12,"candidates":null,"file_path":"TortureCtors.dart","language":"dart","status":"failed","name_tail":"own"} {"from_node_id":"method:4fcd52c7c720267b0a4cf6b9515e3e1f","reference_name":"parent","reference_kind":"calls","line":23,"col":16,"candidates":null,"file_path":"TortureCtors.dart","language":"dart","status":"failed","name_tail":"parent"} {"from_node_id":"method:5066dc69ba3ae94e164d9fc751bbc008","reference_name":"DataRequest","reference_kind":"references","line":22,"col":76,"candidates":null,"file_path":"torture.swift","language":"swift","status":"failed","name_tail":"DataRequest"} +{"from_node_id":"method:5066dc69ba3ae94e164d9fc751bbc008","reference_name":"DataRequest.make","reference_kind":"calls","line":23,"col":14,"candidates":null,"file_path":"torture.swift","language":"swift","status":"failed","name_tail":"make"} {"from_node_id":"method:5066dc69ba3ae94e164d9fc751bbc008","reference_name":"DataRequest.make().validate","reference_kind":"calls","line":23,"col":14,"candidates":null,"file_path":"torture.swift","language":"swift","status":"failed","name_tail":"validate"} {"from_node_id":"method:5066dc69ba3ae94e164d9fc751bbc008","reference_name":"own","reference_kind":"calls","line":26,"col":4,"candidates":null,"file_path":"torture.swift","language":"swift","status":"failed","name_tail":"own"} {"from_node_id":"method:5066dc69ba3ae94e164d9fc751bbc008","reference_name":"perform","reference_kind":"calls","line":28,"col":22,"candidates":null,"file_path":"torture.swift","language":"swift","status":"failed","name_tail":"perform"} @@ -4511,12 +4509,14 @@ {"from_node_id":"method:d85e75675c9c25670ba7f631481482f8","reference_name":"override","reference_kind":"decorates","line":45,"col":2,"candidates":null,"file_path":"TortureMini.dart","language":"dart","status":"failed","name_tail":"override"} {"from_node_id":"method:dd439e569e5643b7eab01af84e7b86a4","reference_name":"register","reference_kind":"calls","line":29,"col":4,"candidates":null,"file_path":"torture.js","language":"javascript","status":"failed","name_tail":"register"} {"from_node_id":"method:dd439e569e5643b7eab01af84e7b86a4","reference_name":"super","reference_kind":"calls","line":27,"col":4,"candidates":null,"file_path":"torture.js","language":"javascript","status":"failed","name_tail":"super"} +{"from_node_id":"method:de718159205cba9b0d17647ac71f8f2a","reference_name":"draw","reference_kind":"calls","line":77,"col":8,"candidates":null,"file_path":"torture.kt","language":"kotlin","status":"failed","name_tail":"draw"} {"from_node_id":"method:e05bd15d7804605167b8fadd29a26f6b","reference_name":"this.items.push","reference_kind":"calls","line":232,"col":28,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"push"} {"from_node_id":"method:e05bd15d7804605167b8fadd29a26f6b","reference_name":"this.mailer.send","reference_kind":"calls","line":232,"col":50,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"send"} {"from_node_id":"method:e05bd15d7804605167b8fadd29a26f6b","reference_name":"this.mailer.send().trim","reference_kind":"calls","line":232,"col":50,"candidates":null,"file_path":"torture.tsx","language":"tsx","status":"failed","name_tail":"trim"} {"from_node_id":"method:e13588704c75e83833678ebaf5efcf3a","reference_name":"p_call","reference_kind":"calls","line":139,"col":32,"candidates":null,"file_path":"torture.R","language":"r","status":"failed","name_tail":"p_call"} {"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"} {"from_node_id":"method:e9e838e0226a45eac1e61f723fb346d1","reference_name":"forward","reference_kind":"calls","line":25,"col":4,"candidates":null,"file_path":"TortureGiven.scala","language":"scala","status":"failed","name_tail":"forward"} +{"from_node_id":"method:e9e838e0226a45eac1e61f723fb346d1","reference_name":"wire","reference_kind":"calls","line":24,"col":4,"candidates":null,"file_path":"TortureGiven.scala","language":"scala","status":"failed","name_tail":"wire"} {"from_node_id":"method:ea9d3d2b24e0b1e6559d12766186eb52","reference_name":"fetch","reference_kind":"calls","line":30,"col":21,"candidates":null,"file_path":"torture.py","language":"python","status":"failed","name_tail":"fetch"} {"from_node_id":"method:ea9d3d2b24e0b1e6559d12766186eb52","reference_name":"obj.method_chain","reference_kind":"calls","line":28,"col":8,"candidates":null,"file_path":"torture.py","language":"python","status":"failed","name_tail":"method_chain","failure_reason":"unknown-receiver"} {"from_node_id":"method:ea9d3d2b24e0b1e6559d12766186eb52","reference_name":"obj.method_chain().deep","reference_kind":"calls","line":28,"col":8,"candidates":null,"file_path":"torture.py","language":"python","status":"failed","name_tail":"deep"} diff --git a/__tests__/fixtures/golden/vue-sfc.dump b/__tests__/fixtures/golden/vue-sfc.dump index 2621e67263..40a73a9d51 100644 --- a/__tests__/fixtures/golden/vue-sfc.dump +++ b/__tests__/fixtures/golden/vue-sfc.dump @@ -76,12 +76,12 @@ {"source":"component:d243da7dd7c0285f73fbc46926de4830","target":"file:src/components/PlayerTable.vue","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"component:d243da7dd7c0285f73fbc46926de4830","target":"function:cbf5b3b6d9cf6f3f971b79716cb24ec1","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"constant:17aec3feee6b90e31758cb1b41bc1451","target":"constant:452cdd44fc910418f24644cca18f6ebf","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"import\",\"refName\":\"useSessionStore\"}","line":10,"col":16,"provenance":null} -{"source":"constant:751e16382875beaa12f2dc749e1d9872","target":"constant:751e16382875beaa12f2dc749e1d9872","kind":"calls","metadata":"{\"confidence\":1,\"resolvedBy\":\"framework\",\"refName\":\"useRouter\"}","line":10,"col":15,"provenance":null} +{"source":"constant:751e16382875beaa12f2dc749e1d9872","target":"constant:751e16382875beaa12f2dc749e1d9872","kind":"calls","metadata":"{\"framework\":\"vue\",\"confidence\":1,\"resolvedBy\":\"framework\",\"refName\":\"useRouter\"}","line":10,"col":15,"provenance":null} {"source":"constant:c14c87c0b0c5b511e512bce492e77e4d","target":"constant:452cdd44fc910418f24644cca18f6ebf","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"import\",\"refName\":\"useSessionStore\"}","line":11,"col":16,"provenance":null} -{"source":"constant:cb450172e958b905f9dac8f66786c11a","target":"constant:cb450172e958b905f9dac8f66786c11a","kind":"calls","metadata":"{\"confidence\":1,\"resolvedBy\":\"framework\",\"refName\":\"useRouter\"}","line":9,"col":15,"provenance":null} -{"source":"constant:cc53d30032c6a64950637c56908b7d0b","target":"component:4adc8e1e4bb22917b580e3d56a1ea521","kind":"imports","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"framework\",\"refName\":\"@/views/Home.vue\"}","line":5,"col":48,"provenance":null} -{"source":"constant:cc53d30032c6a64950637c56908b7d0b","target":"component:51ec8fb32fb2e5eb0334651e5437dbb3","kind":"imports","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"framework\",\"refName\":\"@/views/Profile.vue\"}","line":7,"col":68,"provenance":null} -{"source":"constant:cc53d30032c6a64950637c56908b7d0b","target":"component:bb25dfa092c88625d329317b0410e2cf","kind":"imports","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"framework\",\"refName\":\"@/views/Login.vue\"}","line":6,"col":54,"provenance":null} +{"source":"constant:cb450172e958b905f9dac8f66786c11a","target":"constant:cb450172e958b905f9dac8f66786c11a","kind":"calls","metadata":"{\"framework\":\"vue\",\"confidence\":1,\"resolvedBy\":\"framework\",\"refName\":\"useRouter\"}","line":9,"col":15,"provenance":null} +{"source":"constant:cc53d30032c6a64950637c56908b7d0b","target":"component:4adc8e1e4bb22917b580e3d56a1ea521","kind":"imports","metadata":"{\"framework\":\"vue\",\"confidence\":0.9,\"resolvedBy\":\"framework\",\"refName\":\"@/views/Home.vue\"}","line":5,"col":48,"provenance":null} +{"source":"constant:cc53d30032c6a64950637c56908b7d0b","target":"component:51ec8fb32fb2e5eb0334651e5437dbb3","kind":"imports","metadata":"{\"framework\":\"vue\",\"confidence\":0.9,\"resolvedBy\":\"framework\",\"refName\":\"@/views/Profile.vue\"}","line":7,"col":68,"provenance":null} +{"source":"constant:cc53d30032c6a64950637c56908b7d0b","target":"component:bb25dfa092c88625d329317b0410e2cf","kind":"imports","metadata":"{\"framework\":\"vue\",\"confidence\":0.9,\"resolvedBy\":\"framework\",\"refName\":\"@/views/Login.vue\"}","line":6,"col":54,"provenance":null} {"source":"file:src/components/PlayerTable.vue","target":"function:cbf5b3b6d9cf6f3f971b79716cb24ec1","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:src/components/TheHeader.vue","target":"constant:a9f2e86d1d4a623601d625c820f6d4f8","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:src/router/index.ts","target":"constant:cc53d30032c6a64950637c56908b7d0b","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} @@ -91,17 +91,17 @@ {"source":"file:src/stores/session.ts","target":"function:6be3944381225a6a828e91729caf5788","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:src/stores/session.ts","target":"function:98084f78440e902f0fb6526f37df2e46","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:src/stores/session.ts","target":"import:4c3d03fe5b468700553c025bde17c5da","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"file:src/views/Home.vue","target":"component:a2b972002bba2cab19e7c5b8376f5608","kind":"imports","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"framework\",\"refName\":\"@/components/TheHeader.vue\"}","line":7,"col":0,"provenance":null} {"source":"file:src/views/Home.vue","target":"component:a2b972002bba2cab19e7c5b8376f5608","kind":"imports","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"import\",\"refName\":\"TheHeader\"}","line":7,"col":7,"provenance":null} -{"source":"file:src/views/Home.vue","target":"component:d243da7dd7c0285f73fbc46926de4830","kind":"imports","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"framework\",\"refName\":\"@/components/PlayerTable.vue\"}","line":8,"col":0,"provenance":null} +{"source":"file:src/views/Home.vue","target":"component:a2b972002bba2cab19e7c5b8376f5608","kind":"imports","metadata":"{\"framework\":\"vue\",\"confidence\":0.9,\"resolvedBy\":\"framework\",\"refName\":\"@/components/TheHeader.vue\"}","line":7,"col":0,"provenance":null} {"source":"file:src/views/Home.vue","target":"component:d243da7dd7c0285f73fbc46926de4830","kind":"imports","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"import\",\"refName\":\"PlayerTable\"}","line":8,"col":7,"provenance":null} +{"source":"file:src/views/Home.vue","target":"component:d243da7dd7c0285f73fbc46926de4830","kind":"imports","metadata":"{\"framework\":\"vue\",\"confidence\":0.9,\"resolvedBy\":\"framework\",\"refName\":\"@/components/PlayerTable.vue\"}","line":8,"col":0,"provenance":null} {"source":"file:src/views/Home.vue","target":"constant:452cdd44fc910418f24644cca18f6ebf","kind":"imports","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"import\",\"refName\":\"useSessionStore\"}","line":9,"col":9,"provenance":null} {"source":"file:src/views/Home.vue","target":"constant:5d677039776558e912ac55676ec98100","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:src/views/Home.vue","target":"constant:751e16382875beaa12f2dc749e1d9872","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:src/views/Home.vue","target":"constant:c14c87c0b0c5b511e512bce492e77e4d","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:src/views/Home.vue","target":"constant:d9be2d3d04398687cd5215bbe3161ba5","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"file:src/views/Home.vue","target":"file:src/stores/session.ts","kind":"imports","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"framework\",\"refName\":\"@/stores/session\"}","line":9,"col":0,"provenance":null} -{"source":"file:src/views/Home.vue","target":"file:src/views/Home.vue","kind":"imports","metadata":"{\"confidence\":1,\"resolvedBy\":\"framework\",\"refName\":\"useRouter\"}","line":6,"col":9,"provenance":null} +{"source":"file:src/views/Home.vue","target":"file:src/stores/session.ts","kind":"imports","metadata":"{\"framework\":\"vue\",\"confidence\":0.9,\"resolvedBy\":\"framework\",\"refName\":\"@/stores/session\"}","line":9,"col":0,"provenance":null} +{"source":"file:src/views/Home.vue","target":"file:src/views/Home.vue","kind":"imports","metadata":"{\"framework\":\"vue\",\"confidence\":1,\"resolvedBy\":\"framework\",\"refName\":\"useRouter\"}","line":6,"col":9,"provenance":null} {"source":"file:src/views/Home.vue","target":"function:acb5cc12f5a5aac4fe728346a0195105","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:src/views/Home.vue","target":"import:027a889525f1e6e315129a7ad281251d","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:src/views/Home.vue","target":"import:abb463c3ae2e046fb414b1cc9e6791d0","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} @@ -112,18 +112,18 @@ {"source":"file:src/views/Login.vue","target":"constant:452cdd44fc910418f24644cca18f6ebf","kind":"imports","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"import\",\"refName\":\"useSessionStore\"}","line":7,"col":9,"provenance":null} {"source":"file:src/views/Login.vue","target":"constant:cb450172e958b905f9dac8f66786c11a","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:src/views/Login.vue","target":"constant:ff0949df989967149797da1892030a1d","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} -{"source":"file:src/views/Login.vue","target":"file:src/stores/session.ts","kind":"imports","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"framework\",\"refName\":\"@/stores/session\"}","line":7,"col":0,"provenance":null} -{"source":"file:src/views/Login.vue","target":"file:src/views/Login.vue","kind":"imports","metadata":"{\"confidence\":1,\"resolvedBy\":\"framework\",\"refName\":\"useRouter\"}","line":6,"col":9,"provenance":null} +{"source":"file:src/views/Login.vue","target":"file:src/stores/session.ts","kind":"imports","metadata":"{\"framework\":\"vue\",\"confidence\":0.9,\"resolvedBy\":\"framework\",\"refName\":\"@/stores/session\"}","line":7,"col":0,"provenance":null} +{"source":"file:src/views/Login.vue","target":"file:src/views/Login.vue","kind":"imports","metadata":"{\"framework\":\"vue\",\"confidence\":1,\"resolvedBy\":\"framework\",\"refName\":\"useRouter\"}","line":6,"col":9,"provenance":null} {"source":"file:src/views/Login.vue","target":"function:8d98565edda11804927409e8e272013e","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:src/views/Login.vue","target":"import:18d11ae5a76f290915b58b51dd533f3f","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:src/views/Login.vue","target":"import:cb1810af8536de8faf29efc9265c1557","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"file:src/views/Login.vue","target":"import:f38ceaeccfd0e13483a209bbc3005ae3","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"function:8d98565edda11804927409e8e272013e","target":"function:6be3944381225a6a828e91729caf5788","kind":"calls","metadata":"{\"synthesizedBy\":\"pinia-store\",\"via\":\"login\",\"registeredAt\":\"src/views/Login.vue:12\"}","line":12,"col":null,"provenance":"heuristic"} -{"source":"function:8d98565edda11804927409e8e272013e","target":"route:src/router/index.ts:5:/:vue","kind":"navigates","metadata":"{\"href\":\"home\",\"navMethod\":\"push\",\"by\":\"name\",\"confidence\":0.95,\"resolvedBy\":\"framework\",\"refName\":\"router.push\",\"refKind\":\"calls\"}","line":13,"col":8,"provenance":null} -{"source":"function:acb5cc12f5a5aac4fe728346a0195105","target":"route:src/router/index.ts:5:/:vue","kind":"navigates","metadata":"{\"href\":\"/\",\"navMethod\":\"push\",\"confidence\":0.95,\"resolvedBy\":\"framework\",\"refName\":\"router.push\",\"refKind\":\"calls\"}","line":15,"col":2,"provenance":null} -{"source":"route:src/router/index.ts:5:/:vue","target":"component:4adc8e1e4bb22917b580e3d56a1ea521","kind":"calls","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"framework\",\"refName\":\"import:@/views/Home.vue#Home\"}","line":5,"col":0,"provenance":null} -{"source":"route:src/router/index.ts:6:/login:vue","target":"component:bb25dfa092c88625d329317b0410e2cf","kind":"calls","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"framework\",\"refName\":\"import:@/views/Login.vue#Login\"}","line":6,"col":0,"provenance":null} -{"source":"route:src/router/index.ts:7:/profile/:username:vue","target":"component:51ec8fb32fb2e5eb0334651e5437dbb3","kind":"calls","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"framework\",\"refName\":\"import:@/views/Profile.vue#Profile\"}","line":7,"col":0,"provenance":null} +{"source":"function:8d98565edda11804927409e8e272013e","target":"route:src/router/index.ts:5:/:vue","kind":"navigates","metadata":"{\"href\":\"home\",\"navMethod\":\"push\",\"by\":\"name\",\"framework\":\"vue-router\",\"confidence\":0.95,\"resolvedBy\":\"framework\",\"refName\":\"router.push\",\"refKind\":\"calls\"}","line":13,"col":8,"provenance":null} +{"source":"function:acb5cc12f5a5aac4fe728346a0195105","target":"route:src/router/index.ts:5:/:vue","kind":"navigates","metadata":"{\"href\":\"/\",\"navMethod\":\"push\",\"framework\":\"vue-router\",\"confidence\":0.95,\"resolvedBy\":\"framework\",\"refName\":\"router.push\",\"refKind\":\"calls\"}","line":15,"col":2,"provenance":null} +{"source":"route:src/router/index.ts:5:/:vue","target":"component:4adc8e1e4bb22917b580e3d56a1ea521","kind":"calls","metadata":"{\"framework\":\"vue-router\",\"confidence\":0.95,\"resolvedBy\":\"framework\",\"refName\":\"import:@/views/Home.vue#Home\"}","line":5,"col":0,"provenance":null} +{"source":"route:src/router/index.ts:6:/login:vue","target":"component:bb25dfa092c88625d329317b0410e2cf","kind":"calls","metadata":"{\"framework\":\"vue-router\",\"confidence\":0.95,\"resolvedBy\":\"framework\",\"refName\":\"import:@/views/Login.vue#Login\"}","line":6,"col":0,"provenance":null} +{"source":"route:src/router/index.ts:7:/profile/:username:vue","target":"component:51ec8fb32fb2e5eb0334651e5437dbb3","kind":"calls","metadata":"{\"framework\":\"vue-router\",\"confidence\":0.95,\"resolvedBy\":\"framework\",\"refName\":\"import:@/views/Profile.vue#Profile\"}","line":7,"col":0,"provenance":null} == refs (24) {"from_node_id":"component:a2b972002bba2cab19e7c5b8376f5608","reference_name":"RouterLink","reference_kind":"references","line":2,"col":77,"candidates":null,"file_path":"src/components/TheHeader.vue","language":"vue","status":"failed","name_tail":"RouterLink"} {"from_node_id":"component:bb25dfa092c88625d329317b0410e2cf","reference_name":"RouterLink","reference_kind":"references","line":2,"col":58,"candidates":null,"file_path":"src/views/Login.vue","language":"vue","status":"failed","name_tail":"RouterLink"} diff --git a/__tests__/flow-js.test.ts b/__tests__/flow-js.test.ts new file mode 100644 index 0000000000..1c77e14797 --- /dev/null +++ b/__tests__/flow-js.test.ts @@ -0,0 +1,82 @@ +/** + * A Flow-typed `.js` file (`// @flow` in its leading comments) is read with the + * TSX grammar: the JavaScript grammar can't parse its annotations, and a + * `render(): React.Node` cut the class around it short — segmented-control's + * component came out with no methods. Flow's own syntax (`{| |}`, `?T`, an + * inexact object's `...`, `import typeof`, `opaque type`) is blanked first. + * A `.js` without the pragma is untouched. + */ +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 { detectLanguage } from '../src/extraction/grammars'; +import { blankFlowSyntax } from '../src/extraction/languages/typescript'; + +let root = ''; +let cg: CodeGraph; + +const FLOW = `/** + * @flow strict-local + * @format + */ +import typeof * as React from 'react'; +import * as React from 'react'; + +type Props = $ReadOnly<{| + values: ?Array, + onChange?: ?(event: {nativeEvent: {value: string}, ...}) => mixed, +|}>; + +opaque type Token = string; + +class SegmentedControl extends React.Component { + render(): React.Node { + return this._renderItems(); + } + + _renderItems(): React.Node { + return null; + } +} + +module.exports = SegmentedControl; +`; + +beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-flow-')); + fs.mkdirSync(path.join(root, 'js'), { recursive: true }); + fs.writeFileSync(path.join(root, 'js/SegmentedControl.js'), FLOW); + fs.writeFileSync(path.join(root, 'js/plain.js'), `function plain() { return 1; }\nmodule.exports = plain;\n`); + cg = await CodeGraph.init(root, { index: true }); +}); + +afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); +}); + +describe('Flow-typed JavaScript', () => { + it('is detected by its pragma, and only then', () => { + expect(detectLanguage('js/SegmentedControl.js', FLOW)).toBe('tsx'); + expect(detectLanguage('js/plain.js', 'function plain() {}')).toBe('javascript'); + expect(detectLanguage('js/x.js', '// @noflow\nconst a = 1;')).toBe('javascript'); + }); + + it('keeps its class and methods', () => { + const nodes = cg.getNodesInFile('js/SegmentedControl.js'); + const methods = nodes.filter((n) => n.kind === 'method').map((n) => n.name).sort(); + expect(methods).toEqual(['_renderItems', 'render']); + const render = nodes.find((n) => n.name === 'render')!; + const calls = cg.getOutgoingEdges(render.id).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.name); + expect(calls).toContain('_renderItems'); + }); + + it('blanks Flow-only syntax to spaces, keeping every offset', () => { + const out = blankFlowSyntax(FLOW, 'js/SegmentedControl.js'); + expect(out.length).toBe(FLOW.length); + expect(out).not.toMatch(/\{\||\|\}|import\s+typeof|opaque\s+type|:\s*\?Array/); + expect(blankFlowSyntax('const a = {| b |};', 'plain.js')).toBe('const a = {| b |};'); + }); +}); diff --git a/__tests__/framework-edge-name.test.ts b/__tests__/framework-edge-name.test.ts new file mode 100644 index 0000000000..b2081c82e6 --- /dev/null +++ b/__tests__/framework-edge-name.test.ts @@ -0,0 +1,57 @@ +/** + * A link a framework or bridge resolver made names that resolver on the edge + * (`metadata.framework`), as a synthesized channel names itself with + * `synthesizedBy`: a Swift → Objective-C hop says `swift-objc-bridge`. + */ +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-framework-edge-')); + const files: Record = { + 'Sources/Cache.h': `#import +@interface Cache : NSObject +- (id)fetchEntryForKey:(NSString *)key; +@end +`, + 'Sources/Cache.m': `#import "Cache.h" +@implementation Cache +- (id)fetchEntryForKey:(NSString *)key { + return nil; +} +@end +`, + 'Sources/Caller.swift': `import Foundation + +func load(cache: Cache) { + _ = cache.fetchEntry(forKey: "x") +} +`, + }; + 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 }); +}); + +describe('framework-resolved edges', () => { + it('name the resolver that made them', () => { + const caller = cg.getNodesInFile('Sources/Caller.swift').find((n) => n.name === 'load')!; + const bridged = cg.getOutgoingEdges(caller.id).find((e) => cg.getNode(e.target)?.name === 'fetchEntryForKey:'); + expect(bridged).toBeDefined(); + expect(bridged!.metadata?.resolvedBy).toBe('framework'); + expect(bridged!.metadata?.framework).toBe('swift-objc-bridge'); + }); +}); diff --git a/__tests__/java-declared-receiver.test.ts b/__tests__/java-declared-receiver.test.ts new file mode 100644 index 0000000000..fd9978d579 --- /dev/null +++ b/__tests__/java-declared-receiver.test.ts @@ -0,0 +1,135 @@ +/** + * A Java receiver's declared type is read through generics and for-each + * loops (`final Pair pair`, `for (Element el : this)`), from a field of + * the class (`private DateFormat dateTimeParser;` — outside the project, so + * no edge), or as the bound of a type parameter (`T extends ExceptionContext`). + * A constant's receiver is its type (`Phase.CONTROLLERS.getPhase()`). None of + * these fall back to a same-named method of some other project class: + * commons-lang's `dateTimeParser.parse(…)` went to its own `DateParser.parse` + * 284 times. + */ +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-java-receiver-')); + const files: Record = { + 'src/main/java/app/Pair.java': `package app; + +public abstract class Pair { + public abstract L getKey(); +} +`, + 'src/main/java/app/MutablePair.java': `package app; + +public class MutablePair extends Pair { + private L left; + + public L getKey() { return left; } + + /** + * Copies the key of each element into this pair. + */ + public void setValue(final Pair pair) { + left = pair.getKey(); + } +} +`, + 'src/main/java/app/DateParser.java': `package app; + +public interface DateParser { + Object parse(String source); +} +`, + 'src/main/java/app/Element.java': `package app; + +public class Element { + public void val(String value) { } +} +`, + 'src/main/java/app/Elements.java': `package app; + +import java.util.ArrayList; + +public class Elements extends ArrayList { + public Elements val(String value) { + for (Element element : this) + element.val(value); + return this; + } +} +`, + 'src/main/java/app/Phase.java': `package app; + +public enum Phase { + CONTROLLERS; + + public int getPhase() { return 0; } +} +`, + 'src/main/java/app/ExceptionContext.java': `package app; + +public interface ExceptionContext { + int getContextEntries(); +} +`, + 'src/test/java/app/DateUtilsTest.java': `package app; + +import java.text.DateFormat; + +public abstract class DateUtilsTest { + private DateFormat dateTimeParser; + protected T exceptionContext; + + void rounds() throws Exception { + dateTimeParser.parse("January 1, 2008"); + exceptionContext.getContextEntries(); + Phase.CONTROLLERS.getPhase(); + } +} +`, + }; + 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 }); +}); + +/** `Owner::member` of every call edge out of a file. */ +function callsFrom(file: string): string[] { + const ids = cg.getNodesInFile(file).map((n) => n.id); + return cg + .getOutgoingEdgesFrom(ids) + .filter((e) => e.kind === 'calls') + .map((e) => cg.getNode(e.target)!.qualifiedName) + .sort(); +} + +describe('Java calls through declared receivers', () => { + it('read a generic parameter’s type', () => { + expect(callsFrom('src/main/java/app/MutablePair.java')).toEqual(['app::Pair::getKey']); + }); + + it('read a for-each variable’s type', () => { + expect(callsFrom('src/main/java/app/Elements.java')).toEqual(['app::Element::val']); + }); + + it('follow a field’s type, a type parameter’s bound and a constant’s type — and stop at an outside type', () => { + expect(callsFrom('src/test/java/app/DateUtilsTest.java')).toEqual([ + 'app::ExceptionContext::getContextEntries', + 'app::Phase::getPhase', + ]); + }); +}); diff --git a/__tests__/kotlin-bare-member-scope.test.ts b/__tests__/kotlin-bare-member-scope.test.ts new file mode 100644 index 0000000000..2d1dd09bf3 --- /dev/null +++ b/__tests__/kotlin-bare-member-scope.test.ts @@ -0,0 +1,175 @@ +/** + * A bare Kotlin call reaches a member of a type around it — a class, an + * anonymous `object : Base()`, an extension function's receiver — or of what + * those inherit; a member of a type the project's function types take as a + * lambda receiver (a DSL: koin's `single { get() }` runs on a Scope); or an + * object member the file imports. It never reaches another class's member by + * name: koin's `error("…")` — Kotlin's — went to a Logger's `error`, and + * `module { … }` in one test to another test class's private `module`. + * `require(n >= 0) { … }` is Kotlin's precondition, not a member `require`. + */ +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-kotlin-member-')); + const files: Record = { + 'src/main/kotlin/app/Config.kt': `package app +class Config { + fun configureValue() { } +} +fun interface Configurer { + fun Config.configure() +} +fun withConfig(configurer: Configurer) { } +class Validation { + companion object { + fun installMapper() { } + } +} +`, + 'src/main/kotlin/consumer/ConfigTest.kt': `package consumer +import app.withConfig +import app.Validation.Companion.installMapper +fun configureApp() { + withConfig { configureValue() } + installMapper() +} +`, + 'src/main/kotlin/app/Transaction.kt': `package app +class CoreTransaction +typealias Transaction = CoreTransaction +fun CoreTransaction.verify(value: Boolean) { } +`, + 'src/main/kotlin/consumer/TransactionTest.kt': `package consumer +import app.Transaction +import app.verify +fun withTransaction(body: Transaction.() -> Unit) { } +fun testTransaction() { + withTransaction { verify(true) } +} +`, + 'src/main/kotlin/app/dsl/Module.kt': `package app.dsl + +class Scope { + fun get(): Any = 1 +} + +class Koin { + fun get(): Any = 2 +} + +class Module { + fun single(definition: Scope.() -> Any) { } +} + +fun module(declaration: Module.() -> Unit): Module = Module().apply(declaration) +`, + 'src/main/kotlin/app/log/Logger.kt': `package app.log + +class Logger { + fun error(message: String) { } +} +`, + 'src/main/kotlin/app/db/Table.kt': `package app.db + +abstract class Table { + fun integer(name: String): Int = 0 +} +`, + 'src/main/kotlin/app/io/Source.kt': `package app.io + +interface Source { + fun require(byteCount: Long) +} + +class RealSource : Source { + override fun require(byteCount: Long) { } + + fun read(byteCount: Long) { + require(byteCount >= 0L) { "byteCount < 0: $byteCount" } + require(byteCount) + } +} +`, + 'src/test/kotlin/app/TestUtil.kt': `package app + +object TestUtil { + fun deepCopy(value: Int): Int = value +} +`, + 'src/test/kotlin/app/AppTest.kt': `package app + +import app.TestUtil.deepCopy +import app.db.Table +import app.dsl.module + +class AppTest { + fun wires() { + val m = module { + single { get() } + } + val table = object : Table() { + val id = integer("id") + } + val copy = deepCopy(1) + if (copy < 0) error("negative") + } +} +`, + }; + 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 }); +}); + +/** `Owner::member` of every call edge out of a file. */ +function callsFrom(file: string): string[] { + const ids = cg.getNodesInFile(file).map((n) => n.id); + return cg + .getOutgoingEdgesFrom(ids) + .filter((e) => e.kind === 'calls') + .map((e) => cg.getNode(e.target)!.qualifiedName) + .sort(); +} + +describe('bare Kotlin calls', () => { + it('reach a DSL lambda’s receiver, an anonymous object’s base and an imported object member — nothing else', () => { + const calls = callsFrom('src/test/kotlin/app/AppTest.kt'); + expect(calls).toContain('app.dsl::Module::single'); + expect(calls).toContain('app.dsl::Scope::get'); + expect(calls).toContain('app.db::Table::integer'); + expect(calls).toContain('app::TestUtil::deepCopy'); + expect(calls).not.toContain('app.dsl::Koin::get'); + expect(calls).not.toContain('app.log::Logger::error'); + }); + + it('leave Kotlin’s precondition `require(cond) { … }` to the standard library', () => { + const read = cg.getNodesInFile('src/main/kotlin/app/io/Source.kt').find((n) => n.name === 'read')!; + const requires = cg.getOutgoingEdgesFrom([read.id]).filter((e) => e.kind === 'calls').map((e) => e.line); + expect(requires).toEqual([12]); + }); +}); + +it('keeps an imported top-level extension callable inside a receiver lambda', () => { + expect(callsFrom('src/main/kotlin/consumer/TransactionTest.kt')).toContain('CoreTransaction::verify'); +}); + +it('keeps receiver SAM calls and source-validated companion imports', () => { + const calls = callsFrom('src/main/kotlin/consumer/ConfigTest.kt'); + expect(calls).toContain('app::Config::configureValue'); + expect(calls.some((name) => name.endsWith('::installMapper'))).toBe(true); +}); diff --git a/__tests__/kotlin-column-extension-scope.test.ts b/__tests__/kotlin-column-extension-scope.test.ts new file mode 100644 index 0000000000..d33774c323 --- /dev/null +++ b/__tests__/kotlin-column-extension-scope.test.ts @@ -0,0 +1,58 @@ +import { expect, it } from 'vitest'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { CodeGraph } from '../src'; + +it('binds imported SQL-style extensions through declared property initializer returns', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-kotlin-column-')); + let cg: CodeGraph | undefined; + try { + fs.writeFileSync(path.join(dir, 'Api.kt'), `package api +open class Expression +open class ExpressionWithColumnType : Expression() +class Column : ExpressionWithColumnType() +class LiteralOp : ExpressionWithColumnType() +fun stringLiteral(value: String): LiteralOp = LiteralOp() +fun helper() { fun stringLiteral(value: String): String = value } +fun Expression.trim(): Expression = this +open class Table { fun varchar(name: String): Column = Column() } +fun Table.json(name: String): Column = Column() +fun ExpressionWithColumnType.count(): Int = 0 +fun ExpressionWithColumnType.contains(value: String): Boolean = false +fun Expression.substring(start: Int, end: Int): Expression = this +class AbstractQuery { fun count(): Int = 0 } +class External { fun count(): Int = 0 } +`); + fs.writeFileSync(path.join(dir, 'Tables.kt'), `package app +import api.Table +import api.json +object Users : Table() { val name = varchar("name") } +object Teams : Table() { val project = json("project") } +`); + fs.writeFileSync(path.join(dir, 'Use.kt'), `package app +import api.count +import api.contains +import api.substring +import api.External +import api.stringLiteral +import api.trim +fun trimmed() = stringLiteral(" x ").trim() +fun countNames() = Users.name.count() +fun hasProject() = Teams.project.contains("x") +fun shortened() = Users.name.substring(1, 2) +fun unrelated(value: External) = value.count() +`); + cg = await CodeGraph.init(dir, { index: true }); + const targets = (name: string) => cg!.getOutgoingEdges(cg!.getNodesInFile('Use.kt').find((n) => n.name === name)!.id) + .filter((e) => e.kind === 'calls').map((e) => cg!.getNode(e.target)!.qualifiedName); + expect(targets('trimmed')).toContain('Expression::trim'); + expect(targets('countNames')).toEqual(['ExpressionWithColumnType::count']); + expect(targets('hasProject')).toEqual(['ExpressionWithColumnType::contains']); + expect(targets('shortened')).toEqual(['Expression::substring']); + expect(targets('unrelated')).toEqual(['api::External::count']); + } finally { + cg?.close(); + fs.rmSync(dir, { recursive: true, force: true }); + } +}); diff --git a/__tests__/kotlin-receiver-inference-scope.test.ts b/__tests__/kotlin-receiver-inference-scope.test.ts new file mode 100644 index 0000000000..6fbd530d40 --- /dev/null +++ b/__tests__/kotlin-receiver-inference-scope.test.ts @@ -0,0 +1,109 @@ +import { expect, it } from 'vitest'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { CodeGraph } from '../src'; + +it.each([ + ['shadowed factory', `package api +open class Expression +class Column : Expression() +class External { fun count(): Int = 0 } +fun createColumn(): Column = Column() +fun Expression.count(): Int = 0 +`, `package app +import api.createColumn +import api.count +import api.External +fun run(createColumn: () -> External) = createColumn().count() +`, 'Expression::count'], + ['shadowed apply', `package api +class Other +class Validator { + fun check(apply: (() -> Unit) -> Other) = apply { } + fun getOrNull(): String? = null +} +`, `package app +import api.Validator +import api.Other +fun run(value: Validator) = value.check { Other() }.getOrNull() +`, 'api::Validator::getOrNull'], + ['inherited receiver return', `package api +open class Base { fun self() = this } +class Child : Base() { fun count(): Int = 1 } +fun Base.count(): Int = 0 +`, `package app +import api.Child +import api.count +fun run(value: Child) = value.self().count() +`, 'api::Child::count'], + ['overloaded apply', `package api +class Other +class Validator { + fun apply(): Validator = this + fun apply(block: () -> Unit): Other = Other() + fun check() = apply { } + fun getOrNull(): String? = null +} +`, `package app +import api.Validator +fun run(value: Validator) = value.check().getOrNull() +`, 'api::Validator::getOrNull'], + ['labeled this', `package api +class Outer { + fun getOrNull(): String? = null + inner class Inner { + fun parent() = this@Outer + fun getOrNull(): String? = null + } +} +`, `package app +import api.Outer +fun run(value: Outer.Inner) = value.parent().getOrNull() +`, 'api::Outer::Inner::getOrNull'], +])('does not invent a receiver type through %s', async (_label, api, use, unexpected) => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-kotlin-receiver-shadow-')); + let cg: CodeGraph | undefined; + try { + fs.writeFileSync(path.join(dir, 'Api.kt'), api); + fs.writeFileSync(path.join(dir, 'Use.kt'), use); + cg = await CodeGraph.init(dir, { index: true }); + const caller = cg.getNodesInFile('Use.kt').find((n) => n.name === 'run')!; + const targets = cg.getOutgoingEdges(caller.id).filter((e) => e.kind === 'calls').map((e) => cg!.getNode(e.target)!.qualifiedName); + expect(targets).not.toContain(unexpected); + if (_label === 'inherited receiver return') expect(targets).toContain('Base::count'); + } finally { + cg?.close(); + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +it('carries an imported return-type hypothesis through fluent calls below the trust line', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-kotlin-fluent-confidence-')); + let cg: CodeGraph | undefined; + try { + fs.writeFileSync(path.join(dir, 'Api.kt'), `package api +class Context +class Validator { + fun check(value: Boolean) = apply { } + fun copy(): Validator = this + fun getOrNull(): String? = null +} +fun Context.query(key: String): Validator = Validator() +`); + fs.writeFileSync(path.join(dir, 'Use.kt'), `package app +import api.Context +import api.query +fun use(handler: (Context) -> Unit) {} +fun run() { use { it.query("x").check(true).copy().getOrNull() } } +`); + cg = await CodeGraph.init(dir, { index: true }); + const caller = cg.getNodesInFile('Use.kt').find((n) => n.name === 'run')!; + const edge = cg.getOutgoingEdges(caller.id).find((e) => e.kind === 'calls' && cg!.getNode(e.target)!.qualifiedName === 'api::Validator::getOrNull'); + expect(edge).toBeDefined(); + expect(edge!.metadata?.confidence).toBeLessThanOrEqual(0.7); + } finally { + cg?.close(); + fs.rmSync(dir, { recursive: true, force: true }); + } +}); diff --git a/__tests__/kotlin-split-constructor-std-chain.test.ts b/__tests__/kotlin-split-constructor-std-chain.test.ts new file mode 100644 index 0000000000..d5f865710f --- /dev/null +++ b/__tests__/kotlin-split-constructor-std-chain.test.ts @@ -0,0 +1,263 @@ +/** + * Two Kotlin fixes that travel together: + * - a primary constructor on the line after its class name with a supertype + * list (`expect open class ByteString` / `internal constructor(…) : + * Comparable {`, Kotlin Multiplatform's shape) dropped the class + * and scattered its members as loose functions; the constructor's modifiers + * and keyword are blanked (offsets kept) so the class parses; + * - with members back on their classes, a standard-library name on an + * untyped chain link (`builder.apply { … }`, `"x".toPath().toString()`) + * needs a receiver named after the owner — okhttp's `.apply { }` went to + * `ConnectionSpec.apply`. + */ +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 { getAllFrameworkResolvers, registerFrameworkResolver } from '../src/resolution/frameworks'; +import { joinKotlinSplitConstructors } from '../src/extraction/languages/kotlin'; + +let root = ''; +let cg: CodeGraph; + +const BYTE_STRING = `package okio + +expect open class ByteString +// Trusted internal constructor doesn't clone data. +internal constructor(data: ByteArray) : Comparable { + fun hex(): String + fun toByteArray(): ByteArray +} +`; + +beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-kotlin-split-')); + const files: Record = { + 'src/main/kotlin/app/Validator.kt': `package app +class Context +class Validator { + fun check(value: Boolean) = apply { } + fun copy(): Validator = Validator() + fun mixed(value: Int): Validator = this + fun mixed(value: String): Other = Other() + fun other(): Other = Other() + fun getOrNull(): String? = null + fun getOrDefault(value: String): String = value +} +class ShadowValidator { + fun apply(block: () -> Unit): Other = Other() + fun check() = apply { } + fun getOrNull(): String? = null +} +class ShadowContext { fun query(): ShadowValidator = ShadowValidator() } +fun Context.queryParamAsClass(key: String): Validator = Validator() +class Other { + fun queryParamAsClass(key: String): String = key +} +`, + 'src/main/kotlin/consumer/Validation.kt': `package consumer +import app.Context +import app.Other +import app.ShadowContext +import app.queryParamAsClass +fun checked(ctx: Context) = ctx.queryParamAsClass("x") + .check(true) + .copy() + .getOrNull() +fun shadowed(ctx: ShadowContext) = ctx.query().check().getOrNull() +fun ambiguous(ctx: Context) = ctx.queryParamAsClass("x").mixed("x").getOrNull() +fun changed(ctx: Context) = ctx.queryParamAsClass("x").other().getOrNull() +fun multilineArgs(ctx: Context) = ctx.queryParamAsClass( + "x" +).getOrNull() +fun multiline(ctx: Context) = ctx.queryParamAsClass("x") + .getOrNull() +fun positive(ctx: Context) = ctx.queryParamAsClass("x").getOrNull() +fun negative(ctx: Other) = ctx.queryParamAsClass("x").getOrDefault("default") +fun untyped(others: List) { others.forEach { it.queryParamAsClass("x").getOrNull(0) } } +fun constructed() = Other().queryParamAsClass("x").getOrNull(0) +fun nested(ctx: Context, other: Other) = ctx.queryParamAsClass(other.queryParamAsClass("x").getOrNull(0).toString()).getOrNull() +fun repeated(ctx: Context, other: Other) = listOf(other.queryParamAsClass("x").getOrNull(0), ctx.queryParamAsClass("x").getOrNull()) +`, + 'src/commonMain/kotlin/okio/ByteString.kt': BYTE_STRING, + 'src/main/kotlin/app/ConnectionSpec.kt': `package app + +class ConnectionSpec { + fun apply(sslSocket: Any, isFallback: Boolean) {} +} +`, + 'src/main/kotlin/app/Builder.kt': `package app + +class Builder { + var name = "" + fun build(): String = StringBuilder().apply { append(name) }.toString() +} +`, + }; + 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 }); +}); + +describe('Kotlin split primary constructors and standard chain calls', () => { + it('keeps the class and its members', () => { + const nodes = cg.getNodesInFile('src/commonMain/kotlin/okio/ByteString.kt'); + const hex = nodes.find((n) => n.name === 'hex')!; + expect(hex.qualifiedName).toBe('okio::ByteString::hex'); + const out = joinKotlinSplitConstructors(BYTE_STRING); + expect(out.length).toBe(BYTE_STRING.length); + expect(out.split('\n')[4]).toMatch(/^\s+\(data: ByteArray\) : Comparable \{$/); + }); + + it('never sends a scope function to a project type', () => { + const build = cg.getNodesInFile('src/main/kotlin/app/Builder.kt').find((n) => n.name === 'build')!; + const targets = cg.getOutgoingEdges(build.id).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.qualifiedName); + expect(targets.some((t) => t.endsWith('ConnectionSpec::apply'))).toBe(false); + }); +}); + +it('preserves each newline when blanking annotated constructor modifiers', () => { + const source = 'expect open class ByteString\r\n@Inject\r\ninternal constructor(data: ByteArray) : Comparable {\r\n fun hex(): String\r\n}\r\n'; + const out = joinKotlinSplitConstructors(source); + expect(out.length).toBe(source.length); + expect([...out.matchAll(/\r|\n/g)].map((m) => m.index)).toEqual([...source.matchAll(/\r|\n/g)].map((m) => m.index)); +}); + +it('keeps imported extension return-type chains and rejects a different receiver', () => { + const nodes = cg.getNodesInFile('src/main/kotlin/consumer/Validation.kt'); + const targets = (name: string) => cg.getOutgoingEdges(nodes.find((n) => n.name === name)!.id).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.qualifiedName); + expect(targets('positive')).toContain('app::Validator::getOrNull'); + expect(targets('negative')).not.toContain('app::Validator::getOrDefault'); +}); + +it('does not infer an imported extension through a constructor receiver', () => { + const source = cg.getNodesInFile('src/main/kotlin/consumer/Validation.kt').find((n) => n.name === 'constructed')!; + const targets = cg.getOutgoingEdges(source.id).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.qualifiedName); + expect(targets).not.toContain('app::Validator::getOrNull'); +}); + +it('checks the return chain at its own column when method names repeat', () => { + const source = cg.getNodesInFile('src/main/kotlin/consumer/Validation.kt').find((n) => n.name === 'repeated')!; + const targets = cg.getOutgoingEdges(source.id).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.qualifiedName); + expect(targets).toContain('app::Validator::getOrNull'); +}); + +it('does not borrow an extension return type for an untyped lambda receiver', () => { + const source = cg.getNodesInFile('src/main/kotlin/consumer/Validation.kt').find((n) => n.name === 'untyped')!; + const targets = cg.getOutgoingEdges(source.id).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.qualifiedName); + expect(targets).not.toContain('app::Validator::getOrNull'); +}); + +it('keeps a sole imported return-chain candidate below the trust line without a receiver type', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-kotlin-chain-confidence-')); + let graph: CodeGraph | undefined; + try { + fs.writeFileSync(path.join(dir, 'Api.kt'), `package app +class Context +class Validator { + fun getOrNull(): String? = null +} +class Unannotated { + fun queryParamAsClass(key: String) = key +} +fun Context.queryParamAsClass(key: String): Validator = Validator() +`); + fs.writeFileSync(path.join(dir, 'Use.kt'), `package consumer +import app.Context +import app.queryParamAsClass +fun use(handler: (Context) -> Unit) {} +fun run() { use { it.queryParamAsClass("x").getOrNull() } } +`); + graph = await CodeGraph.init(dir, { index: true }); + const caller = graph.getNodesInFile('Use.kt').find((n) => n.name === 'run')!; + const edge = graph.getOutgoingEdges(caller.id).find((e) => e.kind === 'calls' && graph!.getNode(e.target)?.qualifiedName === 'app::Validator::getOrNull'); + expect(edge).toBeDefined(); + expect(edge!.metadata?.confidence).toBeLessThanOrEqual(0.7); + } finally { + graph?.close(); + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +it('positions an outer return-chain call after nested calls with the same name', () => { + const source = cg.getNodesInFile('src/main/kotlin/consumer/Validation.kt').find((n) => n.name === 'nested')!; + const targets = cg.getOutgoingEdges(source.id).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.qualifiedName); + expect(targets).toContain('app::Validator::getOrNull'); +}); + +it('ranks a framework candidate above a capped native return-chain candidate', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-kotlin-framework-confidence-')); + let graph: CodeGraph | undefined; + const name = 'return-chain-confidence-control'; + registerFrameworkResolver({ + name, languages: ['kotlin'], + detect: (context) => context.getProjectRoot() === dir, + resolve: (ref, context) => { + if (ref.referenceName !== 'getOrNull') return null; + const target = context.getNodesByQualifiedName('app::Validator::getOrNull')[0]; + return target ? { original: ref, targetNodeId: target.id, confidence: 0.8, resolvedBy: 'framework' } : null; + }, + }); + try { + fs.writeFileSync(path.join(dir, 'Api.kt'), `package app +class Context +class Validator { fun getOrNull(): String? = null } +fun Context.queryParamAsClass(key: String): Validator = Validator() +`); + fs.writeFileSync(path.join(dir, 'Use.kt'), `package consumer +import app.Context +import app.queryParamAsClass +fun use(handler: (Context) -> Unit) {} +fun run() { use { it.queryParamAsClass("x").getOrNull() } } +`); + graph = await CodeGraph.init(dir, { index: true }); + const caller = graph.getNodesInFile('Use.kt').find((n) => n.name === 'run')!; + const edge = graph.getOutgoingEdges(caller.id).find((e) => e.kind === 'calls' && graph!.getNode(e.target)?.qualifiedName === 'app::Validator::getOrNull'); + expect(edge).toBeDefined(); + expect(edge!.metadata?.confidence).toBe(0.8); + expect(edge!.metadata?.framework).toBe(name); + } finally { + graph?.close(); + const registry = getAllFrameworkResolvers(); + const index = registry.findIndex((resolver) => resolver.name === name); + if (index >= 0) registry.splice(index, 1); + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +it('keeps a declared return chain split across lines', () => { + const caller = cg.getNodesInFile('src/main/kotlin/consumer/Validation.kt').find((n) => n.name === 'multiline')!; + const targets = cg.getOutgoingEdges(caller.id).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.qualifiedName); + expect(targets).toContain('app::Validator::getOrNull'); +}); + +it('keeps a return chain whose arguments span lines', () => { + const caller = cg.getNodesInFile('src/main/kotlin/consumer/Validation.kt').find((n) => n.name === 'multilineArgs')!; + const targets = cg.getOutgoingEdges(caller.id).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.qualifiedName); + expect(targets).toContain('app::Validator::getOrNull'); +}); + +it('follows receiver-preserving and declared-return intermediate chain members', () => { + const nodes = cg.getNodesInFile('src/main/kotlin/consumer/Validation.kt'); + const targets = (name: string) => cg.getOutgoingEdges(nodes.find((n) => n.name === name)!.id).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.qualifiedName); + expect(targets('checked')).toContain('app::Validator::getOrNull'); + expect(targets('changed')).not.toContain('app::Validator::getOrNull'); +}); + +it.each([ + ['shadowed', 'app::ShadowValidator::getOrNull'], + ['ambiguous', 'app::Validator::getOrNull'], +])('declines an unproven return chain for %s', (name, unexpected) => { + const nodes = cg.getNodesInFile('src/main/kotlin/consumer/Validation.kt'); + const caller = nodes.find((n) => n.name === name)!; + const targets = cg.getOutgoingEdges(caller.id).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.qualifiedName); + expect(targets).not.toContain(unexpected); +}); diff --git a/__tests__/kotlin-top-level-visibility.test.ts b/__tests__/kotlin-top-level-visibility.test.ts index f5dce7ed0e..11850413ef 100644 --- a/__tests__/kotlin-top-level-visibility.test.ts +++ b/__tests__/kotlin-top-level-visibility.test.ts @@ -31,21 +31,26 @@ beforeAll(async () => { class JdbcTransaction +fun withTables(statement: JdbcTransaction.() -> Unit) {} + fun JdbcTransaction.assertEquals(exp: T, act: T) {} `, 'r2dbc/src/main/kotlin/app/r2dbc/tests/Assert.kt': `package app.r2dbc.tests class R2dbcTransaction +fun withTables(statement: R2dbcTransaction.() -> Unit) {} + fun R2dbcTransaction.assertEquals(exp: T, act: T) {} `, 'jdbc/src/test/kotlin/app/jdbc/JdbcTest.kt': `package app.jdbc import app.jdbc.tests.assertEquals +import app.jdbc.tests.withTables class JdbcTest { fun works() { - assertEquals(1, 1) + withTables { assertEquals(1, 1) } } } `, @@ -55,7 +60,7 @@ import app.r2dbc.tests.* class R2dbcTest { fun works() { - assertEquals(1, 1) + withTables { assertEquals(1, 1) } } } `, @@ -72,7 +77,7 @@ class PlainTest { 'jdbc/src/main/kotlin/app/jdbc/tests/Local.kt': `package app.jdbc.tests fun check() { - assertEquals(1, 1) + withTables { assertEquals(1, 1) } } `, 'jdbc/src/main/kotlin/app/jdbc/tests/DatabaseTestsBase.kt': `package app.jdbc.tests diff --git a/__tests__/react-router-nested-lazy.test.ts b/__tests__/react-router-nested-lazy.test.ts new file mode 100644 index 0000000000..545d409bf8 --- /dev/null +++ b/__tests__/react-router-nested-lazy.test.ts @@ -0,0 +1,99 @@ +/** + * A data router's nested, lazy, constant-named routes (bulletproof-react): + * - a child's `path` is relative to its parent's — `discussions` under `/app` + * is `/app/discussions`; + * - `lazy: () => import('./routes/x')` renders the module's default export; + * - `path: paths.app.root.path` reads the constant object where it is + * declared — here through a monorepo app's own tsconfig `@/*` alias, which + * now resolves from the tsconfig nearest the importing file; + * - `element: ()` renders AppRoot. + */ +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-rr-nested-')); + const app = (rel: string) => `apps/web/${rel}`; + const files: Record = { + 'package.json': JSON.stringify({ name: 'mono', private: true }), + [app('package.json')]: JSON.stringify({ name: 'web', dependencies: { react: '^18', 'react-router': '^7' } }), + [app('tsconfig.json')]: JSON.stringify({ compilerOptions: { baseUrl: '.', paths: { '@/*': ['./src/*'] } } }), + [app('src/config/paths.ts')]: `export const paths = { + home: { path: '/', getHref: () => '/' }, + app: { + root: { path: '/app', getHref: () => '/app' }, + discussions: { path: 'discussions', getHref: () => '/app/discussions' }, + }, +} as const; +`, + [app('src/app/router.tsx')]: `import { createBrowserRouter } from 'react-router'; +import { paths } from '@/config/paths'; +import { ProtectedRoute } from '@/lib/auth'; +import AppRoot from './routes/app/root'; + +export const router = createBrowserRouter([ + { path: paths.home.path, lazy: () => import('./routes/landing') }, + { + element: ( + + + + ), + children: [ + { path: paths.app.discussions.path, lazy: () => import('./routes/app/discussions') }, + { path: 'settings', element: }, + ], + path: paths.app.root.path, + }, +]); +`, + [app('src/lib/auth.tsx')]: `export function ProtectedRoute({ children }: { children: unknown }) { return children; } +`, + [app('src/app/routes/landing.tsx')]: `const LandingRoute = () => null; +export default LandingRoute; +`, + [app('src/app/routes/app/root.tsx')]: `export default function AppRoot() { return null; } +`, + [app('src/app/routes/app/discussions.tsx')]: `export default function DiscussionsRoute() { return null; } +`, + }; + 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 renders = (routeName: string): 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); +}; + +describe('nested, lazy and constant React Router routes', () => { + it('compose their paths and render their modules', () => { + const names = cg.getNodesByKind('route').map((r) => r.name).sort(); + expect(names).toEqual(['/', '/app', '/app/discussions', '/app/settings']); + expect(renders('/')).toEqual(['LandingRoute']); + expect(renders('/app')).toEqual(['AppRoot']); + expect(renders('/app/discussions')).toEqual(['DiscussionsRoute']); + }); + + 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); + expect(imported).toContain('apps/web/src/config/paths.ts'); + expect(imported).toContain('apps/web/src/lib/auth.tsx'); + }); +}); diff --git a/__tests__/react-router.test.ts b/__tests__/react-router.test.ts index fd4c9d7e90..fa5cd56e16 100644 --- a/__tests__/react-router.test.ts +++ b/__tests__/react-router.test.ts @@ -548,8 +548,9 @@ describe('react-router: route declaration boundaries (#1348)', () => { } `, extension); expect(result).toEqual({ - paths: ['/dashboard', '/empty', '/legacy', '/long', '/no-element', '/sibling', 'settings'], - bindings: ['/legacy->Settings', '/long->Shell', '/sibling->Settings', 'settings->Settings'], + // A nested route's path is relative to its parent's (`settings` under `/dashboard`). + paths: ['/dashboard', '/dashboard/settings', '/empty', '/legacy', '/long', '/no-element', '/sibling'], + bindings: ['/dashboard/settings->Settings', '/legacy->Settings', '/long->Shell', '/sibling->Settings'], }); }); @@ -573,8 +574,8 @@ describe('react-router: route declaration boundaries (#1348)', () => { ]); `, extension); expect(result).toEqual({ - paths: ['/', '/long', '/quoted', '/sibling', 'prefs'], - bindings: ['/->DataSettings', '/long->DataSettings', '/quoted->DataSettings', '/sibling->DataSettings', 'prefs->DataSettings'], + paths: ['/', '/data/prefs', '/long', '/quoted', '/sibling'], + bindings: ['/->DataSettings', '/data/prefs->DataSettings', '/long->DataSettings', '/quoted->DataSettings', '/sibling->DataSettings'], }); }); diff --git a/__tests__/resolution.test.ts b/__tests__/resolution.test.ts index 0a4e3c1446..e74d3660ef 100644 --- a/__tests__/resolution.test.ts +++ b/__tests__/resolution.test.ts @@ -818,12 +818,9 @@ impl Source for BufSource { // The two ways this could overreach. `self.missing()` names nothing on // the owner, so it must not fall back to some other type's `missing`. // - // The receiver-less half is pinned as it BEHAVES, not as it should: a - // bare `reset()` is a free-function call, and it already resolved to - // `Target::reset` before this change — the mirror image of #1861, where - // a call with no receiver is given one. That is a separate defect in the - // bare-name strategy, measured on this branch's parent; the cell is here - // so this change is pinned to not make it worse. + // And a bare `reset()` is a free-function call: Rust reaches a method + // only through `self.` or `Type::`, so it is the free `reset`, never + // `Target::reset` (it used to be — the mirror image of #1861). writeRustCrate(tempDir, { 'lib.rs': 'pub fn reset() {}\n\n' + @@ -834,8 +831,7 @@ impl Source for BufSource { }); cg = await CodeGraph.init(tempDir, { index: true }); - // Unchanged by this commit — see the note above. - expect(callsFrom('Target::free').map((c) => c.target)).toEqual(['Target::reset']); + expect(callsFrom('Target::free').map((c) => c.target)).toEqual(['reset']); // Nothing on the owner is named `missing`, so no edge at all. expect(callsFrom('Target::absent')).toEqual([]); }); diff --git a/__tests__/returned-binding-factory.test.ts b/__tests__/returned-binding-factory.test.ts index b93cd0af34..0793108d48 100644 --- a/__tests__/returned-binding-factory.test.ts +++ b/__tests__/returned-binding-factory.test.ts @@ -62,6 +62,11 @@ describe('factory returning a module binding', () => { expect(targets).toContain('I18nClass::baseText'); }); + it('keeps return line positions after Unicode string contents', async () => { + const targets = await callTargets({ 'src/i18n.ts': i18n(' const label = "中文";\n return i18n;'), 'src/use.ts': consumer }, 'label'); + expect(targets).toContain('I18nClass::baseText'); + }); + it('types the result by a constructed binding', async () => { const targets = await callTargets({ 'src/i18n.ts': i18n(' return plain;'), 'src/use.ts': consumer }, 'label'); expect(targets).toContain('I18nClass::baseText'); diff --git a/__tests__/rust-go-call-shape.test.ts b/__tests__/rust-go-call-shape.test.ts new file mode 100644 index 0000000000..217258c02d --- /dev/null +++ b/__tests__/rust-go-call-shape.test.ts @@ -0,0 +1,104 @@ +/** + * Rust and Go extractors keep one receiver level, so a chain's later links + * (`sym.filename().map(…)`, `c.Flags().String(…)`) arrive as bare names. Read + * at the call: a bare call never reaches a method (axum's routing `get(h)` + * went to a cookie jar's `get`, tokio's `drop(x)` to a `Drop` impl); a chained + * call with a standard-library name (`unwrap`, `map`, Go's `String`) needs a + * receiver named after the owner, while a project-specific one keeps its + * match (clap's `flag("n").short('n')` → `Arg::short`). + */ +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-rust-go-shape-')); + const files: Record = { + 'Cargo.toml': '[package]\nname = "app"\nversion = "0.1.0"\n', + 'src/guard.rs': `pub struct MutexGuard; +impl MutexGuard { + pub fn map(self) -> Self { self } +} +pub struct CookieJar; +impl CookieJar { + pub fn get(&self) -> Option { None } +} +pub struct Arg; +impl Arg { + pub fn short(self, c: char) -> Self { self } +} +pub fn flag(name: &str) -> Arg { Arg } +pub struct Worker; +impl Worker { + pub fn run_nested(&self) { + fn nested_value() -> u8 { 1 } + let _ = nested_value(); + } +} +`, + 'src/main.rs': `mod guard; +use guard::flag; +fn get(handler: u8) -> u8 { handler } +fn run(sym: Option<&str>) { + let _ = sym.map(|s| s.len()); + let _ = get(1); + let _ = flag("n").short('n'); +} +fn main() { run(None); } +`, + 'go.mod': 'module example.com/app\n\ngo 1.21\n', + 'flags.go': `package app + +type customString struct{} + +func (c customString) String() string { return "" } + +type FlagSet struct{} + +func Flags() *FlagSet { return nil } +`, + 'cmd.go': `package app + +func run() { + Flags().String("name", "", "") +} +`, + }; + 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 callsIn = (file: string, fn: string): string[] => { + const node = cg.getNodesInFile(file).find((n) => n.name === fn)!; + return cg.getOutgoingEdges(node.id).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.qualifiedName); +}; + +describe('Rust and Go call shapes', () => { + it('Rust: bare never a method, std chain names need their receiver, project chain names keep theirs', () => { + const targets = callsIn('src/main.rs', 'run'); + expect(targets.some((t) => t.endsWith('MutexGuard::map'))).toBe(false); + expect(targets.some((t) => t.endsWith('CookieJar::get'))).toBe(false); + expect(targets.some((t) => t.endsWith('Arg::short'))).toBe(true); + }); + + it('Rust: keeps a nested function called without a receiver', () => { + expect(callsIn('src/guard.rs', 'run_nested').some((t) => t.endsWith('nested_value'))).toBe(true); + }); + + it('Go: a std-named chained call is not a project type’s method', () => { + expect(callsIn('cmd.go', 'run').some((t) => t.endsWith('customString::String'))).toBe(false); + }); +}); diff --git a/__tests__/scala-bare-call-scope.test.ts b/__tests__/scala-bare-call-scope.test.ts new file mode 100644 index 0000000000..9f24022c2d --- /dev/null +++ b/__tests__/scala-bare-call-scope.test.ts @@ -0,0 +1,90 @@ +/** + * A bare Scala name reached by name alone must be in reach: + * - a parameter or local binds it: `f(true)` with `f: A => B` is not a case + * class's field `f` (cats: 438 times); + * - a later link of a chain (`fa.iterator.map(f)`) reaches a member of what + * the receiver is named after (`basicRequest.send` → Request), never a + * package object's function; + * - otherwise a member of the types around it — an anonymous subclass's base + * included (`new OptionParser[C]("x") { head("x") }`) — of their supertypes, + * or of an imported object. + */ +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-scala-scope-')); + const files: Record = { + 'src/main/scala/app/FreeT.scala': `package app + +final case class FlatMapped[A](f: A => A) +`, + 'src/main/scala/app/LazyOps.scala': `package app + +class LazyOps[A](xs: List[A]) { + def map[B](g: A => B): List[B] = xs.map(g) +} +`, + 'src/main/scala/app/OptionParser.scala': `package app + +abstract class OptionParser[C](name: String) { + def head(xs: String*): Unit = () +} +`, + 'src/main/scala/app/Base.scala': `package app + +trait Base { + def helper(): Int = 1 +} +`, + 'src/main/scala/app/Request.scala': `package app + +class Request { + def send(backend: String): String = backend +} +`, + 'src/main/scala/app/Use.scala': `package app + +object Use extends Base { + def tabulate[A](f: Boolean => A): (A, A) = (f(true), f(false)) + def twice(fa: List[Int]): List[Int] = fa.iterator.map(_ + 1).toList + def run(basicRequest: Request): String = basicRequest.send("b") + def local(): Int = helper() + val parser = new OptionParser[Int]("scopt") { + head("scopt", "3.x") + } +} +`, + }; + 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 }); +}); + +describe('bare Scala calls', () => { + it('stay with locals, the receiver’s type, the class hierarchy and anonymous bases', () => { + const ids = cg.getNodesInFile('src/main/scala/app/Use.scala').map((n) => n.id); + const targets = cg + .getOutgoingEdgesFrom(ids) + .filter((e) => e.kind === 'calls') + .map((e) => cg.getNode(e.target)!.qualifiedName); + expect(targets.some((t) => t.endsWith('FlatMapped::f'))).toBe(false); + expect(targets.some((t) => t.endsWith('LazyOps::map'))).toBe(false); + expect(targets.some((t) => t.endsWith('Request::send'))).toBe(true); + expect(targets.some((t) => t.endsWith('Base::helper'))).toBe(true); + expect(targets.some((t) => t.endsWith('OptionParser::head'))).toBe(true); + }); +}); diff --git a/__tests__/swift-call-shape.test.ts b/__tests__/swift-call-shape.test.ts new file mode 100644 index 0000000000..0f999609b7 --- /dev/null +++ b/__tests__/swift-call-shape.test.ts @@ -0,0 +1,133 @@ +/** + * Swift's extractor keeps one receiver level, so `super.reset()`, + * `axis.entries.removeAll()` and `min(a, b)` reach the resolver as bare + * names. How the call is written decides what it can mean: + * + * - bare or through `self.`: a member of a type around the call or of what it + * inherits or conforms to (UIKit ancestry and the standard collection + * protocols included), the nearest first — `super.`: the latter only. + * Charts' `min(a, b)` went to a range type's `min` field, `max(a, b)` to a + * `Sequence` extension, `super.init(…)` to an Objective-C demo's `init`; + * - down a longer chain, a name the standard types all carry is theirs unless + * the receiver names the owner or the call gives a label only the project's + * method takes (Kingfisher's `data.kf.contains(jpeg:)`); + * - a global `let` in a playground is nobody else's call target. + */ +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-swift-shape-')); + const files: Record = { + 'Sources/Charts/Utils/Sequence+KeyPath.swift': `extension Sequence { + func max(by keyPath: KeyPath) -> Element? { + return nil + } +} +`, + 'Sources/Charts/Renderers/Renderer.swift': `open class Renderer { + public struct XBounds { + public var min: Int = 0 + } +} +`, + 'Sources/Charts/Data/ChartData.swift': `open class ChartData { + open func removeAll() { } +} +`, + 'Sources/Charts/Formatters/Formatter.swift': `open class Formatter { + open func reset() { } +} +`, + 'Sources/Charts/Charts/ChartViewBase.swift': `open class ChartViewBase: NSObject { + open func highlightValue(_ h: Int) { } + open func reset() { } +} +`, + 'Sources/Charts/Charts/BarChartView.swift': `open class BarChartView: ChartViewBase { + open override func highlightValue(_ h: Int) { } +} +`, + 'Sources/Charts/Charts/BarLineChartViewBase.swift': `open class BarLineChartViewBase: ChartViewBase { + open func tapped(axis: Axis) { + highlightValue(1) + let widest = max(1, 2) + let narrowest = min(3, 4) + super.reset() + axis.entries.removeAll() + } +} +`, + 'Sources/Charts/Wrapper/KingfisherWrapper.swift': `public struct KingfisherWrapper { + public let base: Base +} + +extension KingfisherWrapper where Base == Data { + func contains(jpeg marker: Int) -> Bool { + return false + } +} +`, + 'Sources/Charts/Wrapper/Decoder.swift': `final class Decoder { + func scan(data: Data) -> Bool { + return data.kf.contains(jpeg: 0xC2) + } +} +`, + 'Demo/Extensions/UIViewController+Bar.swift': `extension UIViewController { + func setupOperationBar() { } +} +`, + 'Demo/Controllers/ListController.swift': `class ListController: UICollectionViewController { + override func viewDidLoad() { + setupOperationBar() + } +} +`, + 'Demo/Chart.playground/Contents.swift': `let min = 20.0 +`, + }; + 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 }); +}); + +/** `Owner::member` of every call edge out of a file. */ +function callsFrom(file: string): string[] { + const ids = cg.getNodesInFile(file).map((n) => n.id); + return cg + .getOutgoingEdgesFrom(ids) + .filter((e) => e.kind === 'calls') + .map((e) => cg.getNode(e.target)!.qualifiedName) + .sort(); +} + +describe('Swift calls reached by name alone', () => { + it('reach the nearest inherited member, and nothing outside the hierarchy', () => { + expect(callsFrom('Sources/Charts/Charts/BarLineChartViewBase.swift')).toEqual([ + 'ChartViewBase::highlightValue', + 'ChartViewBase::reset', + ]); + }); + + it('keep a standard-named method the call labels as the project’s', () => { + expect(callsFrom('Sources/Charts/Wrapper/Decoder.swift')).toEqual(['KingfisherWrapper::contains']); + }); + + it('reach an extension of a UIKit ancestor', () => { + expect(callsFrom('Demo/Controllers/ListController.swift')).toEqual(['UIViewController::setupOperationBar']); + }); +}); diff --git a/__tests__/typed-receiver-scope.test.ts b/__tests__/typed-receiver-scope.test.ts index c67ade507d..b83e94f1dc 100644 --- a/__tests__/typed-receiver-scope.test.ts +++ b/__tests__/typed-receiver-scope.test.ts @@ -218,7 +218,7 @@ class App { } `; for (const sub of ['Sub', 'Sub2']) { - const got = await edges({ 'App.cs': app, 'Decoy.cs': CS_DECOY(sub) }); + const got = await edges({ 'App.cs': app, 'Decoy.cs': CS_DECOY(sub) }, ['calls']); expect(got.filter((e) => e.includes('BaseMethod'))).toEqual([]); } const ext = await edges({ @@ -228,7 +228,7 @@ class App { public static int BaseMethod(this Sub s) { return 3; } } `, - }); + }, ['calls']); expect(ext.filter((e) => e.includes('BaseMethod'))).toEqual(['App::Run -calls-> SubExtensions::BaseMethod@Ext.cs']); }); @@ -474,7 +474,7 @@ class App { public static int BaseMethod(this Unrelated u) { return 3; } } `, - }); + }, ['calls']); expect(got.filter((e) => e.includes('BaseMethod'))).toEqual([]); }); }); diff --git a/__tests__/vapor-closure-routes.test.ts b/__tests__/vapor-closure-routes.test.ts new file mode 100644 index 0000000000..ba875649ee --- /dev/null +++ b/__tests__/vapor-closure-routes.test.ts @@ -0,0 +1,70 @@ +/** + * A Vapor route whose handler is a trailing closure — `app.get("hello") { req + * in … }`, `app.webSocket("chat") { req, ws in … }` — and a `routes.on(.POST, + * "x", use:)` registration are routes too; only `use:` handlers were read. + * A `{` that opens an `if` (`if let v = req.parameters.get("x") {`) or an HTTP + * client's `req.client.get("https://…") { … }` is not one. + */ +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-vapor-closure-')); + const files: Record = { + 'Package.swift': `// swift-tools-version:5.9 +import PackageDescription +let package = Package(name: "App", dependencies: [.package(url: "https://github.com/vapor/vapor.git", from: "4.0.0")]) +`, + 'Sources/App/routes.swift': `import Vapor + +func routes(_ app: Application) throws { + app.get("hello", ":name") { req async throws -> String in + guard let name = req.parameters.get("name") else { throw Abort(.badRequest) } + return "Hello, \\(name)" + } + app.webSocket("chat") { req, client in + client.send("hi") + } + let todos = app.grouped("todos") + todos.on(.POST, "import", body: .collect(maxSize: "1mb"), use: importTodos) +} + +func importTodos(req: Request) async throws -> HTTPStatus { .ok } + +func lookup(req: Request) async throws -> String { + if let owner = req.parameters.get("owner") { + return owner + } + let res = try await req.client.get("https://example.com") { out in + try out.query.encode(["q": "x"]) + } + return "\\(res.status)" +} +`, + }; + 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 }); +}); + +describe('Vapor closure and on(…) routes', () => { + it('are routes; an if-let body and an HTTP client call are not', () => { + const names = cg.getNodesByKind('route').map((r) => r.name).sort(); + expect(names).toEqual(['GET /hello/:name', 'POST /todos/import', 'WS /chat']); + const post = cg.getNodesByKind('route').find((r) => r.name === 'POST /todos/import')!; + expect(cg.getOutgoingEdges(post.id).map((e) => cg.getNode(e.target)!.name)).toContain('importTodos'); + }); +}); diff --git a/codegraph-kernel/src/csharp/mod.rs b/codegraph-kernel/src/csharp/mod.rs index 94a249f154..4fd46e2f8a 100644 --- a/codegraph-kernel/src/csharp/mod.rs +++ b/codegraph-kernel/src/csharp/mod.rs @@ -227,7 +227,7 @@ impl<'t> Walker<'t> { // Java-style `modifiers` wrapper (probed). /// getVisibility: FIRST `modifier` child whose text is one of the four - /// levels wins; none → private (the C# default). + /// levels wins; otherwise use the containing declaration's default. fn visibility_of(&self, node: Node) -> u8 { for i in 0..node.child_count() { let Some(child) = node.child(i) else { continue }; @@ -241,7 +241,14 @@ impl<'t> Walker<'t> { } } } - 2 // C# defaults to private + let parent = node.parent().and_then(|p| { + if p.kind() == "declaration_list" { p.parent() } else { Some(p) } + }); + match parent.map(|p| p.kind()) { + Some("interface_declaration") => 1, + None | Some("compilation_unit" | "namespace_declaration" | "file_scoped_namespace_declaration") => 4, + _ => 2, + } } fn is_static(&self, node: Node) -> bool { @@ -799,4 +806,3 @@ fn find_anonymous_class_body(node: Node) -> Option { None } - diff --git a/codegraph-kernel/src/kotlin/calls.rs b/codegraph-kernel/src/kotlin/calls.rs index 6a60d3de8d..c0ceb0195e 100644 --- a/codegraph-kernel/src/kotlin/calls.rs +++ b/codegraph-kernel/src/kotlin/calls.rs @@ -14,6 +14,7 @@ impl<'t> Walker<'t> { .or_else(|| node.named_child(0)); let Some(func) = func else { return }; let mut callee_name = String::new(); + let mut call_at = node; if func.kind() == "navigation_expression" { let property = func @@ -30,6 +31,7 @@ impl<'t> Walker<'t> { }); if let Some(property) = property { let method_name = self.text(property); + call_at = property; let receiver = func .child_by_field_name("object") .or_else(|| func.child_by_field_name("operand")) @@ -84,7 +86,7 @@ impl<'t> Walker<'t> { if let Some(c) = util::paren_conversion().captures(&callee_name) { callee_name = c[1].to_string(); } - self.push_ref_at(caller, &callee_name, crate::buffers::EDGE_CALLS, node); + self.push_ref_at(caller, &callee_name, crate::buffers::EDGE_CALLS, if callee_name.contains('.') { node } else { call_at }); } } diff --git a/codegraph-kernel/src/resolve/affix.rs b/codegraph-kernel/src/resolve/affix.rs index 3d1ef162ce..e8b71f09b0 100644 --- a/codegraph-kernel/src/resolve/affix.rs +++ b/codegraph-kernel/src/resolve/affix.rs @@ -245,7 +245,7 @@ pub(super) static RECEIVER_TYPE_PATTERNS: LazyLock]*(?:<[^;=(){}<>]*>[^;=(){}<>]*)*>)?(?:\s*\[[\s,]*\])*\s+", r"\s*[=;,:)]", false, true, true, 0), ], ); m.insert( @@ -321,7 +321,7 @@ pub(super) static RECEIVER_TYPE_PATTERNS: LazyLock]*(?:<[^;=(){}<>]*>[^;=(){}<>]*)*>)?(?:\s*\[[\s,]*\])*\??\s+", r"\s*(?:[=;,)]|in\b)", false, true, true, 0), ], ); m.insert( diff --git a/codegraph-kernel/src/resolve/bound.rs b/codegraph-kernel/src/resolve/bound.rs index b7b13664bc..1cb09e7039 100644 --- a/codegraph-kernel/src/resolve/bound.rs +++ b/codegraph-kernel/src/resolve/bound.rs @@ -18,6 +18,24 @@ impl KernelResolver { if depth > 4 { return Ok(None); } + if matches!(r.language.as_str(), "java" | "kotlin" | "csharp") && ty.contains('.') { + let name = ty.rsplit('.').next().unwrap_or(ty); + let owners: Vec<_> = self.nodes_by_name(name)?.iter().filter(|n| { + same_language_family(&n.language, &r.language) && TYPE_OWNER_KINDS.contains(&n.kind.as_str()) + && n.qualified_name.replace("::", ".") == ty + }).cloned().collect(); + if let [only] = owners.as_slice() { return Ok(Some(only.clone())); } + let (root, tail) = ty.split_once('.').unwrap(); + if let Some(owner) = self.resolve_bound_type(root, r, depth + 1)? { + let qualified = format!("{}.{}", owner.qualified_name.replace("::", "."), tail); + let nested: Vec<_> = self.nodes_by_name(name)?.iter().filter(|n| { + same_language_family(&n.language, &r.language) && TYPE_OWNER_KINDS.contains(&n.kind.as_str()) + && n.qualified_name.replace("::", ".") == qualified + }).cloned().collect(); + return Ok(match nested.as_slice() { [only] => Some(only.clone()), _ => None }); + } + return Ok(None); + } if r.language == "java" { if let Some((declaration, site)) = self.java_type_parameter(ty, r)? { // An unbounded or self declaration shadows any outer bound. diff --git a/codegraph-kernel/src/resolve/call_shape.rs b/codegraph-kernel/src/resolve/call_shape.rs new file mode 100644 index 0000000000..961cf7dfd2 --- /dev/null +++ b/codegraph-kernel/src/resolve/call_shape.rs @@ -0,0 +1,486 @@ +//! Call-site forms and language scope for names whose extractor drops the receiver. + +use super::*; +use super::method_call::shares_receiver_word; + +const SWIFT_STD_LABELS: &[&str] = &[ + "contentsOf", "where", "by", "at", "keepingCapacity", "separator", + "forKey", "of", "into", "in", "options", "maxSplits", + "omittingEmptySubsequences", "with", "after", "before", "upTo", "through", + "offsetBy", "default", +]; + +const SWIFT_STD_METHODS: &[&str] = &[ + "append", "insert", "remove", "removeAll", "removeFirst", "removeLast", + "removeValue", "contains", "map", "compactMap", "flatMap", "filter", + "reduce", "forEach", "sorted", "sort", "first", "last", + "min", "max", "firstIndex", "lastIndex", "index", "enumerated", + "reversed", "joined", "split", "prefix", "suffix", "dropFirst", + "dropLast", "allSatisfy", "randomElement", "shuffled", "popLast", "replaceSubrange", + "replacingOccurrences", "components", "trimmingCharacters", "hasPrefix", "hasSuffix", "lowercased", + "uppercased", "appending", "updateValue", "merge", "merging", "union", + "intersection", "subtracting", "formUnion", "isEqual", "addSubview", "removeFromSuperview", + "setNeedsDisplay", "setNeedsLayout", "layoutIfNeeded", "addGestureRecognizer", "addTarget", "addObserver", + "removeObserver", "eraseToAnyPublisher", +]; + +const RUST_STD_METHODS: &[&str] = &[ + "unwrap", "unwrap_or", "unwrap_or_else", "unwrap_or_default", "unwrap_err", "unwrap_unchecked", + "expect", "expect_err", "ok", "err", "map", "map_err", + "map_or", "map_or_else", "and_then", "or_else", "ok_or", "ok_or_else", + "is_some", "is_none", "is_ok", "is_err", "is_some_and", "as_ref", + "as_mut", "as_deref", "clone", "cloned", "copied", "iter", + "iter_mut", "into_iter", "collect", "enumerate", "zip", "rev", + "chain", "skip", "step_by", "peekable", "flat_map", "filter_map", + "flatten", "any", "all", "len", "is_empty", "push", + "push_str", "pop", "extend", "drain", "clear", "retain", + "truncate", "reserve", "with_capacity", "capacity", "sort", "sort_by", + "sort_by_key", "dedup", "split_off", "contains_key", "to_string", "to_owned", + "to_vec", "as_str", "as_bytes", "as_slice", "as_ptr", "into", + "try_into", "borrow", "borrow_mut", "deref", "deref_mut", "chars", + "bytes", "lines", "starts_with", "ends_with", "trim", "to_lowercase", + "to_uppercase", "windows", "chunks", "then", "then_some", "eq", + "cmp", "partial_cmp", "read_to_end", "read_to_string", "fetch_add", "fetch_sub", +]; + +const GO_STD_METHODS: &[&str] = &[ + "String", "Error", "Unwrap", "Is", "As", "Header", + "WriteHeader", "WriteString", "Lock", "Unlock", "RLock", "RUnlock", + "Err", "Deadline", "Int", "Bool", "Float64", "Int64", + "Uint64", "Bytes", "Len", "Cap", "Seconds", "Unix", + "Before", "After", "Equal", "IsNil", "IsValid", "Elem", + "NumField", "Interface", "Kind", +]; + +const KOTLIN_STD_METHODS: &[&str] = &[ + "apply", "also", "let", "run", "takeIf", "takeUnless", + "toString", "equals", "hashCode", "map", "mapNotNull", "mapIndexed", + "filter", "filterNot", "filterIsInstance", "forEach", "forEachIndexed", "first", + "firstOrNull", "last", "lastOrNull", "single", "singleOrNull", "isEmpty", + "isNotEmpty", "isNullOrEmpty", "isNullOrBlank", "isBlank", "isNotBlank", "orEmpty", + "joinToString", "toList", "toMutableList", "toSet", "toMutableSet", "toMap", + "toTypedArray", "any", "all", "none", "count", "sumOf", + "maxOf", "minOf", "maxOrNull", "minOrNull", "sortedBy", "sortedByDescending", + "sorted", "sortedWith", "reversed", "drop", "dropLast", "take", + "takeLast", "zip", "flatMap", "flatten", "distinct", "groupBy", + "associate", "associateBy", "associateWith", "partition", "contains", "containsKey", + "getOrElse", "getOrNull", "getOrPut", "getOrDefault", "trim", "trimEnd", + "trimStart", "split", "substring", "startsWith", "endsWith", "replace", + "lowercase", "uppercase", "toInt", "toLong", "toDouble", "toFloat", + "toIntOrNull", "toLongOrNull", "encodeToByteArray", "decodeToString", "copyOf", "copyOfRange", + "indexOf", "lastIndexOf", "withIndex", "asSequence", "asList", "ifEmpty", + "ifBlank", "padStart", "padEnd", "repeat", "lines", "toCharArray", + "coerceAtLeast", "coerceAtMost", "coerceIn", +]; + +const CSHARP_STD_METHODS: &[&str] = &[ + "ToString", "Equals", "GetHashCode", "GetType", "CompareTo", "Add", + "AddRange", "Remove", "RemoveAt", "RemoveAll", "Contains", "ContainsKey", + "ContainsValue", "Clear", "Insert", "IndexOf", "CopyTo", "ToArray", + "ToList", "ToDictionary", "GetEnumerator", "MoveNext", "Reset", "TryGetValue", + "GetValueOrDefault", "TryAdd", "Write", "WriteLine", "WriteAsync", "WriteLineAsync", + "Read", "ReadAsync", "ReadLine", "ReadToEnd", "Flush", "FlushAsync", + "Close", "Dispose", "DisposeAsync", "Parse", "TryParse", "Format", + "Join", "Split", "Replace", "Substring", "Trim", "TrimStart", + "TrimEnd", "StartsWith", "EndsWith", "ToUpper", "ToLower", "ToUpperInvariant", + "ToLowerInvariant", "Select", "Where", "First", "FirstOrDefault", "Single", + "SingleOrDefault", "Last", "LastOrDefault", "Any", "All", "Count", + "Sum", "Max", "Min", "OrderBy", "OrderByDescending", "GroupBy", + "Skip", "Take", "Distinct", "Concat", "Cast", "OfType", + "Aggregate", "Invoke", "DynamicInvoke", "GetMethod", "GetProperty", "GetField", + "GetConstructor", "GetCustomAttributes", "GetGenericArguments", "MakeGenericType", "IsAssignableFrom", "ConfigureAwait", + "Wait", "ContinueWith", "Append", "AppendLine", "Peek", "Push", + "Pop", "Enqueue", "Dequeue", "HasFlag", "Find", "FindAll", + "ForEach", "Sort", "Reverse", "Clone", "Seek", "SetLength", +]; + +#[derive(Clone, PartialEq)] +enum Shape { Bare, Path, SelfCall, SuperCall, Chain } +#[derive(PartialEq)] +pub(super) enum KotlinChainEvidence { Bound, Heuristic } +struct CallSite { shape: Shape, receiver: String, label: String, subscript: bool } + +/// Drop balanced call, subscript and literal arguments while walking back to the receiver. +pub(super) fn receiver_name(text: &str) -> String { + let text = text.trim_end().trim_end_matches('.').trim_end(); + let chars: Vec = text.chars().collect(); + let mut i = chars.len(); + let mut out = String::new(); + while i > 0 { + let ch = chars[i - 1]; + if matches!(ch, ')' | ']' | '}') { + let open = match ch { ')' => '(', ']' => '[', _ => '{' }; + let end = i; + let mut depth = 1; + i -= 1; + while i > 0 && depth > 0 { + i -= 1; + if chars[i] == ch { depth += 1; } + else if chars[i] == open { depth -= 1; } + } + if depth != 0 { return String::new(); } + if ch == ')' && out.is_empty() { + let literal: String = chars[i..end].iter().collect(); + if let Some(m) = re!(r"^\(\s*&?\s*([A-Za-z_][\w.]*)\s*\{").captures(&literal) { + return m[1].to_string(); + } + } + } else if ch.is_ascii_alphanumeric() || "_$.:!".contains(ch) { + if ch != '!' { out.insert(0, ch); } + i -= 1; + } else { break; } + } + out.trim_matches(['.', ':']).to_string() +} + +pub(super) fn receiver_link(receiver: &str) -> &str { + let mut links = receiver.rsplit('.'); + let last = links.next().unwrap_or(""); + if re!(r"^[A-Z][A-Z0-9_]+$").is_match(last) { links.next().unwrap_or(last) } else { last } +} + +pub(super) fn is_std_method(language: &str, name: &str) -> bool { + match language { + "rust" => RUST_STD_METHODS, "go" => GO_STD_METHODS, + "kotlin" => KOTLIN_STD_METHODS, "csharp" => CSHARP_STD_METHODS, + _ => &[], + }.contains(&name) +} + +pub(super) fn owner(n: &KNode) -> Option<&str> { + n.qualified_name.rsplit_once("::").map(|(path, _)| path.rsplit([':', '.']).next().unwrap_or("")) +} +pub(super) fn type_kind(kind: &str) -> bool { + matches!(kind, "class" | "struct" | "enum" | "interface" | "trait" | "protocol" | "module" | "namespace") +} +fn swift_member(n: &KNode) -> bool { + matches!(n.kind.as_str(), "method" | "property" | "field" | "enum_member") + || (owner(n).is_some() && matches!(n.kind.as_str(), "constant" | "variable")) +} + +/// Remove nested generic and constructor parameters from a declaration head. +pub(super) fn flat_head(text: &str, scala: bool) -> String { + let mut depth = 0usize; + let mut flat = String::new(); + for ch in text.chars() { + if ch == '(' || ch == '<' || (scala && ch == '[') { depth += 1; } + else if ch == ')' || ch == '>' || (scala && ch == ']') { depth = depth.saturating_sub(1); } + else if depth == 0 { + if ch == '{' || (scala && ch == '=') { break; } + flat.push(ch); + } + } + flat +} + +impl KernelResolver { + fn call_site(&mut self, r: &ResolveRefIn) -> Option { + let lines = self.read_file(&r.file_path)?; + let line = lines.get((r.line - 1).max(0) as usize)?; + let name = &r.reference_name; + let at = super::lang_scope::name_start_at_column(line, name, r.column.max(0) as usize) + .filter(|&at| at == 0 || !line[..at].ends_with(|c: char| c.is_ascii_alphanumeric() || c == '$' || c == '_')) + .or_else(|| { + Self::cached_regex(&format!(r"(?:^|[^\w$])({})\s*(?:[(<{{\[]|!|::<)", regex::escape(name))) + .ok()?.captures_iter(line).filter_map(|m| m.get(1)) + .find(|m| r.language != "kotlin" || m.start() >= js_unit_to_byte(line, r.column.max(0) as usize)).map(|m| m.start()) + })?; + let before = &line[..at]; + let after = &line[at + name.len()..]; + let shape = if before.trim_end().ends_with("::") { Shape::Path } + else if !before.trim_end().ends_with('.') { Shape::Bare } + else if let Some(m) = re!(r"(?:^|[^\w$.)\]])(self|Self|super)\s*[?!]?\s*\.\s*$").captures(before) { + if &m[1] == "super" { Shape::SuperCall } else { Shape::SelfCall } + } else { Shape::Chain }; + let before = re!(r"[?!]\s*\.").replace_all(before, "."); + Some(CallSite { + shape, receiver: receiver_name(&before), + label: re!(r"^\s*\(\s*([A-Za-z_]\w*)\s*:[^:]").captures(after).map(|m| m[1].to_string()).unwrap_or_default(), + subscript: after.trim_start().starts_with('['), + }) + } + + /// A declared callee result admits a chain; unknown receivers remain heuristic. + pub(super) fn kotlin_chain_evidence(&mut self, n: &KNode, r: &ResolveRefIn) -> Res> { + if r.language != "kotlin" || r.reference_kind != "calls" || r.reference_name != n.name || !is_std_method("kotlin", &n.name) { return Ok(None); } + let Some(lines) = self.read_file(&r.file_path) else { return Ok(None) }; + let Some(line) = lines.get((r.line - 1).max(0) as usize) else { return Ok(None) }; + let code = super::awaited::blank_string_contents(&super::awaited::strip_ts_comments(line)); + let pat = Self::cached_regex(&format!(r"(?:^|[^\w])({})\s*\(", regex::escape(&n.name)))?; + let column = js_unit_to_byte(&code, r.column.max(0) as usize); + let Some(at) = pat.captures_iter(&code).filter_map(|m| m.get(1)).find(|m| m.start() >= column).map(|m| m.start()) else { return Ok(None) }; + let before = code[..at].trim_end().trim_end_matches('.').trim_end(); + let receiver = self.kotlin_chain_receiver_call(r) + .map(|call| super::awaited::blank_string_contents(&super::awaited::strip_ts_comments(&call))); + let before = receiver.as_deref().map(str::trim_end).unwrap_or(before); + if !before.ends_with(')') { return Ok(None); } + let mut depth = 0usize; + let mut open = None; + for (i, ch) in before.char_indices().rev() { + if ch == ')' { depth += 1; } + else if ch == '(' { depth -= 1; if depth == 0 { open = Some(i); break; } } + } + let Some(open) = open else { return Ok(None) }; + let Some(call) = re!(r"(?:([A-Za-z_]\w*)\s*\.)?([A-Za-z_]\w*)(?:<[^<>]*(?:<[^<>]*>[^<>]*)*>)?\s*$").captures(&before[..open]) else { return Ok(None) }; + if call.get(1).is_none() && before[..open][..call.get(0).unwrap().start()].trim_end().ends_with('.') { return Ok(None); } + let name = &call[2]; + let receiver_type = match call.get(1) { + Some(receiver) => self.infer_local_receiver_type(receiver.as_str(), r, true)?, + None => None, + }; + let mut factory = match &receiver_type { + Some(ty) => self.match_bound_type_member(ty, name, r)?.map(|c| c.node), + None => None, + }; + if factory.is_none() { + let callee_ref = r.clone().naming(name, "calls"); + let imported = self.import_mappings(&r.file_path)?.iter().any(|m| m.local_name == name); + if imported { + factory = self.resolve_via_import(&callee_ref)?.map(|c| c.node); + if factory.is_none() { + // Kotlin extensions are methods qualified by the receiver, + // while their imports name the package and function. + let mappings = self.import_mappings(&r.file_path)?; + let mut extensions = Vec::new(); + for candidate in self.nodes_by_name(name)?.iter().filter(|c| c.language == "kotlin" && c.kind == "method") { + let pkg = self.kotlin_file_scope(&candidate.file_path).pkg.clone(); + let imported_name = format!("{pkg}.{}", candidate.name); + if !mappings.iter().any(|m| m.local_name == name && m.source == imported_name) { continue; } + if self.nodes_in_file(&candidate.file_path)?.iter().any(|c| { + type_kind(&c.kind) && c.kind != "namespace" && c.kind != "module" + && c.start_line <= candidate.start_line && c.end_line >= candidate.end_line + }) { continue; } + extensions.push(candidate.clone()); + } + if extensions.len() == 1 { factory = extensions.pop(); } + } + } + else if call.get(1).is_none() { + let bindings = self.bindings(&r.file_path)?; + factory = match innermost_binding(&bindings, name, Some(r.line)) { + Some(binding) => self.node_by_opt_id(binding.node_id.as_deref())?, + None => None, + }; + } + if let (Some(ty), Some(f)) = (&receiver_type, &factory) { + let mut declaration = r.clone(); declaration.file_path = f.file_path.clone(); declaration.line = f.start_line; + let extension_owner = owner(f).unwrap_or(""); + let expected = self.resolve_bound_type(extension_owner, &declaration, 0)?; + let actual = self.resolve_bound_type(ty, r, 0)?; + if !expected.zip(actual).is_some_and(|(a, b)| a.id == b.id) { return Ok(None); } + } + } + let Some(factory) = factory else { return Ok(None) }; + let Some(raw) = &factory.return_type else { return Ok(None) }; + let ty = raw.split('<').next().unwrap_or(raw).trim().trim_end_matches('?'); + if factory.type_parameters.as_ref().is_some_and(|ps| ps.iter().any(|p| p.split_whitespace().next() == Some(ty))) { return Ok(None); } + let mut declaration = r.clone(); declaration.file_path = factory.file_path.clone(); declaration.line = factory.start_line; declaration.from_node_id = factory.id.clone(); + if call.get(1).is_some() && receiver_type.is_none() { + // A declared conflicting member can shadow the imported extension. + // Unannotated competitors keep the result uncertain; the shared + // target gate caps this hypothesis below the trust line. + for candidate in self.nodes_by_name(&factory.name)?.iter().filter(|c| { + matches!(c.language.as_str(), "kotlin" | "java") && matches!(c.kind.as_str(), "method" | "function") + }) { + let Some(raw) = &candidate.return_type else { continue }; + let ty = raw.split('<').next().unwrap_or(raw).trim().trim_end_matches('?'); + let mut site = declaration.clone(); site.file_path = candidate.file_path.clone(); site.line = candidate.start_line; site.from_node_id = candidate.id.clone(); + if !self.match_bound_type_member(ty, &n.name, &site)?.is_some_and(|member| member.node.id == n.id) { return Ok(None); } + } + } + Ok(self.match_bound_type_member(ty, &n.name, &declaration)? + .filter(|c| c.node.id == n.id) + .map(|_| if receiver_type.is_some() { KotlinChainEvidence::Bound } else { KotlinChainEvidence::Heuristic })) + } + + /// Scope predicates only eliminate candidates, never select one. + pub(super) fn call_shape_target(&mut self, n: &KNode, r: &ResolveRefIn) -> Res { + let Some(site) = self.call_site(r) else { return Ok(true) }; + match r.language.as_str() { + "rust" | "go" => Ok(match site.shape { + Shape::Path => true, + Shape::Bare => n.kind != "method" || (r.language == "rust" && n.file_path == r.file_path + && self.nodes_in_file(&r.file_path)?.iter().any(|f| f.id != n.id + && matches!(f.kind.as_str(), "function" | "method") + && f.start_line < n.start_line && f.end_line >= n.end_line + && f.start_line <= r.line && f.end_line >= r.line)), + _ if n.kind != "method" => n.kind != "function", + _ => !is_std_method(&n.language, &n.name) || matches!(site.receiver.as_str(), "self" | "Self") + || (!site.receiver.is_empty() && shares_receiver_word(&site.receiver, n)), + }), + "swift" => self.swift_call_target(n, r, &site), + "scala" => self.scala_call_target(n, r, &site), + "kotlin" if site.shape == Shape::Chain && is_std_method("kotlin", &r.reference_name) => { + if let Some(receiver) = self.kotlin_call_receiver_type(r)? { + return self.kotlin_receiver_accepts(n, &receiver, r); + } + Ok(!matches!(n.kind.as_str(), "method" | "function") || site.receiver == "this" + || (!site.receiver.is_empty() && shares_receiver_word(&site.receiver, n)) + || self.kotlin_chain_evidence(n, r)?.is_some()) + } + _ => Ok(true), + } + } + + fn swift_head(&mut self, n: &KNode) -> (Vec, bool) { + let Some(lines) = self.read_file(&n.file_path) else { return (vec![], false) }; + let from = (n.start_line - 1).max(0) as usize; + let to = (n.end_line.min(n.start_line + 20).max(0) as usize).min(lines.len()); + if from >= to { return (vec![], false); } + let source = super::awaited::strip_ts_comments(&lines[from..to].join("\n")); + let flat = flat_head(&source, false); + let Some(head) = re!(r"(?-u:\b)(class|struct|enum|protocol|extension|actor)\s+[\w.]+\s*([\s\S]*)$").captures(&flat) else { + return (vec![], false); + }; + let supers = head[2].trim_start().strip_prefix(':').unwrap_or("").split("where").next().unwrap_or("") + .split(',').filter_map(|part| re!(r"([A-Za-z_]\w*)\s*$").captures(part).map(|m| m[1].to_string())).collect(); + (supers, &head[1] == "extension") + } + + fn swift_decl(&mut self, name: &str) -> Res<(Vec, bool)> { + let mut supers: Vec = match name { + "RandomAccessCollection" => vec!["BidirectionalCollection"], + "BidirectionalCollection" | "MutableCollection" | "RangeReplaceableCollection" => vec!["Collection"], + "Collection" | "LazySequenceProtocol" => vec!["Sequence"], + "LazyCollectionProtocol" => vec!["Collection", "LazySequenceProtocol"], + "StringProtocol" => vec!["BidirectionalCollection"], + "Array" | "ArraySlice" | "ContiguousArray" => vec!["RandomAccessCollection", "MutableCollection", "RangeReplaceableCollection"], + "String" | "Substring" => vec!["StringProtocol", "RangeReplaceableCollection"], + "Dictionary" => vec!["Collection"], "Set" => vec!["Collection", "SetAlgebra"], + "Range" | "ClosedRange" => vec!["RandomAccessCollection"], _ => vec![], + }.into_iter().map(str::to_string).collect(); + if let Some((_, sup)) = super::member_scope::OBJC_SYSTEM_SUPERS.iter().find(|(ty, _)| *ty == name) { supers.push((*sup).to_string()); } + let mut project = false; + for decl in self.nodes_by_name(name)?.iter().filter(|n| n.language == "swift" && type_kind(&n.kind)) { + let (parents, extension) = self.swift_head(decl); + supers.extend(parents); + project |= !extension; + } + Ok((supers, project)) + } + + fn swift_hierarchy(&mut self, r: &ResolveRefIn) -> Res> { + let mut queue: VecDeque<(String, usize)> = self.nodes_in_file(&r.file_path)?.iter() + .filter(|n| n.language == "swift" && type_kind(&n.kind) && n.start_line <= r.line && n.end_line >= r.line) + .map(|n| (n.name.rsplit('.').next().unwrap_or("").to_string(), 0)).collect(); + let mut seen = HashMap::new(); + while seen.len() < 40 { + let Some((name, depth)) = queue.pop_front() else { break }; + if seen.contains_key(&name) { continue; } + seen.insert(name.clone(), depth); + queue.extend(self.swift_decl(&name)?.0.into_iter().map(|s| (s, depth + 1))); + } + Ok(seen) + } + + fn swift_call_target(&mut self, n: &KNode, r: &ResolveRefIn, site: &CallSite) -> Res { + if matches!(n.kind.as_str(), "constant" | "variable") && n.file_path != r.file_path && !site.subscript { return Ok(false); } + let dirs: Vec<&str> = n.file_path.split('/').collect(); + if let Some(index) = dirs[..dirs.len().saturating_sub(1)].iter().position(|d| d.ends_with("Tests") || d.ends_with(".playground")) { + if !r.file_path.starts_with(&format!("{}/", dirs[..=index].join("/"))) { return Ok(false); } + } + let Some(owner) = owner(n) else { + return Ok(n.kind != "function" || site.shape == Shape::Bare + || (site.shape == Shape::Chain && site.receiver.starts_with(|c: char| c.is_ascii_uppercase()))); + }; + if !swift_member(n) { return Ok(true); } + if site.shape == Shape::Chain { + if site.receiver.is_empty() || !SWIFT_STD_METHODS.contains(&n.name.as_str()) { return Ok(true); } + if shares_receiver_word(site.receiver.rsplit('.').next().unwrap_or(""), n) || !self.swift_decl(owner)?.1 { return Ok(true); } + if site.label.is_empty() || SWIFT_STD_LABELS.contains(&site.label.as_str()) { return Ok(false); } + let Some(lines) = self.read_file(&n.file_path) else { return Ok(false) }; + let from = (n.start_line - 1).max(0) as usize; + let to = (n.start_line + 3).max(0) as usize; + let head = lines[from.min(lines.len())..to.min(lines.len())].join(" "); + return Ok(Self::cached_regex(&format!(r"(?-u:\b)func\s+{}\s*(?:<[^>]*>)?\s*\(\s*{}\b", regex::escape(&n.name), regex::escape(&site.label)))?.is_match(&head)); + } + Ok(self.swift_hierarchy(r)?.get(owner).is_some_and(|&d| site.shape != Shape::SuperCall || d > 0)) + } + + pub(super) fn swift_scope_bound(&mut self, n: &KNode, r: &ResolveRefIn) -> Res { + let Some(site) = self.call_site(r) else { return Ok(false) }; + if site.shape == Shape::Chain { return Ok(false); } + Ok(owner(n).is_some_and(|o| !o.is_empty()) && self.swift_call_target(n, r, &site)?) + } + + pub(super) fn nearest_swift_members(&mut self, candidates: Vec>, r: &ResolveRefIn) -> Res>> { + if r.language != "swift" || r.reference_kind != "calls" { return Ok(candidates); } + let Some(site) = self.call_site(r) else { return Ok(candidates) }; + if site.shape == Shape::Chain { return Ok(candidates); } + let hierarchy = self.swift_hierarchy(r)?; + let depth = |n: &KNode| if swift_member(n) { owner(n).and_then(|o| hierarchy.get(o)).copied() } else { None }; + let Some(nearest) = candidates.iter().filter_map(|n| depth(n)).min() else { return Ok(candidates) }; + Ok(candidates.into_iter().filter(|n| depth(n).is_none_or(|d| d == nearest)).collect()) + } + + fn scala_call_target(&mut self, n: &KNode, r: &ResolveRefIn, site: &CallSite) -> Res { + let member = matches!(n.kind.as_str(), "method" | "field" | "property" | "variable" | "constant"); + if site.shape == Shape::Chain { + if n.file_path == r.file_path { return Ok(true); } + if member { return Ok(!site.receiver.is_empty() && shares_receiver_word(&site.receiver, n)); } + if n.kind != "function" { return Ok(true); } + return Ok(self.read_file(&n.file_path).is_some_and(|lines| { + let from = (n.start_line - 4).max(0) as usize; + let to = (n.start_line.max(0) as usize).min(lines.len()); + lines[from.min(to)..to].iter().any(|l| re!(r"^\s*extension\b").is_match(l)) + })); + } + let nodes = self.nodes_in_file(&r.file_path)?; + let lines = self.read_file(&r.file_path); + if let (Some(f), Some(lines)) = (nodes.iter().filter(|f| matches!(f.kind.as_str(), "method" | "function") + && f.start_line <= r.line && f.end_line >= r.line).min_by_key(|f| f.end_line - f.start_line), &lines) { + let from = (f.start_line - 1).max(0) as usize; + let to = (r.line.max(0) as usize).min(lines.len()); + let text = lines[from.min(to)..to].join("\n"); + let name = regex::escape(&r.reference_name); + let pattern = format!(r"(?:[(,\[]\s*(?:implicit\s+|using\s+)?{name}\s*:)|(?:\b(?:val|var|def|lazy\s+val)\s+{name}\b)|(?:^|[^\w$.]){name}\s*(?:=>|<-)|\(\s*{name}\s*(?:,[^)]*)?\)\s*=>"); + if Self::cached_regex(&pattern)?.is_match(&text) { + return Ok(n.file_path == r.file_path && n.start_line >= f.start_line && n.end_line <= f.end_line); + } + } + if !member || n.file_path == r.file_path { return Ok(true); } + let Some(owner) = owner(n) else { return Ok(true) }; + if let Some(lines) = &lines { + for m in re!(r"(?m)^\s*import\s+([\w.]+?)\.(?:([_*])|\{([^}]*)\}|([\w$]+))\s*$").captures_iter(lines.text()) { + let imported = m[1].rsplit('.').next().unwrap_or(""); + let wildcard = m.get(2).is_some() || m.get(3).is_some_and(|x| x.as_str().split(',').any(|v| matches!(v.trim(), "_" | "*"))); + if wildcard && (imported == owner || imported.starts_with(|c: char| c.is_ascii_lowercase())) { return Ok(true); } + if imported == owner && m.get(3).or_else(|| m.get(4)).is_some_and(|x| x.as_str().split(',').any(|v| v.trim().split("=>").next().unwrap_or("").trim() == r.reference_name)) { return Ok(true); } + } + } + let mut queue: VecDeque = nodes.iter().filter(|t| type_kind(&t.kind) && t.start_line <= r.line && t.end_line >= r.line).map(|t| t.name.clone()).collect(); + if queue.is_empty() { return Ok(true); } + // Open anonymous class bodies introduce their bases as implicit receivers. + if let Some(lines) = &lines { + let mut depth = 0usize; + let to = (r.line.max(0) as usize).min(lines.len()); + for text in lines[to.saturating_sub(400)..to].iter().rev() { + for (at, ch) in text.char_indices().rev() { + if ch == '}' { depth += 1; } + else if ch == '{' { + if depth > 0 { depth -= 1; continue; } + if let Some(head) = re!(r"\bnew\s+([\w.]+(?:\s*\[[^\]]*\])?(?:\s*\([^)]*\))?(?:\s+with\s+[\w.]+(?:\s*\[[^\]]*\])?)*)\s*$").captures(&text[..at]) { + let simple = re!(r"\[[^\]]*\]|\([^)]*\)").replace_all(&head[1], ""); + queue.extend(simple.split("with").map(|s| s.trim().rsplit('.').next().unwrap_or("").to_string())); + } + } + } + } + } + let mut seen = HashSet::new(); + while seen.len() < 60 { + let Some(name) = queue.pop_front() else { break }; + if !seen.insert(name.clone()) { continue; } + if name == owner { return Ok(true); } + for decl in self.nodes_by_name(&name)?.iter().filter(|d| d.language == "scala" && type_kind(&d.kind)) { + let Some(lines) = self.read_file(&decl.file_path) else { continue }; + let from = (decl.start_line - 1).max(0) as usize; + let to = ((decl.start_line + 12).max(0) as usize).min(lines.len()); + let flat = flat_head(&lines[from.min(to)..to].join(" "), true); + let Some((_, clause)) = flat.split_once("extends") else { continue }; + queue.extend(re!(r"[A-Za-z_][\w.]*").find_iter(clause).map(|m| m.as_str().rsplit('.').next().unwrap_or("").to_string()).filter(|s| !matches!(s.as_str(), "with" | "derives") && s != &name)); + } + } + Ok(false) + } +} diff --git a/codegraph-kernel/src/resolve/declared_member.rs b/codegraph-kernel/src/resolve/declared_member.rs new file mode 100644 index 0000000000..99f8e900a1 --- /dev/null +++ b/codegraph-kernel/src/resolve/declared_member.rs @@ -0,0 +1,148 @@ +//! Field and property receiver types, including inherited generic declarations. +use super::*; + +fn class_kind(kind: &str) -> bool { matches!(kind, "class" | "struct" | "interface" | "enum" | "record" | "trait") } +fn angle_arguments(text: &str) -> Vec { + let Some(open) = text.find('<') else { return vec![] }; + let mut depth = 0usize; + let mut out = Vec::new(); + let mut current = String::new(); + for ch in text[open + 1..].chars() { + if ch == '<' { depth += 1; } + else if ch == '>' { if depth == 0 { break; } depth -= 1; } + if ch == ',' && depth == 0 { out.push(current.trim().to_string()); current.clear(); } + else { current.push(ch); } + } + out.push(current.trim().to_string()); out +} +impl KernelResolver { + pub(super) fn explicit_this_receiver(&mut self, receiver: &str, method: &str, r: &ResolveRefIn) -> bool { + let Some(lines) = self.read_file(&r.file_path) else { return false }; + let Some(line) = lines.get((r.line - 1).max(0) as usize) else { return false }; + let call = format!("{receiver}.{method}"); + let mut hits = line.match_indices(&call).map(|(i, _)| &line[..i]).peekable(); + hits.peek().is_some() && hits.all(|before| before.ends_with("this.")) + } + fn declaration_head(&mut self, n: &KNode, count: i64) -> String { + let Some(lines) = self.read_file(&n.file_path) else { return String::new() }; + let from = ((n.start_line - 1).max(0) as usize).min(lines.len()); + let to = ((n.start_line + count).max(0) as usize).min(lines.len()); + super::awaited::strip_ts_comments(&lines[from..to.max(from)].join("\n")) + } + fn declared_member_type(&mut self, cls: &KNode, name: &str) -> Res> { + let key = (cls.id.clone(), name.to_string()); + if let Some(hit) = self.declared_member_memo.get(&key) { return Ok(hit.clone()); } + let escaped = regex::escape(name); + let pattern = if cls.language == "kotlin" { + format!(r"\b(?:val|var)\s+{escaped}\s*:\s*([A-Z][\w.]*)") + } else { + format!(r"(?:^|[\s(,])([A-Za-z_][\w.]*)\s*(?:<[^<>]*(?:<[^<>]*>[^<>]*)*>)?(?:\s*\[[\s,]*\])*\??\s+{escaped}\s*(?:[=;,)]|\{{)") + }; + let re = Self::cached_regex(&pattern)?; + let text = self.declaration_head(cls, cls.end_line - cls.start_line); + let text = super::awaited::blank_string_contents(&text); + let mut depth = 0usize; + let mut found = None; + for line in text.lines() { + if depth <= 1 { + if let Some(m) = re.captures(line) { + let before = &line[..m.get(0).unwrap().start()]; + let in_params = depth == 1 && before.matches('(').count() > before.matches(')').count(); + if !in_params && !matches!(&m[1], "return" | "new" | "throw" | "var" | "val" | "get" | "set" | "init" | "class" | "interface" | "object" | "static" | "final") { + found = if m[1].contains('.') { Some(m[1].to_string()) } else { self.normalize_inferred_type_name(&m[1])? }; break; + } + } + } + for ch in line.chars() { if ch == '{' { depth += 1; } else if ch == '}' { depth = depth.saturating_sub(1); } } + } + self.declared_member_memo.insert(key, found.clone()); Ok(found) + } + fn declared_parameter_bound(&mut self, name: &str, cls: &KNode) -> Res>> { + let head = self.declaration_head(cls, 5); + let head = head.split('{').next().unwrap_or(""); + let name = regex::escape(name); + let bound = Self::cached_regex(&format!(r"[<,]\s*(?:in\s+|out\s+|reified\s+)?{name}\s*(?:extends|:)\s*([A-Z][\w.]*)|\bwhere\s+{name}\s*:\s*([A-Z][\w.]*)"))?; + if let Some(m) = bound.captures(head) { return Ok(Some(m.get(1).or_else(|| m.get(2)).map(|v| v.as_str().to_string()))); } + Ok(Self::cached_regex(&format!(r"[<,]\s*(?:in\s+|out\s+|reified\s+)?{name}\s*[,>]"))?.is_match(head).then_some(None)) + } + pub(super) fn infer_declared_member_receiver_type(&mut self, receiver: &str, r: &ResolveRefIn) -> Res> { + if !matches!(r.language.as_str(), "java" | "kotlin" | "csharp") { return Ok(None); } + let name = receiver.strip_prefix("this.").unwrap_or(receiver); + if !re!(r"^[A-Za-z_]\w*$").is_match(name) { return Ok(None); } + let nodes = self.nodes_in_file(&r.file_path)?; + let Some(cls) = nodes.iter().filter(|n| n.language == r.language && class_kind(&n.kind) && n.start_line <= r.line && n.end_line >= r.line).max_by_key(|n| n.start_line) else { return Ok(None) }; + if !receiver.starts_with("this.") { + if let Some(f) = nodes.iter().filter(|n| n.language == r.language && matches!(n.kind.as_str(), "method" | "function") && n.start_line <= r.line && n.end_line >= r.line).max_by_key(|n| n.start_line) { + let body = self.declaration_head(f, r.line - f.start_line); + let n = regex::escape(name); + if Self::cached_regex(&format!(r"\b(?:var|val|out\s+[\w.<>?]+|foreach\s*\(\s*[\w.<>?,\s]+?)\s+{n}\b|\bfor\s*\([^;)]*\s{n}\s*:|\b{n}\s*=>|[(,]\s*{n}\s*(?:,[^()]*)?\)\s*=>|\b{n}\s*(?:,[^{{}}]*)?->"))?.is_match(&body) { return Ok(None); } + } + } + let mut queue = VecDeque::from([(cls.clone(), HashMap::::new())]); + let mut seen = HashSet::new(); + while seen.len() < 8 { + let Some((decl, args)) = queue.pop_front() else { break }; + if !seen.insert(decl.id.clone()) { continue; } + if let Some(found) = self.declared_member_type(&decl, name)? { + if let Some(given) = args.get(&found) { return Ok(Some(given.clone())); } + return Ok(Some(match self.declared_parameter_bound(&found, &decl)? { Some(Some(b)) => b, Some(None) => "object".to_string(), None => found })); + } + let head = self.declaration_head(&decl, 8); + let flat = super::call_shape::flat_head(&head, false); + let clause = re!(r"\b(?:extends|implements)\b([\s\S]*)$").captures(&flat).map(|m| m[1].to_string()) + .or_else(|| re!(r"\b(?:class|interface|struct|record|object)\s+\w+[^:]*:([\s\S]*)$").captures(&flat).map(|m| m[1].to_string())).unwrap_or_default(); + let clause = clause.split("where").next().unwrap_or(""); + for part in re!(r",|\bimplements\b").split(clause) { + let Some(sup) = re!(r"([A-Z]\w*)\s*$").captures(part).map(|m| m[1].to_string()) else { continue }; + let at = Self::cached_regex(&format!(r"(?:[:,]|\bextends|\bimplements)\s*(?:[\w.]+\.)?{}\s*<", regex::escape(&sup)))?.find(&head).map(|m| m.start()); + let given: Vec = at.map(|at| angle_arguments(&head[at..])).unwrap_or_default().iter().map(|a| { + let simple = a.split('<').next().unwrap_or("").trim(); args.get(simple).cloned().unwrap_or_else(|| simple.to_string()) + }).collect(); + for parent in self.nodes_by_name(&sup)?.iter().filter(|n| n.language == r.language && class_kind(&n.kind)) { + let head = self.declaration_head(parent, 3); + let at = Self::cached_regex(&format!(r"\b{}\s*<", regex::escape(&parent.name)))?.find(&head).map(|m| m.start()); + let params = at.map(|at| angle_arguments(&head[at..])).unwrap_or_default(); + let mappings = params.iter().enumerate().filter_map(|(i, p)| { + let p = re!(r"^(?:in|out|reified)\s+").replace(p, ""); + let name = re!(r"^([A-Za-z_]\w*)").captures(&p).map(|m| m[1].to_string())?; + given.get(i).filter(|a| !a.is_empty()).map(|a| (name, a.clone())) + }).collect(); + queue.push_back((parent.clone(), mappings)); + } + } + } + Ok(None) + } + pub(super) fn csharp_using_alias(&mut self, name: &str, file: &str) -> Option { + if !self.csharp_alias_memo.contains_key(file) { + let text = self.read_file(file).map(|s| super::awaited::strip_ts_comments(s.text())).unwrap_or_default(); + let aliases = re!(r"(?m)^\s*(?:global\s+)?using\s+([A-Za-z_]\w*)\s*=\s*(?:global::)?([\w.]+)\s*(?:<[^;>]*>)?\s*;") + .captures_iter(&text).map(|m| (m[1].to_string(), m[2].to_string())).collect(); + self.csharp_alias_memo.insert(file.to_string(), Rc::new(aliases)); + } + self.csharp_alias_memo[file].get(name).cloned() + } + pub(super) fn inferred_member_type_bound(&mut self, ty: &str, r: &ResolveRefIn) -> Res> { + if !matches!(r.language.as_str(), "java" | "kotlin" | "csharp") { return Ok(Some(ty.to_string())); } + let simple = ty.rsplit('.').next().unwrap_or(ty); + if simple.starts_with(|c: char| c.is_ascii_lowercase()) { return Ok(None); } + if !self.nodes_by_name(simple)?.iter().any(|n| class_kind(&n.kind)) { + let nodes = self.nodes_in_file(&r.file_path)?; + let mut scopes: Vec<_> = nodes.iter().filter(|n| n.start_line <= r.line && n.end_line >= r.line + && (class_kind(&n.kind) || matches!(n.kind.as_str(), "method" | "function"))).collect(); + scopes.sort_by_key(|n| (n.end_line - n.start_line, -n.start_line)); + for scope in scopes { + if let Some(bound) = self.declared_parameter_bound(simple, scope)? { return Ok(bound); } + } + } + Ok(Some(ty.to_string())) + } + pub(super) fn enum_constant_call(&mut self, receiver: &str, method: &str, r: &ResolveRefIn) -> Res> { + let Some((root, constant)) = receiver.split_once('.') else { return Ok(None) }; + if constant.contains('.') { return Ok(None); } + let Some(ty) = self.resolve_bound_type(root, r, 0)? else { return Ok(None) }; + if ty.kind != "enum" { return Ok(None); } + if !self.nodes_by_name(constant)?.iter().any(|n| n.kind == "enum_member" && n.file_path == ty.file_path && n.qualified_name == format!("{}::{constant}", ty.qualified_name)) { return Ok(None); } + self.match_bound_type_member(root, method, r) + } +} diff --git a/codegraph-kernel/src/resolve/esm_scope.rs b/codegraph-kernel/src/resolve/esm_scope.rs new file mode 100644 index 0000000000..ee92edd0c8 --- /dev/null +++ b/codegraph-kernel/src/resolve/esm_scope.rs @@ -0,0 +1,44 @@ +//! ES-module declaration visibility and callable exports returned by factories. + +use super::*; +use super::awaited::{blank_string_contents, strip_ts_comments}; + +impl KernelResolver { + pub(super) fn is_unexported_module_binding(&mut self, n: &KNode) -> Res { + if !is_esm_family(&n.language) || n.is_exported || n.qualified_name.contains("::") + || !matches!(n.kind.as_str(), "function" | "variable" | "constant" | "class" | "interface" | "type_alias" | "enum" | "component") + || re!(r"\.d\.[cm]?ts$").is_match(&n.file_path) { + return Ok(false); + } + if !self.esm_exports_memo.contains_key(&n.file_path) { + let Some(lines) = self.read_file(&n.file_path) else { return Ok(false) }; + let code = blank_string_contents(&strip_ts_comments(lines.text())); + let module = re!(r"(?m)^\s*(?:import\s|export\s)").is_match(&code) + && !re!(r"(?-u:\b)(?:module\.exports|exports\.|declare\s+global)").is_match(&code); + let mut names = HashSet::new(); + if module { + for m in re!(r"(?m)^[ \t]*export\s+(?:type\s+)?\{([^}]*)\}").captures_iter(&code) { + for item in m[1].split(',') { + let local = item.trim().strip_prefix("type ").unwrap_or(item.trim()).split_whitespace().next().unwrap_or(""); + names.insert(local.to_string()); + } + } + for m in re!(r"(?m)^[ \t]*export\s+(?:default|=)\s+([A-Za-z_$][\w$]*)\s*;?\s*$").captures_iter(&code) { + names.insert(m[1].to_string()); + } + for m in re!(r"(?m)^[ \t]*export\s+default\s*\{([^}]*)\}|(?-u:\b)return\s*\{([^{}]*)\}").captures_iter(&code) { + for item in m.get(1).or_else(|| m.get(2)).unwrap().as_str().split(',') { + let value = item.rsplit(':').next().unwrap_or("").trim(); + if re!(r"^[A-Za-z_$][\w$]*$").is_match(value) { names.insert(value.to_string()); } + } + } + } + self.esm_exports_memo.insert(n.file_path.clone(), (module, names)); + } + let (module, names) = &self.esm_exports_memo[&n.file_path]; + if !module || names.contains(&n.name) { return Ok(false) } + let Some(lines) = self.read_file(&n.file_path) else { return Ok(false) }; + let line = lines.get((n.start_line - 1).max(0) as usize).map(String::as_str).unwrap_or(""); + Ok(re!(r"^\s*(?:declare\s+)?(?:async\s+)?(?:function\*?|const|let|var|(?:abstract\s+)?class|interface|type|enum)\s").is_match(line)) + } +} diff --git a/codegraph-kernel/src/resolve/imports.rs b/codegraph-kernel/src/resolve/imports.rs index 597ba4b16c..0f83d87f34 100644 --- a/codegraph-kernel/src/resolve/imports.rs +++ b/codegraph-kernel/src/resolve/imports.rs @@ -462,13 +462,12 @@ impl KernelResolver { let base = fqn.replace('.', "/"); [".java", ".kt"].iter().any(|ext| file_path.ends_with(&format!("{base}{ext}"))) }; - // A chained call reduced to its bare method name starts its column at - // the chain, not the name: only a source that begins with the name is - // the import's (import-resolver.ts isBareCallSite). + // A bare import call names the callee directly, without a member dot before it. let bare_call_site = if r.reference_kind == "calls" { match self.read_file(&r.file_path) { Some(lines) => lines.get((r.line - 1) as usize).is_none_or(|line| { - js_slice(line, r.column as usize) + !line[..js_unit_to_byte(line, r.column.max(0) as usize)].trim_end().ends_with('.') + && js_slice(line, r.column.max(0) as usize) .strip_prefix(r.reference_name.as_str()) .is_some_and(|rest| !rest.starts_with(|c: char| c.is_ascii_alphanumeric() || c == '_' || c == '$')) }), diff --git a/codegraph-kernel/src/resolve/iteration.rs b/codegraph-kernel/src/resolve/iteration.rs index 520d2a311d..9d7fabcb1a 100644 --- a/codegraph-kernel/src/resolve/iteration.rs +++ b/codegraph-kernel/src/resolve/iteration.rs @@ -28,7 +28,7 @@ fn before(a: (usize, usize), b: (usize, usize)) -> bool { /// descendantForPosition (tree.ts): descend into the first child, named or /// not, whose span contains the point, until none does. -fn descendant_for_position<'t>(root: TsNode<'t>, text: &str, at: (usize, usize)) -> TsNode<'t> { +pub(super) fn descendant_for_position<'t>(root: TsNode<'t>, text: &str, at: (usize, usize)) -> TsNode<'t> { let mut node = root; 'down: loop { let mut cursor = node.walk(); @@ -44,7 +44,7 @@ fn descendant_for_position<'t>(root: TsNode<'t>, text: &str, at: (usize, usize)) } } -fn named_children<'t>(node: TsNode<'t>) -> Vec> { +pub(super) fn named_children<'t>(node: TsNode<'t>) -> Vec> { let mut cursor = node.walk(); node.named_children(&mut cursor).collect() } @@ -85,7 +85,15 @@ fn kotlin_callee_name<'a>(callee: TsNode, text: &'a str) -> &'a str { fn kotlin_takes_trailing_lambda(signature: Option<&str>) -> bool { let Some(sig) = signature else { return false }; let sig = sig.trim(); - let inner = sig.strip_prefix('(').and_then(|s| s.strip_suffix(')')).unwrap_or(sig); + let inner = if sig.starts_with('(') { + let mut depth = 0usize; + let close = sig.char_indices().find_map(|(i, ch)| { + if ch == '(' { depth += 1; } + else if ch == ')' { depth -= 1; if depth == 0 { return Some(i); } } + None + }); + close.map(|end| &sig[1..end]).unwrap_or(sig) + } else { sig }; // Depth-0 split points, reading `->` as an arrow rather than a closer. let top_level = |s: &str, target: u8| -> Vec { let (b, mut depth, mut out, mut i) = (s.as_bytes(), 0i32, Vec::new(), 0); @@ -134,7 +142,7 @@ fn kotlin_takes_trailing_lambda(signature: Option<&str>) -> bool { } /// The (row, UTF-16 column) site of `node` in `r`'s file. -fn site_at(node: TsNode, text: &str, r: &ResolveRefIn) -> ResolveRefIn { +pub(super) fn site_at(node: TsNode, text: &str, r: &ResolveRefIn) -> ResolveRefIn { let (row, col) = point16(text, node.start_byte(), node.start_position()); let mut site = r.clone(); site.line = row as i64 + 1; @@ -229,7 +237,7 @@ fn node_text<'a>(node: TsNode, text: &'a str) -> &'a str { impl KernelResolver { /// `file`'s tree, parsed once per resolver instead of once per ref. - fn parsed_tree(&mut self, file: &Rc, r: &ResolveRefIn) -> Option> { + pub(super) fn parsed_tree(&mut self, file: &Rc, r: &ResolveRefIn) -> Option> { let key = format!("{}\0{}", r.language, r.file_path); if let Some((source, tree)) = self.tree_cache.get(&key) { if Rc::ptr_eq(source, file) { @@ -241,6 +249,23 @@ impl KernelResolver { tree } + /// The call expression that supplies a Kotlin navigation receiver across lines. + pub(super) fn kotlin_chain_receiver_call(&mut self, r: &ResolveRefIn) -> Option { + let file = self.read_file(&r.file_path)?; + let tree = self.parsed_tree(&file, r)?; + let mut node = descendant_for_position(tree.root_node(), file.text(), ((r.line - 1).max(0) as usize, r.column.max(0) as usize + 1)); + if node.kind() != "simple_identifier" { return None; } + while let Some(parent) = node.parent() { + if parent.kind() == "navigation_expression" { + let receiver = parent.named_child(0)?; + return (receiver.kind() == "call_expression").then(|| node_text(receiver, file.text()).to_string()); + } + if parent.kind() != "navigation_suffix" { return None; } + node = parent; + } + None + } + /// The names a Kotlin lambda binds: its declared parameters, else /// implicit `it`, except a lambda given to the stdlib `run`, `apply` or /// `with`, which takes its value as `this` and binds nothing. A @@ -271,7 +296,7 @@ impl KernelResolver { /// that name visible here for a bare call, a member or visible extension /// on the receiver's type for `x.run { }`. Either must take a trailing /// lambda. An untyped receiver keeps the stdlib reading. - fn kotlin_user_scope_callee(&mut self, callee: TsNode, name: &str, text: &str, r: &ResolveRefIn) -> Res { + pub(super) fn kotlin_user_scope_callee(&mut self, callee: TsNode, name: &str, text: &str, r: &ResolveRefIn) -> Res { let declared: Vec> = self .nodes_by_name(name)? .iter() @@ -303,7 +328,7 @@ impl KernelResolver { /// Whether Kotlin type `ty` has a member `method`, its own or /// inherited, or a visible extension `fun Ty.method`, that takes a /// trailing lambda. - fn kotlin_user_member(&mut self, ty: &str, method: &str, site: &ResolveRefIn) -> Res { + pub(super) fn kotlin_user_member(&mut self, ty: &str, method: &str, site: &ResolveRefIn) -> Res { if let Some(m) = self.match_bound_type_member(ty, method, site)? { return Ok(kotlin_takes_trailing_lambda(m.node.signature.as_deref())); } @@ -326,7 +351,7 @@ impl KernelResolver { /// Whether a top-level Kotlin declaration (a function, or an extension) /// is visible from `r`'s file: the same file or package, or imported by /// name or by its package's wildcard. - fn kotlin_top_level_visible(&mut self, n: &KNode, r: &ResolveRefIn) -> Res { + pub(super) fn kotlin_top_level_visible(&mut self, n: &KNode, r: &ResolveRefIn) -> Res { if n.file_path == r.file_path { return Ok(true); } diff --git a/codegraph-kernel/src/resolve/kotlin_calls.rs b/codegraph-kernel/src/resolve/kotlin_calls.rs new file mode 100644 index 0000000000..1d1cbc557c --- /dev/null +++ b/codegraph-kernel/src/resolve/kotlin_calls.rs @@ -0,0 +1,150 @@ +//! Implicit Kotlin receivers: type bodies, extensions, anonymous objects and DSL lambdas. +use super::*; +use super::call_shape::flat_head; + +pub(crate) struct KotlinFrame { start: i64, end: i64, names: Vec } + +fn head_names(head: &str) -> Vec { + if re!(r"\bcompanion\s+object(?:\s+\w+)?\s*$").is_match(head) { return vec!["Companion".to_string()]; } + let flat = flat_head(head, false); + if let Some(m) = re!(r"\b(?:class|interface|object)\b(?:\s+([A-Za-z_]\w*))?([^=]*)$").captures(&flat) { + let mut names = m.get(1).map(|n| vec![n.as_str().to_string()]).unwrap_or_default(); + let tail = m.get(2).map(|v| v.as_str()).unwrap_or(""); + if let Some((_, supers)) = tail.split_once(':') { + for part in supers.split("where").next().unwrap_or("").split(',') { + if let Some(m) = re!(r"(?:^|\.)([A-Z]\w*)\s*(?:by\b.*)?$").captures(part.trim()) { names.push(m[1].to_string()); } + } + } + return names; + } + re!(r"\bfun\s+(?:<[^>]*>\s*)?([A-Z]\w*)(?:<[^>]*>)?\??\.[A-Za-z_]\w*\s*\(") + .captures(head).map(|m| vec![m[1].to_string()]).unwrap_or_default() +} +fn platform_supers(name: &str) -> &'static [&'static str] { + match name { + "AppCompatActivity" => &["FragmentActivity"], "FragmentActivity" => &["ComponentActivity"], + "ComponentActivity" => &["Activity", "LifecycleOwner", "ViewModelStoreOwner", "SavedStateRegistryOwner"], + "Activity" => &["ContextThemeWrapper", "ComponentCallbacks2"], "ContextThemeWrapper" => &["ContextWrapper"], + "ContextWrapper" => &["Context"], "Application" | "Service" => &["ContextWrapper", "ComponentCallbacks2"], + "ComponentCallbacks2" => &["ComponentCallbacks"], + "Fragment" => &["ComponentCallbacks", "LifecycleOwner", "ViewModelStoreOwner", "SavedStateRegistryOwner"], + "DialogFragment" => &["Fragment"], "AppCompatDialogFragment" => &["DialogFragment"], + "BottomSheetDialogFragment" => &["AppCompatDialogFragment"], "AndroidViewModel" => &["ViewModel"], _ => &[], + } +} +impl KernelResolver { + fn kotlin_frames(&mut self, file: &str) -> Rc> { + if let Some(hit) = self.kotlin_frames_memo.get(file) { return hit.clone(); } + let source = self.read_file(file).map(|s| super::awaited::blank_string_contents(&super::awaited::strip_ts_comments(s.text()))).unwrap_or_default(); + let mut frames = Vec::new(); + let mut stack = Vec::new(); + let mut pending = String::new(); + let mut line = 1; + for ch in source.chars() { + if ch == '\n' { line += 1; } + match ch { + '{' => { + let names = if re!(r"(?:\bwith\s*\([^{}]*\)|\.\s*(?:apply|run)(?:\s*<[^<>]*>)?)\s*$").is_match(&pending) { + vec!["*".to_string()] + } else { head_names(&pending) }; + stack.push((line, names)); pending.clear(); + } + '}' => { + if let Some((start, names)) = stack.pop() { if !names.is_empty() { frames.push(KotlinFrame { start, end: line, names }); } } + pending.clear(); + } + ';' => pending.clear(), + _ => { pending.push(ch); if pending.len() > 600 { let at = pending.char_indices().nth(300).map(|(i, _)| i).unwrap_or(0); pending.drain(..at); } } + } + } + let frames = Rc::new(frames); + self.kotlin_frames_memo.insert(file.to_string(), frames.clone()); frames + } + fn kotlin_receiver_types(&mut self) -> Res>> { + if let Some(hit) = &self.kotlin_receiver_types_memo { return Ok(hit.clone()); } + let mut names = HashSet::new(); + let mut outside = HashSet::new(); + let files = match self.sorted_files() { + Some(files) => files, + None => { + let mut files: Vec<_> = self.table()?.files.iter().cloned().collect(); + files.sort(); Arc::new(files) + } + }; + { + for file in files.iter().filter(|f| f.ends_with(".kt") || f.ends_with(".kts")) { + let Some(source) = self.read_file(file) else { continue }; + let text = super::awaited::strip_ts_comments(source.text()); + for sam in re!(r"\bfun\s+interface\s+\w+[^{}]*\{([^{}]*)\}").captures_iter(&text) { + for receiver in re!(r"\bfun\s+([A-Z]\w*)(?:<[^<>]*>)?\.\w+\s*\(").captures_iter(&sam[1]) { + names.insert(receiver[1].to_string()); + } + } + for m in re!(r"\b([A-Z]\w*)(?:<[^<>()]*(?:<[^<>()]*>[^<>()]*)*>)?\s*\.\s*\(").captures_iter(&text) { names.insert(m[1].to_string()); } + for m in re!(r"\bfun\s+(?:<[^>]*>\s*)?([A-Z]\w*(?:\.[A-Z]\w*)*)(?:<[^<>()]*(?:<[^<>()]*>[^<>()]*)*>)?\??\.[A-Za-z_`][\w`]*\s*\(").captures_iter(&text) { + outside.extend(m[1].split('.').map(str::to_string)); + } + } + } + for name in outside { + if !self.nodes_by_name(&name)?.iter().any(|n| matches!(n.language.as_str(), "kotlin" | "java") && matches!(n.kind.as_str(), "class" | "interface" | "enum" | "struct" | "trait")) { names.insert(name); } + } + let mut queue: VecDeque = names.iter().cloned().collect(); + while names.len() < 5000 { + let Some(name) = queue.pop_front() else { break }; + for sup in self.kotlin_supertypes_of(&name)?.iter() { if names.insert(sup.clone()) { queue.push_back(sup.clone()); } } + } + let names = Rc::new(names); self.kotlin_receiver_types_memo = Some(names.clone()); Ok(names) + } + fn kotlin_precondition(&mut self, r: &ResolveRefIn) -> bool { + if !matches!(r.reference_name.as_str(), "require" | "check" | "assert") { return false; } + let Some(lines) = self.read_file(&r.file_path) else { return false }; + let Some(line) = lines.get((r.line - 1).max(0) as usize) else { return false }; + let Ok(re) = Self::cached_regex(&format!(r"(?:^|[^\w.]){}\s*\(", regex::escape(&r.reference_name))) else { return false }; + let Some(at) = re.find(line) else { return false }; + let tail = &line[at.end()..]; + let mut depth = 0usize; + let mut end = tail.len(); + for (i, ch) in tail.char_indices() { + if ch == '(' { depth += 1; } + else if ch == ')' { if depth == 0 { end = i; break; } depth -= 1; } + } + re!(r"[<>=!]=|&&|\|\||(?:^|[\s(])!|\s[<>]\s|\bis\b|\bin\b|\btrue\b|\bfalse\b").is_match(&tail[..end]) + || tail.get(end + 1..).is_some_and(|rest| rest.trim_start().starts_with('{')) + } + pub(super) fn kotlin_member_reachable(&mut self, n: &KNode, r: &ResolveRefIn) -> Res { + if n.kind != "method" || !matches!(n.language.as_str(), "kotlin" | "java") { return Ok(true); } + if self.kotlin_precondition(r) { return Ok(false); } + // Top-level extensions use package imports; their receiver is not an enclosing class. + if n.language == "kotlin" && !self.nodes_in_file(&n.file_path)?.iter().any(|o| { + o.id != n.id && matches!(o.kind.as_str(), "class" | "interface" | "object" | "enum" | "struct" | "trait") + && o.start_line <= n.start_line && o.end_line >= n.end_line + }) { return Ok(true); } + if r.file_path.ends_with(".kts") { return Ok(true); } + let Some((path, _)) = n.qualified_name.rsplit_once("::") else { return Ok(true) }; + let mut parts = path.rsplit([':', '.']).filter(|v| !v.is_empty()); + let mut owner = parts.next().unwrap_or(""); + let companion = owner == "Companion"; + if companion { owner = parts.next().unwrap_or(owner); } + if n.name == owner { return Ok(true); } + let mut queue: VecDeque = self.nodes_in_file(&r.file_path)?.iter().filter(|t| t.start_line <= r.line && t.end_line >= r.line) + .filter_map(|t| if matches!(t.kind.as_str(), "class" | "interface" | "enum" | "struct" | "trait") { Some(t.name.clone()) } + else if matches!(t.kind.as_str(), "function" | "method") { t.qualified_name.rsplit_once("::").map(|(p, _)| p.rsplit([':', '.']).next().unwrap_or("").to_string()) } else { None }).collect(); + for frame in self.kotlin_frames(&r.file_path).iter().filter(|f| f.start <= r.line && f.end >= r.line) { queue.extend(frame.names.iter().cloned()); } + if self.kotlin_receiver_types()?.contains(owner) { return Ok(true); } + let mut seen = HashSet::new(); + while seen.len() < 60 { + let Some(name) = queue.pop_front() else { break }; + if !seen.insert(name.clone()) { continue; } + if name == owner || name == "*" { return Ok(true); } + queue.extend(platform_supers(&name).iter().map(|s| (*s).to_string())); + queue.extend(self.kotlin_supertypes_of(&name)?.iter().cloned()); + } + let pkg = self.kotlin_file_scope(&n.file_path).pkg.clone(); + let object = format!("{}{}{}", if pkg.is_empty() { String::new() } else { format!("{pkg}.") }, owner, if companion { ".Companion" } else { "" }); + let source_companion = self.kotlin_frames(&n.file_path).iter().any(|f| f.start <= n.start_line && f.end >= n.end_line && f.names.iter().any(|name| name == "Companion")); + let here = self.kotlin_file_scope(&r.file_path); + Ok(here.imports.contains(&format!("{object}.{}", n.name)) || here.stars.contains(&object) + || (!companion && source_companion && (here.imports.contains(&format!("{object}.Companion.{}", n.name)) || here.stars.contains(&format!("{object}.Companion"))))) + } +} diff --git a/codegraph-kernel/src/resolve/kotlin_receiver.rs b/codegraph-kernel/src/resolve/kotlin_receiver.rs new file mode 100644 index 0000000000..ac1ac3c433 --- /dev/null +++ b/codegraph-kernel/src/resolve/kotlin_receiver.rs @@ -0,0 +1,234 @@ +//! Proven Kotlin receiver types through declarations and bounded expression walks. +use super::*; +use super::iteration::{descendant_for_position, named_children, site_at}; +use tree_sitter::Node; + +pub(super) struct KotlinReceiverHit { + pub(super) ty: String, + pub(super) site: ResolveRefIn, + pub(super) heuristic: bool, +} + +fn text<'a>(node: Node, source: &'a str) -> &'a str { &source[node.start_byte()..node.end_byte()] } +fn simple_type(raw: &str) -> &str { raw.split('<').next().unwrap_or(raw).trim().trim_end_matches('?') } +fn property(node: Node) -> Option { + let child = node.named_child(1)?; + if child.kind() == "navigation_suffix" { + named_children(child).into_iter().find(|n| n.kind() == "simple_identifier") + } else { Some(child) } +} + +impl KernelResolver { + pub(super) fn kotlin_call_receiver_type(&mut self, r: &ResolveRefIn) -> Res> { + if r.language != "kotlin" || r.reference_kind != "calls" { return Ok(None); } + let Some(file) = self.read_file(&r.file_path) else { return Ok(None) }; + let Some(tree) = self.parsed_tree(&file, r) else { return Ok(None) }; + let mut node = descendant_for_position(tree.root_node(), file.text(), ((r.line - 1).max(0) as usize, r.column.max(0) as usize + 1)); + if node.kind() != "simple_identifier" { return Ok(None); } + while let Some(parent) = node.parent() { + if parent.kind() == "navigation_expression" { + let Some(receiver) = parent.named_child(0) else { return Ok(None) }; + return self.kotlin_expression_type(receiver, file.text(), r, None, 0); + } + if parent.kind() != "navigation_suffix" { return Ok(None); } + node = parent; + } + Ok(None) + } + + fn kotlin_type_ancestors(&mut self, hit: &KotlinReceiverHit) -> Res>> { + let Some(root) = self.resolve_bound_type(&hit.ty, &hit.site, 0)? else { return Ok(vec![]) }; + let mut queue = VecDeque::from([root]); + let mut seen = HashSet::new(); + let mut result = Vec::new(); + while result.len() < 32 { + let Some(node) = queue.pop_front() else { break }; + if !seen.insert(node.id.clone()) { continue; } + queue.extend(self.supertype_nodes(&node.id)?); + result.push(node); + } + Ok(result) + } + + pub(super) fn kotlin_receiver_accepts(&mut self, n: &KNode, hit: &KotlinReceiverHit, r: &ResolveRefIn) -> Res { + let Some(owner_name) = super::call_shape::owner(n) else { return Ok(false) }; + let site = r.clone().at(n); + let Some(owner) = self.resolve_bound_type(owner_name, &site, 0)? else { return Ok(false) }; + if !self.kotlin_type_ancestors(hit)?.iter().any(|a| a.id == owner.id) { return Ok(false); } + if n.file_path == owner.file_path && n.qualified_name == format!("{}::{}", owner.qualified_name, n.name) { return Ok(true); } + self.kotlin_top_level_visible(n, r) + } + + fn kotlin_receiver_member(&mut self, hit: &KotlinReceiverHit, name: &str, r: &ResolveRefIn) -> Res>> { + if let Some(member) = self.match_bound_type_member(&hit.ty, name, &hit.site)? { return Ok(Some(member.node)); } + let candidates: Vec<_> = self.nodes_by_name(name)?.iter().filter(|n| n.language == "kotlin" && n.kind == "method").cloned().collect(); + let mut matching = Vec::new(); + for candidate in candidates { + if self.kotlin_receiver_accepts(&candidate, hit, r)? { matching.push(candidate); } + } + let Some(first) = matching.first() else { return Ok(None) }; + if matching.iter().all(|m| m.qualified_name == first.qualified_name && m.file_path == first.file_path) { Ok(Some(first.clone())) } + else { Ok(None) } + } + + fn kotlin_expression_type(&mut self, node: Node, source: &str, r: &ResolveRefIn, implicit: Option<&KotlinReceiverHit>, depth: usize) -> Res> { + if depth > 8 { return Ok(None); } + let site = site_at(node, source, r); + if node.kind() == "simple_identifier" { + let name = text(node, source); + if let Some(ty) = self.infer_local_receiver_type(name, &site, true)? { + return Ok(Some(KotlinReceiverHit { ty: simple_type(&ty).to_string(), site, heuristic: false })); + } + return Ok(self.resolve_bound_type(name, &site, 0)?.map(|_| KotlinReceiverHit { ty: name.to_string(), site, heuristic: false })); + } + if node.kind() == "navigation_expression" { + let (Some(root), Some(member)) = (node.named_child(0), property(node)) else { return Ok(None) }; + let Some(hit) = self.kotlin_expression_type(root, source, &site, implicit, depth + 1)? else { return Ok(None) }; + for owner in self.kotlin_type_ancestors(&hit)? { + let properties: Vec<_> = self.nodes_by_qualified_name(&format!("{}::{}", owner.qualified_name, text(member, source)))?.iter() + .filter(|n| n.file_path == owner.file_path && matches!(n.kind.as_str(), "property" | "field" | "variable" | "constant")).cloned().collect(); + if let [decl] = properties.as_slice() { + if let Some(raw) = &decl.return_type { return Ok(Some(KotlinReceiverHit { ty: simple_type(raw).to_string(), site: site.clone().at(decl), heuristic: hit.heuristic })); } + let mut inferred = self.kotlin_property_initializer(decl, &owner, &site, depth + 1)?; + if let Some(value) = &mut inferred { value.heuristic |= hit.heuristic; } + return Ok(inferred); + } + if !properties.is_empty() { return Ok(None); } + } + return Ok(None); + } + if node.kind() != "call_expression" { return Ok(None); } + let Some(func) = node.named_child(0) else { return Ok(None) }; + let (member, receiver) = if func.kind() == "navigation_expression" { + let (Some(root), Some(name)) = (func.named_child(0), property(func)) else { return Ok(None) }; + let hit = match self.kotlin_expression_type(root, source, &site, implicit, depth + 1)? { + Some(hit) => hit, + None if root.kind() == "simple_identifier" => return self.kotlin_imported_return_hypothesis(text(name, source), &site), + None => return Ok(None), + }; + let member = self.kotlin_receiver_member(&hit, text(name, source), &site)?; + (member, Some(hit)) + } else if func.kind() == "simple_identifier" { + let name = text(func, source); + let bindings = self.bindings(&site.file_path)?; + let binding = innermost_binding(&bindings, name, Some(site.line)); + if let Some(bound) = binding.filter(|b| b.kind != "import") { + if !self.node_by_opt_id(bound.node_id.as_deref())?.is_some_and(|n| matches!(n.kind.as_str(), "function" | "method" | "class" | "interface" | "enum" | "type_alias")) { return Ok(None); } + } + if let Some(owner) = self.resolve_bound_type(name, &site, 0)? { + return Ok(Some(KotlinReceiverHit { ty: owner.name.clone(), site: site.clone().at(&owner), heuristic: false })); + } + let mut member = match implicit { Some(hit) => self.kotlin_receiver_member(hit, name, &site)?, None => None }; + if member.is_none() { + match binding { + Some(binding) if binding.kind == "import" => { + member = self.resolve_via_import(&site.clone().naming(name, "calls"))?.map(|c| c.node).filter(|n| n.kind == "function"); + if member.is_none() { + let mappings = self.import_mappings(&site.file_path)?; + let candidates: Vec<_> = self.nodes_by_name(name)?.iter().filter(|n| n.language == "kotlin" && n.kind == "function").cloned().collect(); + let mut imported = Vec::new(); + for n in candidates { + let pkg = self.kotlin_file_scope(&n.file_path).pkg.clone(); + if mappings.iter().any(|m| m.local_name == name && m.source == format!("{pkg}.{}", n.name) && m.source == n.qualified_name.replace("::", ".")) && self.kotlin_top_level_visible(&n, &site)? { imported.push(n); } + } + if let [only] = imported.as_slice() { member = Some(only.clone()); } + } + } + Some(binding) => { member = self.node_by_opt_id(binding.node_id.as_deref())?.filter(|n| n.kind == "function"); } + None => { + let candidates: Vec<_> = self.nodes_by_name(name)?.iter().filter(|n| n.language == "kotlin" && n.kind == "function").cloned().collect(); + let mut visible = Vec::new(); + for n in candidates { if self.kotlin_top_level_visible(&n, &site)? { visible.push(n); } } + if let [only] = visible.as_slice() { member = Some(only.clone()); } + } + } + } + (member, implicit.map(|h| KotlinReceiverHit { ty: h.ty.clone(), site: h.site.clone(), heuristic: h.heuristic })) + } else { return Ok(None); }; + let Some(member) = member else { return Ok(None) }; + let overloads: Vec<_> = self.nodes_by_qualified_name(&member.qualified_name)?.iter().filter(|m| m.file_path == member.file_path && m.kind == member.kind).cloned().collect(); + let mut result: Option = None; + for overload in overloads { + let next = match &overload.return_type { + Some(raw) => { + let ty = simple_type(raw); + if overload.type_parameters.as_ref().is_some_and(|ps| ps.iter().any(|p| p.split_whitespace().next() == Some(ty))) { return Ok(None); } + Some(KotlinReceiverHit { ty: ty.to_string(), site: site.clone().at(&overload), heuristic: receiver.as_ref().is_some_and(|h| h.heuristic) }) + } + None => match &receiver { + Some(hit) if self.kotlin_preserves_receiver(&overload, hit, &site)? => Some(KotlinReceiverHit { ty: hit.ty.clone(), site: hit.site.clone(), heuristic: hit.heuristic }), + _ => None, + }, + }; + let Some(next) = next else { return Ok(None) }; + if let Some(previous) = &result { + let a = self.resolve_bound_type(&previous.ty, &previous.site, 0)?; + let b = self.resolve_bound_type(&next.ty, &next.site, 0)?; + if !a.zip(b).is_some_and(|(a,b)| a.id == b.id) { return Ok(None); } + } + result = Some(next); + } + Ok(result) + } + + fn kotlin_imported_return_hypothesis(&mut self, name: &str, r: &ResolveRefIn) -> Res> { + let mappings = self.import_mappings(&r.file_path)?; + let candidates: Vec<_> = self.nodes_by_name(name)?.iter().filter(|n| n.language == "kotlin" && n.kind == "method").cloned().collect(); + let mut imported = Vec::new(); + for n in candidates { + let pkg = self.kotlin_file_scope(&n.file_path).pkg.clone(); + if !mappings.iter().any(|m| m.local_name == name && m.source == format!("{pkg}.{name}")) { continue; } + if self.nodes_in_file(&n.file_path)?.iter().any(|o| super::call_shape::type_kind(&o.kind) && !matches!(o.kind.as_str(), "namespace" | "module") && o.start_line <= n.start_line && o.end_line >= n.end_line) { continue; } + imported.push(n); + } + let Some(factory) = imported.first() else { return Ok(None) }; + let Some(raw) = &factory.return_type else { return Ok(None) }; + let hit = KotlinReceiverHit { ty: simple_type(raw).to_string(), site: r.clone().at(factory), heuristic: true }; + let Some(owner) = self.resolve_bound_type(&hit.ty, &hit.site, 0)? else { return Ok(None) }; + // A declared incompatible member can shadow the imported extension. + let competing: Vec<_> = self.nodes_by_name(name)?.iter().filter(|n| matches!(n.language.as_str(), "kotlin" | "java") && matches!(n.kind.as_str(), "function" | "method")).cloned().collect(); + for member in competing { + let Some(raw) = &member.return_type else { continue }; + if !self.resolve_bound_type(simple_type(raw), &r.clone().at(&member), 0)?.is_some_and(|other| other.id == owner.id) { return Ok(None); } + } + Ok(Some(hit)) + } + + fn kotlin_property_initializer(&mut self, decl: &KNode, owner: &KNode, r: &ResolveRefIn, depth: usize) -> Res> { + let Some(file) = self.read_file(&decl.file_path) else { return Ok(None) }; + let site = r.clone().at(decl); + let Some(tree) = self.parsed_tree(&file, &site) else { return Ok(None) }; + let mut node = descendant_for_position(tree.root_node(), file.text(), ((decl.start_line - 1).max(0) as usize, decl.start_column.max(0) as usize + 1)); + while node.kind() != "property_declaration" { + let Some(parent) = node.parent() else { return Ok(None) }; node = parent; + } + let Some(initializer) = named_children(node).into_iter().last().filter(|n| n.kind() == "call_expression") else { return Ok(None) }; + let implicit = KotlinReceiverHit { ty: owner.name.clone(), site: site.clone().at(owner), heuristic: false }; + self.kotlin_expression_type(initializer, file.text(), &site, Some(&implicit), depth) + } + + fn kotlin_preserves_receiver(&mut self, decl: &KNode, receiver: &KotlinReceiverHit, r: &ResolveRefIn) -> Res { + let Some(file) = self.read_file(&decl.file_path) else { return Ok(false) }; + let site = r.clone().at(decl); + let Some(tree) = self.parsed_tree(&file, &site) else { return Ok(false) }; + let mut node = descendant_for_position(tree.root_node(), file.text(), ((decl.start_line - 1).max(0) as usize, decl.start_column.max(0) as usize + 1)); + while node.kind() != "function_declaration" { + let Some(parent) = node.parent() else { return Ok(false) }; node = parent; + } + let Some(body) = named_children(node).into_iter().find(|n| n.kind() == "function_body") else { return Ok(false) }; + let children: Vec<_> = named_children(body).into_iter().filter(|n| !n.is_extra()).collect(); + let [expr] = children.as_slice() else { return Ok(false) }; + if !text(body, file.text()).trim_start().starts_with('=') { return Ok(false); } + if expr.kind() == "this_expression" { return Ok(text(*expr, file.text()).trim() == "this"); } + if expr.kind() != "call_expression" { return Ok(false); } + let Some(func) = expr.named_child(0) else { return Ok(false) }; + if func.kind() != "simple_identifier" || text(func, file.text()) != "apply" { return Ok(false); } + let call_site = site_at(func, file.text(), &site); + let bindings = self.bindings(&site.file_path)?; + if let Some(binding) = innermost_binding(&bindings, "apply", Some(call_site.line)) { + if binding.kind != "import" || !self.import_mappings(&site.file_path)?.iter().any(|m| m.local_name == "apply" && m.source == "kotlin.apply") { return Ok(false); } + } + // A project scope function can return a different type. + Ok(!self.kotlin_user_member(&receiver.ty, "apply", &site)? && !self.kotlin_user_scope_callee(func, "apply", file.text(), &site)?) + } +} diff --git a/codegraph-kernel/src/resolve/lang_scope.rs b/codegraph-kernel/src/resolve/lang_scope.rs index 739d859495..17704e3282 100644 --- a/codegraph-kernel/src/resolve/lang_scope.rs +++ b/codegraph-kernel/src/resolve/lang_scope.rs @@ -37,9 +37,9 @@ fn is_kotlin_type_kind(kind: &str) -> bool { /// A Kotlin file's `package` and the names and packages its `import`s bring in. #[derive(Default)] pub(crate) struct KotlinFileScope { - pkg: String, - imports: HashSet, - stars: HashSet, + pub(super) pkg: String, + pub(super) imports: HashSet, + pub(super) stars: HashSet, } fn collect_kotlin_file_scope(text: &str) -> KotlinFileScope { @@ -77,7 +77,7 @@ fn is_bare_cfml_call(r: &ResolveRefIn) -> bool { /// Where a ref's name starts on its line, as a byte offset: at the ref's /// column, or just before it (a Dart ref's column sits past the name). -fn name_start_at_column(line: &str, name: &str, column: usize) -> Option { +pub(super) fn name_start_at_column(line: &str, name: &str, column: usize) -> Option { if js_slice(line, column).starts_with(name) { return Some(js_unit_to_byte(line, column)); } @@ -116,6 +116,8 @@ impl KernelResolver { candidates: Vec>, r: &ResolveRefIn, ) -> Res<(Vec>, bool)> { + let shape_call = matches!(r.language.as_str(), "rust" | "go" | "swift" | "scala" | "kotlin") + && r.reference_kind == "calls" && re!(r"^[A-Za-z_$][A-Za-z0-9_$]*$").is_match(&r.reference_name); let ruby_bare = is_bare_ruby_call(r); let cfml_bare = is_bare_cfml_call(r); let kotlin_call = is_bare_call_of(r, "kotlin") && !self.is_kotlin_qualified_call(r); @@ -123,12 +125,13 @@ impl KernelResolver { let member_site = matches!(r.language.as_str(), "vbnet" | "csharp" | "objc") .then(|| self.member_site(r)) .filter(|s| s.is_judged()); - if !ruby_bare && !cfml_bare && !kotlin_call && !dart_bare && member_site.is_none() { + if !shape_call && !ruby_bare && !cfml_bare && !kotlin_call && !dart_bare && member_site.is_none() { return Ok((candidates, false)); } let mut shrank = false; let mut kept = Vec::with_capacity(candidates.len()); for n in candidates { + if shape_call && !self.call_shape_target(&n, r)? { shrank = true; continue; } if let Some(site) = &member_site { if !self.is_member_in_reach(&n, site, r)? { shrank = true; @@ -141,7 +144,7 @@ impl KernelResolver { if cfml_bare && n.kind == "method" && !self.is_cfml_method_in_scope(&n, r)? { continue; } - if kotlin_call && !self.is_kotlin_top_level_visible(&n, r)? { + if kotlin_call && (!self.is_kotlin_top_level_visible(&n, r)? || !self.kotlin_member_reachable(&n, r)?) { shrank = true; continue; } @@ -157,9 +160,11 @@ impl KernelResolver { /// removing others is bound to the call site (see /// `is_kotlin_survivor_in_scope` and `is_member_survivor_bound`). pub(super) fn is_lang_survivor_bound(&mut self, n: &KNode, r: &ResolveRefIn) -> Res { + if n.file_path == r.file_path { return Ok(true); } if r.language == "kotlin" { return self.is_kotlin_survivor_in_scope(n, r); } + if r.language == "swift" { return self.swift_scope_bound(n, r); } let site = self.member_site(r); self.is_member_survivor_bound(n, &site, r) } @@ -243,7 +248,7 @@ impl KernelResolver { /// The simple names a Kotlin type's declarations list after `:` in their /// head (`class A(x: Int) : B(x), C by d`), constructor arguments and type /// arguments dropped. - fn kotlin_supertypes_of(&mut self, type_name: &str) -> Res>> { + pub(super) fn kotlin_supertypes_of(&mut self, type_name: &str) -> Res>> { if let Some(hit) = self.kotlin_supers_memo.get(type_name) { return Ok(hit.clone()); } @@ -460,7 +465,7 @@ impl KernelResolver { || KOTLIN_DEFAULT_IMPORTS.contains(&pkg.as_str())) } - fn kotlin_file_scope(&mut self, file: &str) -> Rc { + pub(super) fn kotlin_file_scope(&mut self, file: &str) -> Rc { if let Some(hit) = self.kotlin_scope_memo.get(file) { return hit.clone(); } diff --git a/codegraph-kernel/src/resolve/member_scope.rs b/codegraph-kernel/src/resolve/member_scope.rs index 7ecccb5085..e543fb654e 100644 --- a/codegraph-kernel/src/resolve/member_scope.rs +++ b/codegraph-kernel/src/resolve/member_scope.rs @@ -56,7 +56,16 @@ pub(super) enum ObjcShape { /// UIKit / AppKit superclasses, for a category on a system class: an /// `UIImageView (WebCache)` method sending `[self sd_internalSetImageWithURL:…]` /// reaches the `UIView (WebCache)` category. -const OBJC_SYSTEM_SUPERS: &[(&str, &str)] = &[ +pub(super) const OBJC_SYSTEM_SUPERS: &[(&str, &str)] = &[ + ("UISegmentedControl", "UIControl"), ("UIStepper", "UIControl"), ("UIPageControl", "UIControl"), + ("UIDatePicker", "UIControl"), ("UIRefreshControl", "UIControl"), ("UIStackView", "UIView"), + ("UINavigationBar", "UIView"), ("UIToolbar", "UIView"), ("UITabBar", "UIView"), ("UISearchBar", "UIView"), + ("UIVisualEffectView", "UIView"), ("UIActivityIndicatorView", "UIView"), ("UIProgressView", "UIView"), + ("UIPickerView", "UIView"), ("UITableViewHeaderFooterView", "UIView"), + ("UITableViewController", "UIViewController"), ("UICollectionViewController", "UIViewController"), + ("UINavigationController", "UIViewController"), ("UITabBarController", "UIViewController"), + ("UIPageViewController", "UIViewController"), ("UISplitViewController", "UIViewController"), + ("UIAlertController", "UIViewController"), ("UIHostingController", "UIViewController"), ("UIResponder", "NSObject"), ("UIView", "UIResponder"), ("UIViewController", "UIResponder"), ("UIWindow", "UIView"), ("UIControl", "UIView"), ("UIButton", "UIControl"), ("UITextField", "UIControl"), ("UISwitch", "UIControl"), ("UISlider", "UIControl"), ("UIImageView", "UIView"), ("UILabel", "UIView"), ("UIScrollView", "UIView"), @@ -386,14 +395,14 @@ impl KernelResolver { if !dir.is_empty() { owners.extend(self.csharp_project_static_usings(pos_dirname(dir)).iter().cloned()); } - let abs = if dir.is_empty() { self.root_abs.clone() } else { pos_resolve(&self.root_abs, dir) }; for entry in self.csharp_dir_entries(dir) { let lower = entry.to_ascii_lowercase(); if !(lower.ends_with(".csproj") || lower.ends_with(".props")) { continue; } - let Ok(text) = std::fs::read_to_string(format!("{abs}/{entry}")) else { continue }; - for m in re!(r#"(?i)]*(?-u:\b)Static\s*=\s*"true""#).captures_iter(&text) { + let rel = if dir.is_empty() { entry } else { format!("{dir}/{entry}") }; + let Some(text) = self.read_file(&rel) else { continue }; + for m in re!(r#"(?i)]*(?-u:\b)Static\s*=\s*"true""#).captures_iter(text.text()) { owners.insert(m[1].rsplit('.').next().unwrap_or("").to_string()); } } diff --git a/codegraph-kernel/src/resolve/method_call.rs b/codegraph-kernel/src/resolve/method_call.rs index 1e14ac2222..3e14e602bf 100644 --- a/codegraph-kernel/src/resolve/method_call.rs +++ b/codegraph-kernel/src/resolve/method_call.rs @@ -350,6 +350,9 @@ impl KernelResolver { McShape::Done(res) => return Ok(res), }; + 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)); } + } let bindings = self.bindings(&r.file_path)?; let binding = innermost_binding(&bindings, &object_or_class, Some(r.line)).cloned(); @@ -392,11 +395,14 @@ impl KernelResolver { } // TS passes requireReceiverEvidence (=true) as preserveQualifiedName // to both inferrers here — qualified names stay intact. - let mut inferred = if r.language == "cpp" { + let mut inferred = if matches!(r.language.as_str(), "java" | "kotlin" | "csharp") && self.explicit_this_receiver(&object_or_class, &method_name, r) { + self.infer_declared_member_receiver_type(&format!("this.{object_or_class}"), r)? + } else if r.language == "cpp" { self.infer_cpp_receiver_type(&object_or_class, r, 0, true)? } else { probe!(r, "mc:infer-local", self.infer_local_receiver_type(&object_or_class, &site, true)?) }; + if inferred.is_none() { inferred = self.infer_declared_member_receiver_type(&object_or_class, r)?; } if inferred.is_none() && r.language == "go" { if let Some(c) = self.match_go_factory_receiver(&object_or_class, &method_name, r)? { return Ok(Some(c)); @@ -433,6 +439,7 @@ impl KernelResolver { } } if let Some(t) = inferred.take() { + let Some(t) = self.inferred_member_type_bound(&t, r)? else { return Ok(None) }; let mut bsite = r.clone(); if let Some(b) = &binding { bsite.line = b.line; @@ -528,6 +535,9 @@ impl KernelResolver { McShape::Done(res) => return Ok(res), }; + 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)); } + } if inferable { // No binding anchor under requireReceiverEvidence=false — the // inferrers run at the ref's own site with qualified names @@ -539,7 +549,10 @@ impl KernelResolver { }; // guarded/gofactory/iteration are evidence-gated in TS and // never run here; mc-await still does. - let mut inferred = inferred; + let mut inferred = if matches!(r.language.as_str(), "java" | "kotlin" | "csharp") && self.explicit_this_receiver(&object_or_class, &method_name, r) { + self.infer_declared_member_receiver_type(&format!("this.{object_or_class}"), r)? + } else { inferred }; + if inferred.is_none() { inferred = self.infer_declared_member_receiver_type(&object_or_class, r)?; } let mut awaited_file: Option = None; if inferred.is_none() && is_esm_family(&r.language) { if let Some(a) = self.infer_esm_awaited_call_type(&object_or_class, r)? { @@ -570,6 +583,10 @@ impl KernelResolver { } } if let Some(t) = inferred { + let Some(t) = self.inferred_member_type_bound(&t, r)? else { return Ok(None) }; + if matches!(r.language.as_str(), "java" | "kotlin" | "csharp") && t.contains('.') { + return self.match_bound_type_member(&t, &method_name, r); + } // Java/Kotlin: the file's import pins WHICH same-named class. let fqn = if r.language == "java" || r.language == "kotlin" { self.imported_fqn_of(&t, r)? @@ -599,6 +616,9 @@ impl KernelResolver { return Ok(Some(c)); } None if awaited_file.is_some() => return Ok(None), + None if matches!(r.language.as_str(), "java" | "kotlin" | "csharp") => { + return self.unique_csharp_extension_method(&t, &method_name, r); + } // A C# or Ruby receiver whose type is a project class // that carries no such method (nor do its supertypes) // is not a call on whichever class does: the name @@ -721,6 +741,29 @@ impl KernelResolver { } } + if r.language == "csharp" { + if let Some(alias) = self.csharp_using_alias(&object_or_class, &r.file_path) { + let name = alias.rsplit('.').next().unwrap_or(&alias); + let owners: Vec<_> = self.nodes_by_name(name)?.iter().filter(|n| n.language == "csharp" + && is_class_like(&n.kind) && n.qualified_name.replace("::", ".") == alias).cloned().collect(); + return match owners.as_slice() { + [only] => self.resolve_method_on_qualified_type(&only.qualified_name, &method_name, r, 0.9, "instance-method"), + _ => Ok(None), + }; + } + } + + // An unbound type name from outside the project cannot name a project method. + let type_name = re!(r"^[A-Z][A-Za-z0-9_]*$").is_match(&object_or_class) + && !matches!(r.language.as_str(), "go" | "c" | "cpp" | "rust" | "cuda" | "metal") + && (r.language != "pascal" || re!(r"^(?:[TEI][A-Z]\w*|Exception)$").is_match(&object_or_class)); + if type_name && !self.nodes_by_name(&object_or_class)?.iter().any(|n| { + same_language_family(&n.language, &r.language) + && (!matches!(r.language.as_str(), "csharp" | "java") + || is_class_like(&n.kind) || matches!(n.kind.as_str(), "enum" | "namespace" | "module")) + }) { return Ok(None); } + if r.language == "csharp" && matches!(object_or_class.as_str(), "string" | "object" | "int" | "long" | "short" | "byte" | "bool" | "char" | "double" | "float" | "decimal" | "uint" | "ulong" | "ushort" | "sbyte") { return Ok(None); } + // Strategy 2 — capitalized receiver (`permissionEngine` → // `PermissionEngine`) against the same class scan. let capitalized = capitalize_first(&object_or_class); @@ -766,6 +809,7 @@ impl KernelResolver { target.retain(|m| m.file_path != r.file_path); narrowed = target.len() != before; } + if !is_test_path(&r.file_path) { target.retain(|n| !is_test_path(&n.file_path)); } // A Vue component's own method is reached as `this.m()` inside it — // never as `e.preventDefault()` on an event, nor // `this.editor.setValue()` on something the component holds. The @@ -780,6 +824,10 @@ impl KernelResolver { }); narrowed |= target.len() != before; } + if super::call_shape::is_std_method(&r.language, &method_name) + && !matches!(object_or_class.as_str(), "self" | "Self" | "this" | "base") { + target.retain(|n| shares_receiver_word(super::call_shape::receiver_link(&object_or_class), n)); + } let target = ⌖ // Nothing types a Ruby, CFML or Objective-C receiver here: the one // method must also belong to something the receiver is named after @@ -818,14 +866,15 @@ impl KernelResolver { })); } if target.len() > 1 { - let receiver_words = split_camel_case(&object_or_class); + let receiver_words = split_camel_case(super::call_shape::receiver_link(&object_or_class)); // Same-file candidates first, so a score tie resolves to the // call site's own file (`score > bestScore` keeps first seen). let ordered = prefer_call_site_file(target.clone(), &r.file_path); let mut best: Option> = None; let mut best_score = 0i64; for m in &ordered { - let class_words = split_camel_case(&m.qualified_name); + let owner = m.qualified_name.rsplit_once("::").map(|(p, _)| p.rsplit([':', '.']).next().unwrap_or("")).unwrap_or(""); + let class_words = split_camel_case(owner); let mut score = receiver_words .iter() .filter(|w| { @@ -834,6 +883,7 @@ impl KernelResolver { .any(|cw| cw.eq_ignore_ascii_case(w)) }) .count() as i64; + if receiver_words.last().zip(class_words.last()).is_some_and(|(a, b)| a.eq_ignore_ascii_case(b)) { score += 1; } if m.language == r.language { score += 1; } @@ -1031,7 +1081,7 @@ impl KernelResolver { // descent: an owner means btm owns the ref (or refuses a // deeper receiver); a miss falls through to br:import. if (r.language == "java" || r.language == "kotlin") && self.resolve_bound_type(root, r, 0)?.is_some() { - return if receiver == root { self.match_bound_type_member(root, method, r) } else { Ok(None) }; + return if receiver == root { self.match_bound_type_member(root, method, r) } else { self.enum_constant_call(receiver, method, r) }; } return Ok(match self.resolve_via_import_member(r)? { Some(c) => { @@ -1197,23 +1247,21 @@ pub(super) struct FactoryInit { /// that function itself: no function body opens between the outermost /// brace and the `return` (`=> {`, `function … {`, `name(…) {` that is not /// `if`/`for`/`while`/`switch`/`catch`/`with`). Control-flow blocks count. -fn own_return_lines(body: &str) -> HashSet { +pub(super) fn own_return_offsets(body: &str) -> HashSet { let code = super::awaited::blank_string_contents(&super::awaited::strip_ts_comments(body)); let return_kw = re!(r"(?-u:\b)return(?-u:\b)"); let returns: Vec = return_kw.find_iter(&code).map(|m| m.start()).collect(); let mut own = HashSet::new(); let mut stack: Vec = Vec::new(); let mut next = 0; - let mut line = 0; for (i, b) in code.bytes().enumerate() { if next < returns.len() && returns[next] == i { if !stack.iter().skip(1).any(|&is_fn| is_fn) { - own.insert(line); + own.insert(i); } next += 1; } match b { - b'\n' => line += 1, b'{' => stack.push(opens_function_body(&code[..i])), b'}' => { stack.pop(); @@ -1224,6 +1272,11 @@ fn own_return_lines(body: &str) -> HashSet { own } +fn own_return_lines(body: &str) -> HashSet { + let code = super::awaited::blank_string_contents(&super::awaited::strip_ts_comments(body)); + own_return_offsets(&code).into_iter().map(|at| code[..at].bytes().filter(|b| *b == b'\n').count()).collect() +} + /// Does a `{` after `prefix` open a function body rather than a block or /// an object literal? fn opens_function_body(prefix: &str) -> bool { @@ -1283,7 +1336,7 @@ mod tests { /// letters or more (`web_push_request` → WebPushRequest, `executor1` → /// Executor, `decodedImage` → UIImage). Two-letter words are class /// prefixes (`SD`, `NS`, `UI`), not names. -fn shares_receiver_word(receiver: &str, method: &KNode) -> bool { +pub(super) fn shares_receiver_word(receiver: &str, method: &KNode) -> bool { let Some(cut) = method.qualified_name.rfind("::") else { return false }; let owner_qn = &method.qualified_name[..cut]; let flat = |w: &str| -> String { diff --git a/codegraph-kernel/src/resolve/mod.rs b/codegraph-kernel/src/resolve/mod.rs index a598324c34..2221a80b69 100644 --- a/codegraph-kernel/src/resolve/mod.rs +++ b/codegraph-kernel/src/resolve/mod.rs @@ -497,11 +497,16 @@ mod file_refs; mod rust_modules; mod awaited; mod iteration; +mod kotlin_receiver; mod this_member; mod member_fn_ref; mod object_literal; mod cpp; mod store; +mod esm_scope; +mod call_shape; +mod kotlin_calls; +mod declared_member; mod live_conn; use self::tables::*; use self::affix::*; @@ -548,6 +553,7 @@ pub struct KernelResolver { import_path_memo: HashMap>, exported_symbol_memo: HashMap>>, sealed_memo: HashMap, + esm_exports_memo: HashMap)>, c_static_memo: HashMap, rust_trait_memo: HashMap, /// javaSupertypesOf, by type name. @@ -561,6 +567,10 @@ pub struct KernelResolver { /// Kotlin supertypes named in a type's head, by type name. kotlin_supers_memo: HashMap>>, /// kotlinFileScope, by file. + csharp_alias_memo: HashMap>>, + declared_member_memo: HashMap<(String, String), Option>, + kotlin_frames_memo: HashMap>>, + kotlin_receiver_types_memo: Option>>, kotlin_scope_memo: HashMap>, /// Python methods named `member` on classes deriving from a class, by /// (class id, member). @@ -712,12 +722,17 @@ impl KernelResolver { import_path_memo: HashMap::new(), exported_symbol_memo: HashMap::new(), sealed_memo: HashMap::new(), + esm_exports_memo: HashMap::new(), c_static_memo: HashMap::new(), rust_trait_memo: HashMap::new(), java_supers_memo: HashMap::new(), php_supers_memo: HashMap::new(), dart_supers_memo: HashMap::new(), dart_hierarchy_memo: HashMap::new(), + csharp_alias_memo: HashMap::new(), + declared_member_memo: HashMap::new(), + kotlin_frames_memo: HashMap::new(), + kotlin_receiver_types_memo: None, kotlin_scope_memo: HashMap::new(), kotlin_supers_memo: HashMap::new(), py_descendants_memo: HashMap::new(), diff --git a/codegraph-kernel/src/resolve/names.rs b/codegraph-kernel/src/resolve/names.rs index bb844b3dcd..ed55c08cb1 100644 --- a/codegraph-kernel/src/resolve/names.rs +++ b/codegraph-kernel/src/resolve/names.rs @@ -257,7 +257,7 @@ impl KernelResolver { if !is_esm_family(&candidate.language) { return Ok(true); } - self.is_sealed_module(&candidate.file_path).map(|s| !s) + Ok(!self.is_sealed_module(&candidate.file_path)? && !self.is_unexported_module_binding(candidate)?) } /// isVisibleAcrossFiles (name-matcher.ts). @@ -731,6 +731,7 @@ impl KernelResolver { // A bare Dart call means the nearest member of the hierarchy around it. let candidates = self.nearest_dart_members(candidates, r)?; + let candidates = self.nearest_swift_members(candidates, r)?; // C/C++ call-site form. let Some(cpp_form) = self.apply_cpp_call_site_form(r, candidates)? else { diff --git a/codegraph-kernel/src/resolve/pipeline.rs b/codegraph-kernel/src/resolve/pipeline.rs index 5426011c89..0479c6cea3 100644 --- a/codegraph-kernel/src/resolve/pipeline.rs +++ b/codegraph-kernel/src/resolve/pipeline.rs @@ -431,10 +431,11 @@ impl KernelResolver { None }); } + let mut cand = cand; + self.cap_kotlin_chain_confidence(&mut cand, r)?; if !is_inheritance_ref(&r.reference_kind) { return Ok(Some(cand)); } - let mut cand = cand; if !is_supertype_target(&cand.node) { // `export const IFoo = createDecorator(…)` beside `export // interface IFoo` (VS Code's service pattern): the strategy found @@ -764,6 +765,15 @@ impl KernelResolver { )) } + fn cap_kotlin_chain_confidence(&mut self, cand: &mut KCand, r: &ResolveRefIn) -> Res<()> { + if cand.confidence > 0.7 && (self.kotlin_chain_evidence(&cand.node, r)? == Some(super::call_shape::KotlinChainEvidence::Heuristic) + || (r.language == "kotlin" && r.reference_kind == "calls" && super::call_shape::is_std_method("kotlin", &r.reference_name) + && self.kotlin_call_receiver_type(r)?.is_some_and(|hit| hit.heuristic))) { + cand.confidence = 0.7; + } + Ok(()) + } + /// First-max on strict `>` over `cands` (non-empty), the TS /// candidates.reduce, then the target-kind gate and `finish`. Under active /// frameworks the reported list keeps the ORIGINAL candidate order so the @@ -771,6 +781,9 @@ impl KernelResolver { /// gated-out winner still reports it, since a framework candidate may win /// the merged first-max on the TS side. pub(super) fn settle(&mut self, r: &ResolveRefIn, mut cands: Vec) -> Res { + for cand in &mut cands { + self.cap_kotlin_chain_confidence(cand, r)?; + } let reported = self .frameworks_active .then(|| cands.iter().map(KernelCandidateOut::from).collect::>()); diff --git a/codegraph-kernel/src/resolve/receivers.rs b/codegraph-kernel/src/resolve/receivers.rs index 3df96d0019..7c92ac4d28 100644 --- a/codegraph-kernel/src/resolve/receivers.rs +++ b/codegraph-kernel/src/resolve/receivers.rs @@ -104,6 +104,9 @@ impl KernelResolver { if guard1_tail_re().is_match(rest) && !nullable_union_tail_re().is_match(rest) { continue; } + } else if pat.guard == 4 { + // An assignment statement, not a trailing keyword argument. + if line[m.end..].split('\n').next().is_some_and(|rest| rest.trim_end().ends_with(',')) { continue; } } else if pat.guard == 3 { // `(?!\s*\()` — a declarator followed by `(` is a function. if cpp_call_opener_re().is_match(&line[m.end..]) { diff --git a/codegraph-kernel/src/resolve/store.rs b/codegraph-kernel/src/resolve/store.rs index f47fa7913d..fe08e0231e 100644 --- a/codegraph-kernel/src/resolve/store.rs +++ b/codegraph-kernel/src/resolve/store.rs @@ -58,6 +58,71 @@ fn source_before(lines: &[String], from: usize, line: i64, column: i64) -> Strin } impl KernelResolver { + /// A destructured callable returned by an imported or local factory. + pub(super) fn match_destructured_call_result(&mut self, r: &ResolveRefIn) -> Res> { + let Some(lines) = self.read_file(&r.file_path) else { return Ok(None) }; + let code = blank_string_contents(&strip_ts_comments(&source_before(&lines, 0, r.line, r.column))); + let binding = re!(r"(?-u:\b)(?:const|let|var)\s*\{([^{}]*)\}\s*=\s*(?:await\s+)?([A-Za-z_$][\w$]*)\s*(?:<[^<>()]*>)?\s*\("); + let call_scope = brace_stack(&code, code.len()); + let matches: Vec<_> = binding.captures_iter(&code).map(|m| { + let span = m.get(0).unwrap(); + (span.start(), span.end(), m[1].to_string(), m[2].to_string()) + }).collect(); + for (at, end, names, callee_name) in matches.into_iter().rev() { + let key = names.split(',').find_map(|part| { + let mut pair = part.split(':'); + let key = pair.next()?.trim().split('=').next()?.trim(); + let local = pair.next().unwrap_or(key).trim().split('=').next()?.trim(); + (local == r.reference_name && re!(r"^[A-Za-z_$][\w$]*$").is_match(key)).then(|| key.to_string()) + }); + let Some(key) = key else { continue }; + if !in_scope(&code, at, &call_scope) { continue } + if declared_in_scope(&code, end, &r.reference_name, &call_scope) { return Ok(None) } + let mut binding_site = r.clone().naming(&callee_name, "calls"); + binding_site.line = code[..at].bytes().filter(|b| *b == b'\n').count() as i64 + 1; + let mappings = self.import_mappings(&r.file_path)?; + let imported = mappings.iter().any(|m| m.local_name == callee_name); + let callee = if imported { + self.resolve_via_import(&binding_site)?.map(|c| c.node) + } else { + let bindings = self.bindings(&r.file_path)?; + let bound = innermost_binding(&bindings, &callee_name, Some(binding_site.line)); + self.nodes_in_file(&r.file_path)?.iter().find(|n| n.name == callee_name + && matches!(n.kind.as_str(), "function" | "constant" | "variable") + && bound.is_none_or(|b| b.node_id.as_deref() == Some(n.id.as_str()))).cloned() + }; + let Some(callee) = callee else { return Ok(None) }; + if !same_language_family(&callee.language, &r.language) { return Ok(None) } + let Some(source) = self.read_file(&callee.file_path) else { return Ok(None) }; + let lo = (callee.start_line - 1).max(0) as usize; + let hi = (callee.end_line.max(0) as usize).min(source.len()); + let text = blank_string_contents(&strip_ts_comments(&source.get(lo..hi).unwrap_or_default().join("\n"))); + let own = super::method_call::own_return_offsets(&text); + let returns: Vec<_> = re!(r"(?-u:\b)return\s*\{([^{}]*)\}").captures_iter(&text).filter(|m| own.contains(&m.get(0).unwrap().start())).map(|m| m[1].to_string()).collect(); + if returns.len() != 1 { return Ok(None) } + let target_name = returns[0].split(',').find_map(|part| { + let mut pair = part.split(':'); + let returned_key = pair.next()?.trim(); + let value = pair.next().unwrap_or(returned_key).trim(); + (returned_key == key && re!(r"^[A-Za-z_$][\w$]*$").is_match(value)).then(|| value.to_string()) + }); + let Some(target_name) = target_name else { return Ok(None) }; + let nodes = self.nodes_in_file(&callee.file_path)?; + let callable = |n: &&Arc| n.name == target_name && n.id != callee.id + && matches!(n.kind.as_str(), "function" | "method" | "constant" | "variable"); + let mut inner: Vec<_> = nodes.iter().filter(callable).filter(|n| range_within(n, &callee) + && !nodes.iter().any(|f| f.id != callee.id && f.id != n.id + && matches!(f.kind.as_str(), "function" | "method") && range_within(f, &callee) && range_within(n, f))).cloned().collect(); + if inner.is_empty() { + inner = nodes.iter().filter(callable).filter(|n| !n.qualified_name.contains("::") + && !nodes.iter().any(|f| f.id != n.id && matches!(f.kind.as_str(), "function" | "method") && range_within(n, f))).cloned().collect(); + } + if inner.len() != 1 { return Ok(None) } + return Ok(Some(KCand { node: inner.remove(0), confidence: 0.85, resolved_by: "instance-method" })); + } + Ok(None) + } + /// matchJsStoreBindingCall — a bare JS call bound to a store action. pub(super) fn match_js_store_binding_call(&mut self, r: &ResolveRefIn) -> Res> { if !self.is_bare_js_call(r)? { @@ -66,6 +131,9 @@ impl KernelResolver { if let Some(c) = self.match_destructured_store_call(r)? { return Ok(Some(c)); } + if let Some(c) = self.match_destructured_call_result(r)? { + return Ok(Some(c)); + } self.match_selected_store_call(r) } diff --git a/codegraph-kernel/src/resolve/tables.rs b/codegraph-kernel/src/resolve/tables.rs index 5681592caf..e02a702c2f 100644 --- a/codegraph-kernel/src/resolve/tables.rs +++ b/codegraph-kernel/src/resolve/tables.rs @@ -378,6 +378,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-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:"), @@ -395,7 +396,7 @@ pub(super) fn framework_claims_reference(framework: &str, name: &str) -> bool { } "vue" => name.starts_with("nuxt-page:"), "aspnet" | "express" | "expo-modules" | "expo-modules-js" | "fabric-view" | "fastapi" | "flask" - | "go" | "goframe" | "http-routing" | "nestjs" | "nuxt" | "react" | "react-router-files" | "redwood" + | "go" | "goframe" | "http-routing" | "nestjs" | "nuxt" | "react-router-files" | "redwood" | "rust" | "svelte" | "swiftui" | "uikit" => false, "vapor" => vapor_handler_re().is_match(name), @@ -1126,3 +1127,16 @@ mod tests { assert_eq!(path, Some(format!("{}leaf", "m::".repeat(depth)))); } } + +/// Test filenames and source sets, matching the shared query surface's test-path contract. +pub(super) fn is_test_path(path: &str) -> bool { + let filename = path.rsplit('/').next().unwrap_or(path); + let lower = path.to_ascii_lowercase(); + let name = filename.to_ascii_lowercase(); + name.starts_with("test_") || name.starts_with("test.") + || re!(r"[._-](?:test|tests|spec|specs)\.[a-z0-9]+$").is_match(&name) + || re!(r"(?:Test|Tests|TestCase|Tester|Spec|Specs)\.[A-Za-z0-9]+$").is_match(filename) + || re!(r"(?:^|/)(?:tests?|__tests__|specs?|testlib|testing|e2e)/").is_match(&lower) + || re!(r"(?:^|/)[A-Za-z0-9]*(?:Test|Tests|Spec)/").is_match(path) + || re!(r"(?:^|/)(?:[\w.]+[-_]test(?:s|ing)?|testdata|testutils?|test[-_]utils?|fakes?|mocks?|__mocks__|stubs)/").is_match(&lower) +} diff --git a/docs/design/framework-coverage.md b/docs/design/framework-coverage.md index 7b39ab9014..b09eca5c67 100644 --- a/docs/design/framework-coverage.md +++ b/docs/design/framework-coverage.md @@ -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's `paths.x.path` constants and `lazy` routes are not read yet | +| 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 remain unsupported | | 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) | diff --git a/site/src/content/docs/core-concepts/resolution.md b/site/src/content/docs/core-concepts/resolution.md index cb3ae101a8..00a7eb65d8 100644 --- a/site/src/content/docs/core-concepts/resolution.md +++ b/site/src/content/docs/core-concepts/resolution.md @@ -10,7 +10,7 @@ Extraction produces nodes and raw edges; **resolution** turns names into real co After parsing, CodeGraph resolves: - **Imports** → the source files they point at, including tsconfig/jsconfig path aliases (each file uses its nearest config's `paths`), workspace packages (an entry that points at uncommitted `dist/` output maps back to its `src/` file) and cargo workspace members. -- **Calls** → their definitions, by import resolution and name matching. A member call such as `i18n.baseText()` resolves through the receiver's type: a `new` or type annotation, or the value a factory like `useI18n()` returns. Vue, Svelte and Astro single-file components get the same treatment as TypeScript files. A name matches a definition only in its own case, except in PHP, Pascal, CFML, COBOL and VB.NET, whose names ignore case, and a `super` call in an override goes to the parent's method, never back to the override itself. A call written without a receiver reaches only what is in scope there: in Java, Dart, Ruby and CFML a method of the class it is written in or of that class's supertypes and mixins, in C# a member of the types around it, their base types or a `using static` type, in Objective-C a message to `self` or `super` a method of the sender's class hierarchy, and in Kotlin a top-level function from the file's own package or one it imports. A VB.NET member access reaches a member only through `Me`, `MyBase`, `MyClass` or the member's own type or module. A Lua `local`, or a variable at the top of an R test file, is not visible from other files. +- **Calls** → their definitions, by import resolution and name matching. A member call such as `i18n.baseText()` resolves through the receiver's type: a `new` or type annotation, or the value a factory like `useI18n()` returns. Vue, Svelte and Astro single-file components get the same treatment as TypeScript files. A name matches a definition only in its own case, except in PHP, Pascal, CFML, COBOL and VB.NET, whose names ignore case, and a `super` call in an override goes to the parent's method, never back to the override itself. A call written without a receiver reaches only what is in scope there: in Java, Dart, Ruby and CFML a method of the class it is written in or of that class's supertypes and mixins, in C# a member of the types around it, their base types or a `using static` type, in Objective-C a message to `self` or `super` a method of the sender's class hierarchy, and in Kotlin a top-level function from the file's own package or one it imports, or a member of an implicit class, extension or DSL receiver. A VB.NET member access reaches a member only through `Me`, `MyBase`, `MyClass` or the member's own type or module. A Lua `local`, or a variable at the top of an R test file, is not visible from other files. - **Inheritance** → `extends` / `implements` between types. ## Framework awareness @@ -28,3 +28,9 @@ Static parsing misses computed and indirect calls, so flows can break at dynamic - Interface → implementation dispatch Every synthesized edge is marked `provenance: 'heuristic'` with the site that wired it, and is shown inline wherever a path crosses it. + +Java, C# and Kotlin field or property receivers use their declared types, including inherited generic members. A declared type outside the project prevents a guess at an unrelated project method. Scala local binders and class ancestry, Swift implicit receivers and argument labels, and Rust/Go bare versus chained call forms constrain name matches. Names destructured from a composable or hook result resolve to the functions it returns under those keys. Unexported ESM bindings stay local unless explicitly exposed. + +Kotlin chains use the callee position and declared return type, including nested and multiline calls; imported return-type hypotheses for standard method names keep confidence at most 0.7 when the receiver type is unknown. + +Kotlin receiver inference follows bounded chains of declared returns and verified receiver-preserving methods. Properties initialized by typed factory calls retain compatible imported extensions. diff --git a/site/src/content/docs/guides/framework-routes.md b/site/src/content/docs/guides/framework-routes.md index d1d969fb56..e3331bac5d 100644 --- a/site/src/content/docs/guides/framework-routes.md +++ b/site/src/content/docs/guides/framework-routes.md @@ -28,9 +28,9 @@ CodeGraph detects web-framework routing files and emits `route` nodes linked by | **Play** | `GET`/`POST`/… verb routes in `conf/routes` → `Controller.method` actions (Scala + Java), including projects kept in subdirectories | | **Gin / chi / gorilla / mux** | `r.GET(...)`, `router.HandleFunc(...)` | | **Axum / actix / Rocket** | `.route("/x", get(handler))` | -| **ASP.NET** | `[HttpGet("/x")]` attributes on action methods | -| **Vapor** | `app.get("x", use: handler)`; `use: Controller.show` links to that type's `show` (nested types and extensions included), `use: self.index` to the collection's own | -| **React Router / Remix** | JSX/data-router pages; literal framework-mode arrays; default Remix and registered `flatRoutes()` file pages, linked to named default components | +| **ASP.NET** | `[HttpGet("/x")]` attributes on action methods and FastEndpoints `Configure()` verb calls | +| **Vapor** | `app.get("x", use: handler)` and closure handlers; `use: Controller.show` links to that type's `show` (nested types and extensions included), `use: self.index` to the collection's own | +| **React Router / Remix** | JSX/data-router pages with nested children, constant paths, `Component` and lazy module exports; literal framework-mode arrays; default Remix and registered `flatRoutes()` file pages, linked to named default components | | **SvelteKit** | Route component nodes, named by URL: `(group)` folders dropped, `[id=matcher]` read as `:id` | | **TanStack Router / Start** | Page routes plus literal `server.handlers` method tables and `createHandlers` callbacks on exported file routes; middleware is excluded from handler links | | **Next.js** | App Router and Pages Router pages; `app/api/**/route.ts` method exports and `pages/api/**` default handlers | diff --git a/site/src/content/docs/reference/languages.md b/site/src/content/docs/reference/languages.md index be613eef84..5b7996a057 100644 --- a/site/src/content/docs/reference/languages.md +++ b/site/src/content/docs/reference/languages.md @@ -34,3 +34,5 @@ Language support is automatic from the file extension — there's nothing to con | Markdown | `.md`, `.mdx`, `.markdown` | Documentation structure (headings, sections, local links, selected table rows/list items, shell command references) | Markdown files use a dedicated documentation extractor. `.mdx` files receive the same documentation indexing; embedded JSX and JavaScript are not parsed as MDX code. + +JavaScript files with a Flow pragma use the TSX grammar after Flow-only syntax is blanked, preserving source locations. diff --git a/src/extraction/grammars.ts b/src/extraction/grammars.ts index 053c1ccb59..b3a700f206 100644 --- a/src/extraction/grammars.ts +++ b/src/extraction/grammars.ts @@ -325,6 +325,11 @@ export function detectLanguage(filePath: string, source?: string, overrides?: Re if (isErlangAppFile(filePath)) return 'erlang'; const lang = (overrides && overrides[ext]) || EXTENSION_MAP[ext] || 'unknown'; + // A Flow-typed `.js` (`// @flow` in its leading comments) parses as TSX: + // the JavaScript grammar can't read its annotations — `render(): React.Node` + // cut a class short — and TypeScript's syntax covers most of Flow's. + if ((lang === 'javascript' || lang === 'jsx') && source && hasFlowPragma(source)) return 'tsx'; + // .h files could be C, C++, or Objective-C — check source content if (lang === 'c' && ext === '.h' && source) { if (looksLikeCpp(source)) return 'cpp'; @@ -334,6 +339,13 @@ export function detectLanguage(filePath: string, source?: string, overrides?: Re return lang; } +/** Whether a JavaScript file's leading comments carry Flow's `@flow` pragma (and not `@noflow`). */ +export function hasFlowPragma(source: string): boolean { + const head = source.slice(0, 4096).replace(/^#![^\n]*\n/, ''); + const lead = /^(?:\s*(?:\/\/[^\n]*|\/\*[\s\S]*?\*\/))*/.exec(head)?.[0] ?? ''; + return /@flow\b/.test(lead) && !/@noflow\b/.test(lead); +} + /** * A class/struct BASE CLAUSE — `struct Derived : Base {`, `class Foo final : * public Bar, private Baz {`, `struct D : ns::B {` — which is never valid diff --git a/src/extraction/index.ts b/src/extraction/index.ts index 90b243fdde..4d198534cc 100644 --- a/src/extraction/index.ts +++ b/src/extraction/index.ts @@ -809,6 +809,8 @@ export function preloadLanguagesForFiles( overrides?: Record ): Language[] { const languages = [...new Set(files.map((f) => detectLanguage(f, undefined, overrides)))]; + // A Flow-typed `.js` is read with the TSX grammar (see detectLanguage). + if ((languages.includes('javascript') || languages.includes('jsx')) && !languages.includes('tsx')) languages.push('tsx'); if (languages.includes('c')) { for (const ambiguous of ['cpp', 'objc'] as const) { if (!languages.includes(ambiguous)) languages.push(ambiguous); diff --git a/src/extraction/languages/csharp.ts b/src/extraction/languages/csharp.ts index 2619006d59..ed09eaff7e 100644 --- a/src/extraction/languages/csharp.ts +++ b/src/extraction/languages/csharp.ts @@ -52,6 +52,9 @@ function extractCsharpReturnType(node: SyntaxNode, source: string): string | und return last; } +/** Where a C# declaration with no access modifier defaults to `internal`. */ +const CSHARP_TYPE_SCOPES = new Set(['compilation_unit', 'namespace_declaration', 'file_scoped_namespace_declaration']); + export const csharpExtractor: LanguageExtractor = { preParse: blankCsharpPreprocessorDirectives, functionTypes: [], @@ -110,7 +113,12 @@ export const csharpExtractor: LanguageExtractor = { if (text === 'internal') return 'internal'; } } - return 'private'; // C# defaults to private + // No modifier: an interface's members are public, a type declared in a + // namespace (or the file) is internal, and a class or struct member is private. + const container = node.parent?.type === 'declaration_list' ? node.parent.parent : node.parent; + if (container?.type === 'interface_declaration') return 'public'; + if (!container || CSHARP_TYPE_SCOPES.has(container.type)) return 'internal'; + return 'private'; }, isStatic: (node) => { for (let i = 0; i < node.childCount; i++) { diff --git a/src/extraction/languages/kotlin.ts b/src/extraction/languages/kotlin.ts index 15e2afd618..5d8e8e0b68 100644 --- a/src/extraction/languages/kotlin.ts +++ b/src/extraction/languages/kotlin.ts @@ -309,9 +309,25 @@ export function rewriteKotlinCallMemberAssignments(source: string): string { return out ? out.join('') : source; } -/** Every offset-preserving rewrite the Kotlin grammar needs before parsing. */ +/** + * A primary constructor written on the line after its class name — + * `expect open class ByteString` / `internal constructor(data: ByteArray) : + * Comparable {` (okio, and Kotlin Multiplatform code generally) — + * with a supertype list after it: the grammar reads it as two statements and + * drops the class, its members coming out as loose functions. Blanking the + * constructor's modifiers and keyword (`(data: ByteArray) : …` parses) keeps + * the class, its supertypes and every offset; only the visibility is lost. + */ +const SPLIT_PRIMARY_CONSTRUCTOR = + /(\bclass\s+[A-Za-z_]\w*(?:\s*<[^<>{}]*(?:<[^<>{}]*>[^<>{}]*)*>)?[ \t]*\r?\n(?:[ \t]*\/\/[^\n]*\n)*[ \t]*)((?:(?:public|private|protected|internal|actual|expect)\s+|@[\w.]+(?:\([^)]*\))?\s+)*constructor)(?=\s*\()/g; + +export function joinKotlinSplitConstructors(source: string): string { + if (!source.includes('constructor')) return source; + return source.replace(SPLIT_PRIMARY_CONSTRUCTOR, (_m, head: string, ctor: string) => head + ctor.replace(/[^\r\n]/g, ' ')); +} + export function preParseKotlin(source: string): string { - return rewriteKotlinCallMemberAssignments(blankKotlinQualifiedReceivers(source)); + return rewriteKotlinCallMemberAssignments(joinKotlinSplitConstructors(blankKotlinQualifiedReceivers(source))); } export const kotlinExtractor: LanguageExtractor = { diff --git a/src/extraction/languages/typescript.ts b/src/extraction/languages/typescript.ts index 1a8a4e024d..c3bcc4d639 100644 --- a/src/extraction/languages/typescript.ts +++ b/src/extraction/languages/typescript.ts @@ -1,6 +1,7 @@ import { getNodeText, getChildByField } from '../tree-sitter-helpers'; import type { LanguageExtractor } from '../tree-sitter-types'; import type { TreeNode as SyntaxNode } from '../parse-tree'; +import { hasFlowPragma } from '../grammars'; /** * A TS/JS class field (`public_field_definition` / `field_definition`) is a @@ -38,7 +39,26 @@ export function classifyTsClassMember(node: SyntaxNode): 'method' | 'property' { return 'property'; } +/** + * Flow's own syntax, blanked to spaces (offsets survive) so a Flow file reads + * as TSX: exact object types `{| a: T |}`, a maybe type's `?` (`x: ?string`, + * `Array`), an inexact object's `...`, `import typeof`, `opaque type`. Only files with the `@flow` + * pragma — the ones `detectLanguage` sends here from `.js`. + */ +export function blankFlowSyntax(source: string, filePath?: string): string { + if (!filePath || !/\.(?:jsx?|mjs|cjs)$/.test(filePath) || !hasFlowPragma(source)) return source; + return source + .replace(/\{\|/g, '{ ') + .replace(/\|\}/g, ' }') + .replace(/([:<,]\s*)\?(?=[\w$({[])/g, '$1 ') + .replace(/\bimport\s+typeof\b/g, (m) => 'import' + ' '.repeat(m.length - 'import'.length)) + .replace(/\bopaque(?=\s+type\b)/g, ' ') + // An inexact object type's bare `...` (`{a: T, ...}`) — never a value spread. + .replace(/\.\.\.(?=\s*[},])/g, ' '); +} + export const typescriptExtractor: LanguageExtractor = { + preParse: blankFlowSyntax, functionTypes: ['function_declaration', 'generator_function_declaration', 'arrow_function', 'function_expression', 'generator_function'], classTypes: ['class_declaration', 'abstract_class_declaration'], // `method_signature` is the interface/type-literal form of a method; without it diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index fa95361e53..74c274a401 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -4744,6 +4744,7 @@ export class TreeSitterExtractor { // Get the function/method being called let calleeName = ''; + let callAt = node; // Java/Kotlin method_invocation has 'object' + 'name' fields instead of 'function' // PHP member_call_expression has 'object' + 'name', scoped_call_expression has 'scope' + 'name' @@ -5015,6 +5016,7 @@ export class TreeSitterExtractor { } } if (property) { + if (this.language === 'kotlin') callAt = property; const methodName = getNodeText(property, this.source); // Include receiver name for qualified resolution (e.g., console.print → "console.print") // This helps the resolver distinguish method calls from bare function calls @@ -5347,8 +5349,8 @@ export class TreeSitterExtractor { fromNodeId: callerId, referenceName: calleeName, referenceKind: 'calls', - line: node.startPosition.row + 1, - column: node.startPosition.column, + line: (this.language === 'kotlin' && !calleeName.includes('.') ? callAt : node).startPosition.row + 1, + column: (this.language === 'kotlin' && !calleeName.includes('.') ? callAt : node).startPosition.column, }); } } diff --git a/src/resolution/frameworks/csharp.ts b/src/resolution/frameworks/csharp.ts index 765acbd658..81e7fd73a9 100644 --- a/src/resolution/frameworks/csharp.ts +++ b/src/resolution/frameworks/csharp.ts @@ -6,7 +6,7 @@ import { Node } from '../../types'; import { FrameworkResolver, UnresolvedRef, ResolvedRef, ResolutionContext } from '../types'; -import { stripCommentsForRegex } from '../strip-comments'; +import { blankStringContents, stripCommentsForRegex } from '../strip-comments'; export const aspnetResolver: FrameworkResolver = { name: 'aspnet', @@ -191,7 +191,9 @@ export const aspnetResolver: FrameworkResolver = { // Minimal APIs: app.MapGet("/path", handler) const minimalRegex = /\.Map(Get|Post|Put|Patch|Delete)\s*\(\s*"([^"]+)"\s*,\s*([^,)]+)/g; while ((match = minimalRegex.exec(safe)) !== null) { - const [, verb, routePath, handlerExpr] = match; + const [, verb, rawPath, handlerExpr] = match; + // `app.MapGet("api/todos", …)` serves `/api/todos`. + const routePath = joinCsPath('', rawPath!); const method = verb!.toUpperCase(); const line = safe.slice(0, match.index).split('\n').length; @@ -272,45 +274,122 @@ export const aspnetResolver: FrameworkResolver = { }); } + // FastEndpoints: an endpoint class (`: Endpoint`, + // `EndpointWithoutRequest`, `Ep.Req<…>.Res<…>`) declares its verb and path in + // `Configure()` — `Get("/Contributors")`, `Post(CreateContributorRequest.Route)` + // — and handles the request in its own `HandleAsync` / `ExecuteAsync`. A + // constant path (`X.Route`, usually in the request's own file) is read in + // postExtract; until then the route is named by the expression. + const structural = blankStringContents(safe); + const endpointClass = /\bclass\s+([A-Za-z_]\w*)[^{;]*?:\s*(?:FastEndpoints\.)?(?:Endpoint(?:WithoutRequest|WithoutResponse)?\b|Ep\.)/g; + while ((match = endpointClass.exec(safe)) !== null) { + const open = safe.indexOf('{', match.index + match[0].length); + const end = closingBrace(structural, open); + if (open < 0 || end < 0) continue; + const body = safe.slice(open + 1, end); + const configure = /\bvoid\s+Configure\s*\(\s*\)\s*\{/.exec(body); + if (!configure) continue; + const handler = /\b(HandleAsync|ExecuteAsync)\s*\(/.exec(body)?.[1]; + const verbRegex = /\b(Get|Post|Put|Patch|Delete)\s*\(\s*([^;]*?)\s*\)\s*;/g; + const configureOpen = open + 1 + configure.index + configure[0].length - 1; + const configureEnd = closingBrace(structural, configureOpen); + if (configureEnd < 0) continue; + const configureBody = safe.slice(configureOpen + 1, configureEnd); + let verb: RegExpExecArray | null; + while ((verb = verbRegex.exec(configureBody)) !== null) { + const method = verb[1]!.toUpperCase(); + const args = verb[2]!; + // `$"/{nameof(Project)}s"` is `/Projects`. + const literals = [...args.matchAll(/"([^"]+)"/g)].map((l) => joinCsPath('', l[1]!.replace(/\{\s*nameof\s*\(\s*(\w+)\s*\)\s*\}/g, '$1'))); + const constant = /^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/.exec(args); + const targets = literals.length > 0 ? literals.map((p) => ({ name: p, key: p })) : constant ? [{ name: `${constant[1]}.${constant[2]}`, key: `const:${constant[1]}.${constant[2]}` }] : []; + const line = safe.slice(0, configureOpen + 1 + verb.index).split('\n').length; + for (const t of targets) { + const id = `route:${filePath}:${line}:${method}:${t.name}`; + nodes.push({ + id, kind: 'route', name: `${method} ${t.name}`, + qualifiedName: `${filePath}::fastendpoint:${method}:${t.key}`, + filePath, startLine: line, endLine: line, startColumn: 0, endColumn: 0, language: 'csharp', updatedAt: now, + }); + if (handler) references.push({ fromNodeId: id, referenceName: handler, referenceKind: 'references', line, column: 0, filePath, language: 'csharp' }); + } + } + } + return { nodes, references }; }, - /** - * The endpoint-group prefix convention, read once from the app: the - * `MapGroup($"/api/{groupName}")` that registers every `IEndpointGroup` - * (or `EndpointGroupBase`) under a head — `/api/` — before the class name. - * A group route extracted as `POST /TodoItems` becomes `POST /api/TodoItems`; - * a class with its own `RoutePrefix` literal already has its path. Idempotent: - * `qualifiedName` keeps the group and the sub-path. - */ + /** Route names only the whole repository can give: FastEndpoints constant paths, endpoint-group prefixes. */ postExtract(context: ResolutionContext): Node[] { - let head: string | null = null; - let looked = 0; - for (const file of context.getAllFiles()) { - if (!file.endsWith('.cs')) continue; - const content = context.readFile(file); - if (!content || !content.includes('MapGroup')) continue; - if (++looked > 400) break; - const m = /\$"([^"{]*)\{\s*(?:groupName|type\.Name|name|prefix)\s*\}"/.exec(content) ?? /MapGroup\(\s*\$"([^"{]*)\{/.exec(content); + return [...fastEndpointConstantRoutes(context), ...endpointGroupRoutes(context)]; + }, +}; + +/** + * A FastEndpoints route named by a constant (`Post(CreateContributorRequest.Route)`) + * gets the constant's value: `public const string Route = "/Contributors";` in + * that class, wherever it is declared. Idempotent — the qualified name keeps the + * expression. + */ +function fastEndpointConstantRoutes(context: ResolutionContext): Node[] { + const updates: Node[] = []; + for (const route of context.getNodesByKind('route')) { + if (route.language !== 'csharp') continue; + const q = /::fastendpoint:([A-Z]+):const:([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/.exec(route.qualifiedName); + if (!q) continue; + const [, method, owner, field] = q; + let value: string | null = null; + for (const decl of context.getNodesByName(owner!)) { + if (decl.language !== 'csharp' || (decl.kind !== 'class' && decl.kind !== 'struct')) continue; + const text = context.readFile(decl.filePath) ?? ''; + const m = new RegExp(`\\b(?:const\\s+string|static\\s+readonly\\s+string|static\\s+string)\\s+${field}\\s*=\\s*"([^"]+)"`).exec(text); if (m) { - head = m[1]!; + value = m[1]!; break; } } - if (!head || head === '/' || head === '') return []; - const updates: Node[] = []; - for (const route of context.getNodesByKind('route')) { - if (route.language !== 'csharp') continue; - const q = /::group:([A-Za-z_]\w*):([A-Z]+):(.*)$/.exec(route.qualifiedName); - if (!q) continue; - const content = context.readFile(route.filePath); - if (content && /\bRoutePrefix\s*(?:=>|=)\s*"/.test(content)) continue; - const name = `${q[2]} ${joinCsPath(head.replace(/\/+$/, '') + '/' + q[1], q[3]!)}`; - if (name !== route.name) updates.push({ ...route, name }); + if (!value) continue; + const name = `${method} ${joinCsPath('', value)}`; + if (name !== route.name) updates.push({ ...route, name }); + } + return updates; +} + +/** + * The endpoint-group prefix convention, read once from the app: the + * `MapGroup($"/api/{groupName}")` that registers every `IEndpointGroup` + * (or `EndpointGroupBase`) under a head — `/api/` — before the class name. + * A group route extracted as `POST /TodoItems` becomes `POST /api/TodoItems`; + * a class with its own `RoutePrefix` literal already has its path. Idempotent: + * `qualifiedName` keeps the group and the sub-path. + */ +function endpointGroupRoutes(context: ResolutionContext): Node[] { + let head: string | null = null; + let looked = 0; + for (const file of context.getAllFiles()) { + if (!file.endsWith('.cs')) continue; + const content = context.readFile(file); + if (!content || !content.includes('MapGroup')) continue; + if (++looked > 400) break; + const m = /\$"([^"{]*)\{\s*(?:groupName|type\.Name|name|prefix)\s*\}"/.exec(content) ?? /MapGroup\(\s*\$"([^"{]*)\{/.exec(content); + if (m) { + head = m[1]!; + break; } - return updates; - }, -}; + } + if (!head || head === '/' || head === '') return []; + const updates: Node[] = []; + for (const route of context.getNodesByKind('route')) { + if (route.language !== 'csharp') continue; + const q = /::group:([A-Za-z_]\w*):([A-Z]+):(.*)$/.exec(route.qualifiedName); + if (!q) continue; + const content = context.readFile(route.filePath); + if (content && /\bRoutePrefix\s*(?:=>|=)\s*"/.test(content)) continue; + const name = `${q[2]} ${joinCsPath(head.replace(/\/+$/, '') + '/' + q[1], q[3]!)}`; + if (name !== route.name) updates.push({ ...route, name }); + } + return updates; +} /** Join a class-level [Route] prefix and an action's path into one normalized `/path`. */ function joinCsPath(prefix: string, sub: string): string { @@ -360,3 +439,14 @@ function resolveByNameAndKind( // Fall back to any match return kindFiltered[0]!.id; } + +/** Match braces in comment-stripped text with string contents blanked. */ +function closingBrace(text: string, open: number): number { + if (open < 0 || text[open] !== '{') return -1; + let depth = 0; + for (let i = open; i < text.length; i++) { + if (text[i] === '{') depth++; + else if (text[i] === '}' && --depth === 0) return i; + } + return -1; +} diff --git a/src/resolution/frameworks/react-router.ts b/src/resolution/frameworks/react-router.ts index 681b9b149c..213af72e1e 100644 --- a/src/resolution/frameworks/react-router.ts +++ b/src/resolution/frameworks/react-router.ts @@ -28,11 +28,12 @@ * in JavaScript, so the receiver has to name a router — a bare `push` is an * array's, and is never claimed. * - * Known limits, both deliberate: a nested route's path is relative to its - * parent (`` inside ``), and the - * markup scan does not compose that tree, so only an absolute path is a - * destination an href can name; and a splat (`/admin/*`) matches anything, so - * it is never the answer to a concrete href. + * A nested route's path is relative to its parent's (`` + * 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. */ import type { Language, Node } from '../../types'; @@ -306,14 +307,17 @@ 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. + * (`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. */ function isReactRouterRoute(node: Node): boolean { - return ( - node.id.startsWith(`route:react-router:${node.filePath}:`) || - ((node.language === 'tsx' || node.language === 'jsx') && - node.id === `route:${node.filePath}:${node.startLine}:${node.name}`) - ); + 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; + // 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); + return rest === node.name || (!/^[A-Z]+:/.test(rest) && Boolean(node.signature?.startsWith('route-parts:'))); } /** Expand bounded optional segments, including Remix's optional language prefix. */ diff --git a/src/resolution/frameworks/react.ts b/src/resolution/frameworks/react.ts index 5507ca0165..6d294c8f7e 100644 --- a/src/resolution/frameworks/react.ts +++ b/src/resolution/frameworks/react.ts @@ -10,6 +10,7 @@ import { FrameworkResolver, UnresolvedRef, ResolvedRef, ResolutionContext } from import { dependsOn } from './package-deps'; import { detectLanguage } from '../../extraction/grammars'; import { parseSourceTreeSync } from '../../extraction/parse-tree'; +import { resolveImportPath } from '../import-resolver'; export const reactResolver: FrameworkResolver = { name: 'react', @@ -29,7 +30,16 @@ 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. + claimsReference(name: string): boolean { + return name.startsWith(LAZY_ROUTE_PREFIX); + }, + resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null { + if (ref.referenceName.startsWith(LAZY_ROUTE_PREFIX)) { + 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; + } // A component, hook or context the file IMPORTS is the import's: the // package's (`useQuery` from `@tanstack/react-query`, `