From 993a14da3fc40adcd162c0efaa25342bd1a6a89b Mon Sep 17 00:00:00 2001 From: ximengkai Date: Mon, 17 Aug 2026 18:02:05 +0800 Subject: [PATCH] feat(objective-c): finalize provider MVP --- .../workflows/build-tree-sitter-prebuilds.yml | 11 +- .../tree-sitter-upgrade-readiness.yml | 2 +- README.md | 5 +- docs/fork/OBJECTIVE_C_PROVIDER.md | 16 +- docs/fork/README.md | 4 +- .../core/ingestion/languages/objective-c.ts | 108 ++- .../ingestion/languages/objective-c/facts.ts | 708 +++++++----------- .../languages/objective-c/scope-resolver.ts | 292 +++----- .../fixtures/objective-c/SYModuleBridge.mm | 9 + .../test/helpers/grammar-introspection.ts | 2 +- .../integration/objective-c-provider.test.ts | 392 ++++------ .../test/unit/grammar-update-monitor.test.ts | 9 +- .../unit/objective-c-parser-loader.test.ts | 5 +- .../test/unit/objective-c-provider.test.ts | 244 +++--- gitnexus/test/unit/parser-loader-abi.test.ts | 3 +- 15 files changed, 708 insertions(+), 1102 deletions(-) create mode 100644 gitnexus/test/fixtures/objective-c/SYModuleBridge.mm diff --git a/.github/workflows/build-tree-sitter-prebuilds.yml b/.github/workflows/build-tree-sitter-prebuilds.yml index 485f9b89e..04d75f546 100644 --- a/.github/workflows/build-tree-sitter-prebuilds.yml +++ b/.github/workflows/build-tree-sitter-prebuilds.yml @@ -7,7 +7,7 @@ name: Build tree-sitter prebuilds # # Grammars covered here (the at-risk set — everything else already ships 6 # upstream prebuilds AND stays dependency-review-tracked, so it is left alone). -# All five are vendored under gitnexus/vendor/; `kind` (below) only picks where +# All six are vendored under gitnexus/vendor/; `kind` (below) only picks where # the build job fetches the C source to compile: # - tree-sitter-c (vendored prebuild-only; built from the published npm # package — closes upstream's 4/6 ARM gap #2116 for a @@ -17,6 +17,8 @@ name: Build tree-sitter prebuilds # - tree-sitter-kotlin (vendored source; built from gitnexus/vendor/ — pinned to # an unreleased main commit for `fun interface` support # (#169) that no npm release carries yet) +# - tree-sitter-objc (vendored source; built from gitnexus/vendor/ — pinned +# for the Objective-C provider MVP) # - tree-sitter-swift (vendored source; built from gitnexus/vendor/ — its # prebuilds were originally upstream-shipped, now # GitNexus-cross-built like the rest for uniformity) @@ -25,7 +27,7 @@ name: Build tree-sitter prebuilds # all 6 targets ({linux,darwin,win32}-{x64,arm64}). tree-sitter grammars are # N-API, so one ABI-stable .node per platform-arch works across all Node majors. # -# COST DISCIPLINE — this is a HEAVY native matrix (up to 3 grammars x 6 runners, +# COST DISCIPLINE — this is a HEAVY native matrix (up to 6 grammars x 6 runners, # incl. macOS + arm64). It is DELIBERATELY NOT wired into normal PR/push CI. It # runs only: # 1. on manual dispatch (workflow_dispatch); or @@ -55,7 +57,7 @@ on: workflow_dispatch: inputs: grammars: - description: 'Comma-separated grammar shortnames to build (c,dart,proto,kotlin,swift), or "all".' + description: 'Comma-separated grammar shortnames to build (c,dart,proto,kotlin,objc,swift), or "all".' required: false type: string default: 'all' @@ -153,6 +155,9 @@ jobs: // unreleased main commit for `fun interface` support (#169) that no // npm release carries yet — so it must build from the vendored source. kotlin: { name: 'tree-sitter-kotlin', kind: 'vendored' }, + // Objective-C is vendored WITH its source and its native bindings + // must be recut together with the pinned grammar snapshot. + objc: { name: 'tree-sitter-objc', kind: 'vendored' }, // swift is vendored WITH its source (parser.c/scanner.c/binding.gyp), // so it builds from gitnexus/vendor/ like dart/proto. Its prebuilds // were originally upstream-shipped; rebuilding them here unifies it. diff --git a/.github/workflows/tree-sitter-upgrade-readiness.yml b/.github/workflows/tree-sitter-upgrade-readiness.yml index 13b79e093..88ee0bd42 100644 --- a/.github/workflows/tree-sitter-upgrade-readiness.yml +++ b/.github/workflows/tree-sitter-upgrade-readiness.yml @@ -4,7 +4,7 @@ name: Tree-sitter Upgrade Readiness # 1. Peer-dep compatibility — can each NPM-installed grammar install cleanly # with tree-sitter@0.25.0 without --legacy-peer-deps? # 2. Vendored grammars — each grammar in .github/vendored-grammars.json -# (c/swift/kotlin/dart/proto) is classified by its vendored ABI, read +# (c/swift/kotlin/dart/proto/objc) is classified by its vendored ABI, read # straight from gitnexus/vendor//src/parser.c (NOT node_modules, # which is never populated for vendored grammars — that mismatch is why # the report used to render bare "?" placeholders, #858). diff --git a/README.md b/README.md index db811f0a8..e5a2a2c30 100644 --- a/README.md +++ b/README.md @@ -61,13 +61,12 @@ That's it. `analyze` indexes the codebase, installs agent skills, registers Clau ## ForgeMate Fork Extensions -This fork keeps upstream GitNexus behavior as the default. The following fork-specific capabilities are **planned**, not yet implemented or released: +This fork keeps upstream GitNexus behavior as the default. Objective-C semantic indexing is implemented as an MVP on the fork's `dev` branch; it is not part of upstream `main` or the published upstream package. The following other fork-specific capabilities remain **planned**: -- Objective-C semantic indexing. - An external per-repository index directory via `GITNEXUS_STORAGE_PATH`. - Configurable source-content retention via `GITNEXUS_CONTENT_RETENTION`. -The implementation contracts, compatibility requirements, and acceptance criteria live in [docs/fork/README.md](docs/fork/README.md). Do not rely on these planned variables until the corresponding implementation and README environment-variable entries are released. +The implementation contracts, compatibility requirements, and acceptance criteria live in [docs/fork/README.md](docs/fork/README.md). Do not rely on the planned storage and retention variables until their corresponding implementations and README environment-variable entries are released.
Install problems? npm 11 crash · slow cold install · no C++ toolchain diff --git a/docs/fork/OBJECTIVE_C_PROVIDER.md b/docs/fork/OBJECTIVE_C_PROVIDER.md index 6d6a04aba..84ad88f30 100644 --- a/docs/fork/OBJECTIVE_C_PROVIDER.md +++ b/docs/fork/OBJECTIVE_C_PROVIDER.md @@ -1,11 +1,12 @@ # Objective-C Provider -Status: planned +Status: implemented -Implementation note (`objc-provider` branch): a first deterministic provider is wired in and -covered by focused tests, but this document remains `planned` until the full acceptance matrix -is green in CI. The local macOS arm64 grammar smoke and Linux Docker arm64/x64 binding-load -smokes pass; the full repository suite still has unrelated host-environment failures. +Implementation note (`dev` branch): the first deterministic provider is wired in and covered by +focused unit/integration tests. The parser-loader ABI smoke runs in the published multi-OS test +matrix, and the native prebuild workflow owns Objective-C together with all six vendored grammar +targets. This status describes the implemented MVP; it does not promise full Objective-C +runtime dispatch. ## Goal @@ -88,7 +89,7 @@ The acceptance bar is: The MVP does not promise exact runtime type inference for `id` or `instancetype`, reflection, swizzling, arbitrary category replacement, dynamic selector construction, or complete impact analysis across every runtime dispatch path. Tool results must surface confidence and unresolved evidence rather than presenting guesses as certain graph facts. -## Current implementation coverage on `objc-provider` +## Current implementation coverage on `dev` Implemented in the branch: @@ -99,9 +100,8 @@ Implemented in the branch: - Persisted query/context support for Objective-C class and method nodes, including implementation evidence via `DECLARES`. - Regression tests for grammar loading, `.h` classification, stable identities, conservative calls, metadata feature mismatch, persisted query/context behavior, and incremental-vs-force parity for Objective-C fixture edits. -Known limits before changing this status: +Known limits of this MVP: -- Linux Docker grammar loading is verified for arm64 and x86-64 Node 22 runners; the published CI matrix still needs to exercise the new grammar before this status changes. - The first version does not perform full Objective-C runtime dispatch, swizzling, dynamic selector construction, macro expansion, or `id` flow inference. - Protocol receiver handling records the protocol method and candidate implementation evidence, but candidate implementations are not emitted as certain call edges. - Objective-C++ `.mm` files are parsed with the Objective-C grammar path for this MVP; deep C++ semantic extraction inside Objective-C++ bodies remains outside this provider. diff --git a/docs/fork/README.md b/docs/fork/README.md index 500023c00..ac4eb703a 100644 --- a/docs/fork/README.md +++ b/docs/fork/README.md @@ -13,11 +13,11 @@ This directory records behavior that belongs to the `mengkaka/GitNexus` fork. It | Capability | Status | Contract | | --- | --- | --- | -| Objective-C Provider | Planned | [OBJECTIVE_C_PROVIDER.md](OBJECTIVE_C_PROVIDER.md) | +| Objective-C Provider | Implemented | [OBJECTIVE_C_PROVIDER.md](OBJECTIVE_C_PROVIDER.md) | | External index storage | Planned | [EXTERNAL_STORAGE_AND_CONTENT_RETENTION.md](EXTERNAL_STORAGE_AND_CONTENT_RETENTION.md) | | Content retention profiles | Planned | [EXTERNAL_STORAGE_AND_CONTENT_RETENTION.md](EXTERNAL_STORAGE_AND_CONTENT_RETENTION.md) | -`Planned` means no CLI, MCP, Web UI, or environment-variable behavior may claim support yet. Each implementation PR must update this table, its related design document, tests, and the public README environment-variable table where applicable. +`Implemented` means the documented MVP, regression tests, metadata contract, and package/runtime wiring are present on the fork's `dev` branch. It does not expand the provider into full Objective-C runtime dispatch. `Planned` means no CLI, MCP, Web UI, or environment-variable behavior may claim support yet. Each implementation PR must update this table, its related design document, tests, and the public README environment-variable table where applicable. ## Reading order diff --git a/gitnexus/src/core/ingestion/languages/objective-c.ts b/gitnexus/src/core/ingestion/languages/objective-c.ts index c47b3bb6d..020c16bd5 100644 --- a/gitnexus/src/core/ingestion/languages/objective-c.ts +++ b/gitnexus/src/core/ingestion/languages/objective-c.ts @@ -1,15 +1,16 @@ -import path from "path"; +import path from 'path'; import { SupportedLanguages, type CaptureMatch, type ParsedImport, type ParsedTypeBinding, -} from "gitnexus-shared"; -import Parser from "tree-sitter"; -import { defineLanguage } from "../language-provider.js"; -import type { ImportResolverFn } from "../import-resolvers/types.js"; -import { getLanguageGrammar } from "../../tree-sitter/parser-loader.js"; -import { assertCloneable } from "../workers/clone-safety.js"; +} from 'gitnexus-shared'; +import Parser from 'tree-sitter'; +import { defineLanguage } from '../language-provider.js'; +import type { ImportResolverFn } from '../import-resolvers/types.js'; +import { getLanguageGrammar } from '../../tree-sitter/parser-loader.js'; +import { parseSourceSafe } from '../../tree-sitter/safe-parse.js'; +import { assertCloneable } from '../workers/clone-safety.js'; import { buildObjectiveCSemanticGraph, buildObjectiveCScopeCaptures, @@ -17,7 +18,7 @@ import { collectObjectiveCFacts, parseObjCType, setObjectiveCFileFacts, -} from "./objective-c/facts.js"; +} from './objective-c/facts.js'; const OBJECTIVE_C_SCOPE_QUERY = `((translation_unit) @objc.root)`; @@ -35,30 +36,30 @@ function normalizedExt(filePath: string): string { function isObjectiveCSourcePath(filePath: string): boolean { const ext = normalizedExt(filePath); - return ext === ".m" || ext === ".mm"; + return ext === '.m' || ext === '.mm'; } function isHeaderPath(filePath: string): boolean { - return normalizedExt(filePath) === ".h"; + return normalizedExt(filePath) === '.h'; } const OBJECTIVE_C_HEADER_NODE_TYPES = new Set([ - "class_declaration", - "class_interface", - "class_implementation", - "compatibility_alias_declaration", - "module_import", - "protocol_declaration", + 'class_declaration', + 'class_interface', + 'class_implementation', + 'compatibility_alias_declaration', + 'module_import', + 'protocol_declaration', ]); const OBJECTIVE_C_FRAMEWORK_NAMES = [ - "AppKit", - "Foundation", - "UIKit", - "CoreData", - "CoreFoundation", - "QuartzCore", - "Swift", + 'AppKit', + 'Foundation', + 'UIKit', + 'CoreData', + 'CoreFoundation', + 'QuartzCore', + 'Swift', ]; function hasObjectiveCHeaderSyntax(sourceText: string): boolean { @@ -66,18 +67,14 @@ function hasObjectiveCHeaderSyntax(sourceText: string): boolean { const tree = parseObjectiveCSource(sourceText); const stack: Parser.SyntaxNode[] = [tree.rootNode]; while (stack.length > 0) { - const node = stack.pop()!; + const node = stack.pop(); + if (node === undefined) continue; if (OBJECTIVE_C_HEADER_NODE_TYPES.has(node.type)) return true; - if ( - node.type === "preproc_include" && - node.text.trimStart().startsWith("#import") - ) { + if (node.type === 'preproc_include' && node.text.trimStart().startsWith('#import')) { const pathNode = node.namedChildren[0]; if ( pathNode !== undefined && - OBJECTIVE_C_FRAMEWORK_NAMES.some((name) => - pathNode.text.includes(name), - ) + OBJECTIVE_C_FRAMEWORK_NAMES.some((name) => pathNode.text.includes(name)) ) { return true; } @@ -93,10 +90,7 @@ function hasObjectiveCHeaderSyntax(sourceText: string): boolean { return false; } -export function classifyObjectiveCFileContent( - filePath: string, - sourceText: string, -): boolean { +export function classifyObjectiveCFileContent(filePath: string, sourceText: string): boolean { if (isObjectiveCSourcePath(filePath)) return true; if (!isHeaderPath(filePath)) return false; return hasObjectiveCHeaderSyntax(sourceText); @@ -105,13 +99,10 @@ export function classifyObjectiveCFileContent( function parseObjectiveCSource(sourceText: string): Parser.Tree { const parser = new Parser(); parser.setLanguage(getLanguageGrammar(SupportedLanguages.ObjectiveC)); - return parser.parse(sourceText); + return parseSourceSafe(parser, sourceText, undefined, undefined, 'Objective-C source'); } -function treeFromCachedOrSource( - cachedTree: unknown, - sourceText: string, -): Parser.Tree { +function treeFromCachedOrSource(cachedTree: unknown, sourceText: string): Parser.Tree { if (cachedTree !== undefined && looksLikeTree(cachedTree)) return cachedTree; return parseObjectiveCSource(sourceText); } @@ -119,49 +110,42 @@ function treeFromCachedOrSource( function looksLikeTree(value: unknown): value is Parser.Tree { return ( value !== null && - typeof value === "object" && - "rootNode" in value && + typeof value === 'object' && + 'rootNode' in value && (value as { rootNode?: unknown }).rootNode !== undefined ); } -function interpretObjectiveCImport( - captures: CaptureMatch, -): ParsedImport | null { - const source = captures["@import.source"]; +function interpretObjectiveCImport(captures: CaptureMatch): ParsedImport | null { + const source = captures['@import.source']; if (source === undefined || source.text.trim().length === 0) return null; const targetRaw = source.text.trim(); - const kind = captures["@import.kind"]?.text.trim(); + const kind = captures['@import.kind']?.text.trim(); return { - kind: "side-effect", + kind: 'side-effect', // Scope resolution needs to distinguish a quoted header path from a bare // @import module name, while the semantic graph retains the original raw // import spelling in ObjCImportFact. - targetRaw: - kind === "module" || targetRaw.startsWith("./") - ? targetRaw - : `./${targetRaw}`, + targetRaw: kind === 'module' || targetRaw.startsWith('./') ? targetRaw : `./${targetRaw}`, }; } -function interpretObjectiveCTypeBinding( - captures: CaptureMatch, -): ParsedTypeBinding | null { - const name = captures["@type-binding.name"]; - const type = captures["@type-binding.type"]; +function interpretObjectiveCTypeBinding(captures: CaptureMatch): ParsedTypeBinding | null { + const name = captures['@type-binding.name']; + const type = captures['@type-binding.type']; if (name === undefined || type === undefined) return null; const parsed = parseObjCType(type.text); return { boundName: name.text, rawTypeName: parsed?.name ?? parsed?.raw ?? type.text, declaredSpelling: type.text, - source: "annotation", + source: 'annotation', }; } export const objectiveCProvider = defineLanguage({ id: SupportedLanguages.ObjectiveC, - extensions: [".m", ".mm"], + extensions: ['.m', '.mm'], treeSitterQueries: OBJECTIVE_C_SCOPE_QUERY, typeConfig: EMPTY_TYPE_CONFIG, exportChecker: () => true, @@ -170,11 +154,7 @@ export const objectiveCProvider = defineLanguage({ shouldClassifyFileContent: isHeaderPath, importsExecuteWhereWritten: false, - emitScopeCaptures: ( - sourceText, - filePath, - cachedTree, - ): readonly CaptureMatch[] => { + emitScopeCaptures: (sourceText, filePath, cachedTree): readonly CaptureMatch[] => { const tree = treeFromCachedOrSource(cachedTree, sourceText); const facts = collectObjectiveCFacts(tree, filePath); setObjectiveCFileFacts(facts); diff --git a/gitnexus/src/core/ingestion/languages/objective-c/facts.ts b/gitnexus/src/core/ingestion/languages/objective-c/facts.ts index 5379248cb..60b2c0a52 100644 --- a/gitnexus/src/core/ingestion/languages/objective-c/facts.ts +++ b/gitnexus/src/core/ingestion/languages/objective-c/facts.ts @@ -1,44 +1,35 @@ -import type { - Capture, - CaptureMatch, - NodeLabel, - ParsedFile, -} from "gitnexus-shared"; -import { SupportedLanguages } from "gitnexus-shared"; -import type Parser from "tree-sitter"; -import { generateId } from "../../../../lib/utils.js"; +import type { Capture, CaptureMatch, NodeLabel, ParsedFile } from 'gitnexus-shared'; +import { SupportedLanguages } from 'gitnexus-shared'; +import type Parser from 'tree-sitter'; +import { generateId } from '../../../../lib/utils.js'; import type { ProviderSemanticGraph, ProviderSemanticNode, ProviderSemanticRelationship, ProviderSemanticSymbol, -} from "../../language-provider.js"; -import { - nodeToCapture, - walkNamedTree, - type SyntaxNode, -} from "../../utils/ast-helpers.js"; +} from '../../language-provider.js'; +import { nodeToCapture, walkNamedTree, type SyntaxNode } from '../../utils/ast-helpers.js'; -export const OBJECTIVE_C_PROVIDER_VERSION = "0.1.0"; -export const OBJECTIVE_C_GRAMMAR_PACKAGE = "tree-sitter-objc"; -export const OBJECTIVE_C_GRAMMAR_VERSION = "3.0.2"; +export const OBJECTIVE_C_PROVIDER_VERSION = '0.1.0'; +export const OBJECTIVE_C_GRAMMAR_PACKAGE = 'tree-sitter-objc'; +export const OBJECTIVE_C_GRAMMAR_VERSION = '3.0.2'; -export type ObjCMethodKind = "-" | "+"; -export type ObjCContainerKind = "class" | "protocol" | "category" | "extension"; +export type ObjCMethodKind = '-' | '+'; +export type ObjCContainerKind = 'class' | 'protocol' | 'category' | 'extension'; export interface ObjCTypeInfo { - readonly kind: "class" | "protocol" | "dynamic" | "class-object" | "unknown"; + readonly kind: 'class' | 'protocol' | 'dynamic' | 'class-object' | 'unknown'; readonly name?: string; readonly raw: string; } export interface ObjCContainerFact { readonly kind: ObjCContainerKind; - readonly declarationRole: "interface" | "implementation"; + readonly declarationRole: 'interface' | 'implementation'; readonly name: string; readonly qualifiedName: string; readonly nodeId: string; - readonly label: "Class" | "Protocol" | "Category"; + readonly label: 'Class' | 'Protocol' | 'Category'; readonly filePath: string; readonly startLine: number; readonly endLine: number; @@ -61,21 +52,21 @@ export interface ObjCMethodFact { readonly filePath: string; readonly startLine: number; readonly endLine: number; - readonly declarationRole: "declaration" | "implementation"; + readonly declarationRole: 'declaration' | 'implementation'; readonly returnType?: string; readonly parameterTypes: readonly string[]; readonly parameterNames: readonly string[]; } export interface ObjCMemberFact { - readonly kind: "property" | "ivar"; + readonly kind: 'property' | 'ivar'; readonly name: string; readonly qualifiedName: string; readonly nodeId: string; readonly ownerQualifiedName: string; readonly ownerName: string; readonly ownerKind: ObjCContainerKind; - readonly ownerLabel: "Class" | "Protocol" | "Category"; + readonly ownerLabel: 'Class' | 'Protocol' | 'Category'; readonly hostClass?: string; readonly declaredType?: string; readonly filePath: string; @@ -95,7 +86,7 @@ export interface ObjCFunctionFact { } export interface ObjCImportFact { - readonly kind: "import" | "include" | "module"; + readonly kind: 'import' | 'include' | 'module'; readonly raw: string; readonly targetRaw: string; readonly filePath: string; @@ -107,14 +98,14 @@ export interface ObjCMessageFact { readonly selector: string; readonly receiverText: string; readonly receiverKind: - | "self" - | "super" - | "class" - | "local" - | "property" - | "ivar" - | "dynamic" - | "unknown"; + | 'self' + | 'super' + | 'class' + | 'local' + | 'property' + | 'ivar' + | 'dynamic' + | 'unknown'; readonly receiverType?: ObjCTypeInfo; readonly sourceMethodQualifiedName: string; readonly sourceMethodId: string; @@ -151,7 +142,7 @@ export interface ObjCFileFacts { } export interface ObjCCaptureSideChannel { - readonly kind: "objective-c"; + readonly kind: 'objective-c'; readonly facts: ObjCFileFacts; } @@ -161,9 +152,7 @@ export function setObjectiveCFileFacts(facts: ObjCFileFacts): void { factsByFile.set(facts.filePath, facts); } -export function getObjectiveCFileFacts( - filePath: string, -): ObjCFileFacts | undefined { +export function getObjectiveCFileFacts(filePath: string): ObjCFileFacts | undefined { return factsByFile.get(filePath); } @@ -171,7 +160,7 @@ export function collectObjectiveCCaptureSideChannel( filePath: string, ): ObjCCaptureSideChannel | undefined { const facts = factsByFile.get(filePath); - return facts === undefined ? undefined : { kind: "objective-c", facts }; + return facts === undefined ? undefined : { kind: 'objective-c', facts }; } export function applyObjectiveCCaptureSideChannel(parsed: ParsedFile): void { @@ -187,9 +176,7 @@ export function objectiveCFactsFromParsedFiles( const seen = new Set(); for (const parsed of parsedFiles) { const payload = parsed.captureSideChannel; - const fromPayload = isObjectiveCSideChannel(payload) - ? payload.facts - : undefined; + const fromPayload = isObjectiveCSideChannel(payload) ? payload.facts : undefined; const fact = fromPayload ?? factsByFile.get(parsed.filePath); if (fact === undefined || seen.has(fact.filePath)) continue; seen.add(fact.filePath); @@ -198,53 +185,32 @@ export function objectiveCFactsFromParsedFiles( return facts; } -function isObjectiveCSideChannel( - value: unknown, -): value is ObjCCaptureSideChannel { - if (value === null || typeof value !== "object") return false; +function isObjectiveCSideChannel(value: unknown): value is ObjCCaptureSideChannel { + if (value === null || typeof value !== 'object') return false; const record = value as Record; - return ( - record.kind === "objective-c" && - record.facts !== null && - typeof record.facts === "object" - ); + return record.kind === 'objective-c' && record.facts !== null && typeof record.facts === 'object'; } -export const objcClassQualifiedName = (name: string): string => - `objc:class:${name}`; -export const objcProtocolQualifiedName = (name: string): string => - `objc:protocol:${name}`; -export const objcCategoryQualifiedName = ( - hostClass: string, - categoryName: string, -): string => `objc:category:${hostClass}:${categoryName}`; +export const objcClassQualifiedName = (name: string): string => `objc:class:${name}`; +export const objcProtocolQualifiedName = (name: string): string => `objc:protocol:${name}`; +export const objcCategoryQualifiedName = (hostClass: string, categoryName: string): string => + `objc:category:${hostClass}:${categoryName}`; export const objcMethodQualifiedName = ( ownerQualifiedName: string, methodKind: ObjCMethodKind, selector: string, ): string => `objc:method:${ownerQualifiedName}:${methodKind}:${selector}`; -export const objcPropertyQualifiedName = ( - ownerQualifiedName: string, - name: string, -): string => `objc:property:${ownerQualifiedName}:${name}`; -export const objcIvarQualifiedName = ( - ownerQualifiedName: string, - name: string, -): string => `objc:ivar:${ownerQualifiedName}:${name}`; -export const objcFunctionQualifiedName = (name: string): string => - `objc:function:${name}`; +export const objcPropertyQualifiedName = (ownerQualifiedName: string, name: string): string => + `objc:property:${ownerQualifiedName}:${name}`; +export const objcIvarQualifiedName = (ownerQualifiedName: string, name: string): string => + `objc:ivar:${ownerQualifiedName}:${name}`; +export const objcFunctionQualifiedName = (name: string): string => `objc:function:${name}`; const graphNodeId = (label: NodeLabel, qualifiedName: string): string => generateId(label, qualifiedName); -function ownerLabel( - kind: ObjCContainerKind, -): "Class" | "Protocol" | "Category" { - return kind === "class" - ? "Class" - : kind === "protocol" - ? "Protocol" - : "Category"; +function ownerLabel(kind: ObjCContainerKind): 'Class' | 'Protocol' | 'Category' { + return kind === 'class' ? 'Class' : kind === 'protocol' ? 'Protocol' : 'Category'; } const range = (node: SyntaxNode) => ({ @@ -270,24 +236,15 @@ function directChildren(node: SyntaxNode): SyntaxNode[] { return out; } -function firstDirectIdentifier(node: SyntaxNode): SyntaxNode | null { - return ( - directNamedChildren(node).find((child) => child.type === "identifier") ?? - null - ); -} - function directIdentifiers(node: SyntaxNode): SyntaxNode[] { - return directNamedChildren(node).filter( - (child) => child.type === "identifier", - ); + return directNamedChildren(node).filter((child) => child.type === 'identifier'); } function stripQuotes(raw: string): string { const trimmed = raw.trim(); if ( (trimmed.startsWith('"') && trimmed.endsWith('"')) || - (trimmed.startsWith("<") && trimmed.endsWith(">")) + (trimmed.startsWith('<') && trimmed.endsWith('>')) ) { return trimmed.slice(1, -1); } @@ -296,51 +253,41 @@ function stripQuotes(raw: string): string { function cleanType(raw: string | undefined): string | undefined { if (raw === undefined) return undefined; - let text = raw - .replace(/^\(/, "") - .replace(/\)$/, "") - .replace( - /\b(?:nullable|nonnull|__nullable|__nonnull|_Nullable|_Nonnull|const)\b/g, - " ", - ) - .replace(/\s+/g, " ") + const text = raw + .replace(/^\(/, '') + .replace(/\)$/, '') + .replace(/\b(?:nullable|nonnull|__nullable|__nonnull|_Nullable|_Nonnull|const)\b/g, ' ') + .replace(/\s+/g, ' ') .trim(); if (text.length === 0) return undefined; return text; } -export function parseObjCType( - raw: string | undefined, -): ObjCTypeInfo | undefined { +export function parseObjCType(raw: string | undefined): ObjCTypeInfo | undefined { const text = cleanType(raw); if (text === undefined) return undefined; - if (text === "id" || text === "instancetype") - return { kind: "dynamic", raw: text }; - if (text === "Class") return { kind: "class-object", raw: text }; + if (text === 'id' || text === 'instancetype') return { kind: 'dynamic', raw: text }; + if (text === 'Class') return { kind: 'class-object', raw: text }; const protocolMatch = text.match(/^id\s*<\s*([A-Za-z_][A-Za-z0-9_]*)\s*>/); if (protocolMatch !== null) { - return { kind: "protocol", name: protocolMatch[1], raw: text }; + return { kind: 'protocol', name: protocolMatch[1], raw: text }; } - const classMatch = text.match( - /([A-Za-z_][A-Za-z0-9_]*)\s*(?:<[^>]+>)?\s*\*?$/, - ); + const classMatch = text.match(/([A-Za-z_][A-Za-z0-9_]*)\s*(?:<[^>]+>)?\s*\*?$/); if (classMatch !== null) { - return { kind: "class", name: classMatch[1], raw: text }; + return { kind: 'class', name: classMatch[1], raw: text }; } - return { kind: "unknown", raw: text }; + return { kind: 'unknown', raw: text }; } function methodKind(node: SyntaxNode): ObjCMethodKind { const first = node.child(0)?.text; - return first === "+" ? "+" : "-"; + return first === '+' ? '+' : '-'; } function methodTypeText(node: SyntaxNode): string | undefined { - const typeNode = directNamedChildren(node).find( - (child) => child.type === "method_type", - ); + const typeNode = directNamedChildren(node).find((child) => child.type === 'method_type'); return cleanType(typeNode?.text); } @@ -349,15 +296,15 @@ function methodSelector(node: SyntaxNode): string { const pieces: string[] = []; for (let i = 0; i < children.length; i++) { const child = children[i]; - if (child.type !== "identifier") continue; + if (child.type !== 'identifier') continue; const next = children[i + 1]; - if (next?.type === "method_parameter") { + if (next?.type === 'method_parameter') { pieces.push(`${child.text}:`); continue; } if (pieces.length === 0) pieces.push(child.text); } - return pieces.join(""); + return pieces.join(''); } function messageSelector(node: SyntaxNode): string { @@ -365,13 +312,12 @@ function messageSelector(node: SyntaxNode): string { const pieces: string[] = []; for (let i = 2; i < children.length; i++) { const child = children[i]; - if (child.type !== "identifier" && child.type !== "field_identifier") - continue; + if (child.type !== 'identifier' && child.type !== 'field_identifier') continue; const next = children[i + 1]; - if (next?.text === ":") pieces.push(`${child.text}:`); - else if (pieces.length === 0 && next?.text === "]") pieces.push(child.text); + if (next?.text === ':') pieces.push(`${child.text}:`); + else if (pieces.length === 0 && next?.text === ']') pieces.push(child.text); } - return pieces.join(""); + return pieces.join(''); } function methodParameterInfo(node: SyntaxNode): { @@ -383,10 +329,10 @@ function methodParameterInfo(node: SyntaxNode): { const parameterNames: string[] = []; const typeBindings = new Map(); for (const child of directNamedChildren(node)) { - if (child.type !== "method_parameter") continue; + if (child.type !== 'method_parameter') continue; const named = directNamedChildren(child); - const typeNode = named.find((n) => n.type === "method_type"); - const nameNode = [...named].reverse().find((n) => n.type === "identifier"); + const typeNode = named.find((n) => n.type === 'method_type'); + const nameNode = [...named].reverse().find((n) => n.type === 'identifier'); const typeText = cleanType(typeNode?.text); if (typeText !== undefined) parameterTypes.push(typeText); if (nameNode !== undefined) { @@ -398,27 +344,21 @@ function methodParameterInfo(node: SyntaxNode): { return { parameterTypes, parameterNames, typeBindings }; } -function propertyInfo( - node: SyntaxNode, -): { name: string; type?: string } | null { - const structDecl = directNamedChildren(node).find( - (child) => child.type === "struct_declaration", - ); +function propertyInfo(node: SyntaxNode): { name: string; type?: string } | null { + const structDecl = directNamedChildren(node).find((child) => child.type === 'struct_declaration'); if (structDecl === undefined) return null; return declarationNameAndType(structDecl); } -function declarationNameAndType( - node: SyntaxNode, -): { name: string; type?: string } | null { +function declarationNameAndType(node: SyntaxNode): { name: string; type?: string } | null { const name = declaratorName(node); if (name === undefined) return null; return { name, type: cleanType(firstTypeNode(node)?.text) }; } function declaratorName(node: SyntaxNode): string | undefined { - if (node.type === "identifier") return node.text; - if (node.type === "init_declarator") { + if (node.type === 'identifier') return node.text; + if (node.type === 'init_declarator') { for (const child of directNamedChildren(node)) { const name = declaratorName(child); if (name !== undefined) return name; @@ -426,11 +366,11 @@ function declaratorName(node: SyntaxNode): string | undefined { return undefined; } if ( - node.type === "pointer_declarator" || - node.type === "array_declarator" || - node.type === "function_declarator" || - node.type === "parenthesized_declarator" || - node.type === "struct_declarator" + node.type === 'pointer_declarator' || + node.type === 'array_declarator' || + node.type === 'function_declarator' || + node.type === 'parenthesized_declarator' || + node.type === 'struct_declarator' ) { for (const child of directNamedChildren(node)) { const name = declaratorName(child); @@ -441,13 +381,13 @@ function declaratorName(node: SyntaxNode): string | undefined { for (const child of directNamedChildren(node)) { if ( - child.type === "init_declarator" || - child.type === "pointer_declarator" || - child.type === "array_declarator" || - child.type === "function_declarator" || - child.type === "parenthesized_declarator" || - child.type === "struct_declarator" || - child.type === "struct_declaration" + child.type === 'init_declarator' || + child.type === 'pointer_declarator' || + child.type === 'array_declarator' || + child.type === 'function_declarator' || + child.type === 'parenthesized_declarator' || + child.type === 'struct_declarator' || + child.type === 'struct_declaration' ) { const name = declaratorName(child); if (name !== undefined) return name; @@ -458,9 +398,9 @@ function declaratorName(node: SyntaxNode): string | undefined { function firstTypeNode(node: SyntaxNode): SyntaxNode | undefined { if ( - node.type === "type_identifier" || - node.type === "primitive_type" || - node.type === "typedefed_specifier" + node.type === 'type_identifier' || + node.type === 'primitive_type' || + node.type === 'typedefed_specifier' ) { return node; } @@ -478,16 +418,12 @@ function functionInfo(node: SyntaxNode): { } | null { let declarator: SyntaxNode | undefined; walkNamedTree(node, (child) => { - if (declarator === undefined && child.type === "function_declarator") - declarator = child; + if (declarator === undefined && child.type === 'function_declarator') declarator = child; }); if (declarator === undefined) return null; // `int (*callback)(int)` is a function pointer declaration, not a callable // definition. A pointer return (`int *func(void)`) remains a C function. - if ( - declarator.childForFieldName("declarator")?.type === - "parenthesized_declarator" - ) { + if (declarator.childForFieldName('declarator')?.type === 'parenthesized_declarator') { return null; } const name = declaratorName(declarator); @@ -495,19 +431,19 @@ function functionInfo(node: SyntaxNode): { const returnType = cleanType( directNamedChildren(node).find( (child) => - child.type === "type_identifier" || - child.type === "primitive_type" || - child.type === "typedefed_specifier", + child.type === 'type_identifier' || + child.type === 'primitive_type' || + child.type === 'typedefed_specifier', )?.text, ); const parameterTypes: string[] = []; walkNamedTree(declarator, (child) => { - if (child.type !== "parameter_declaration") return; + if (child.type !== 'parameter_declaration') return; const typeNode = directNamedChildren(child).find( (n) => - n.type === "type_identifier" || - n.type === "primitive_type" || - n.type === "typedefed_specifier", + n.type === 'type_identifier' || + n.type === 'primitive_type' || + n.type === 'typedefed_specifier', ); const typeText = cleanType(typeNode?.text); if (typeText !== undefined) parameterTypes.push(typeText); @@ -518,14 +454,12 @@ function functionInfo(node: SyntaxNode): { function directProtocolNames(node: SyntaxNode): string[] { const out: string[] = []; for (const child of directNamedChildren(node)) { - if (child.type === "protocol_reference_list") { + if (child.type === 'protocol_reference_list') { out.push(...directIdentifiers(child).map((id) => id.text)); - } else if (child.type === "parameterized_arguments") { + } else if (child.type === 'parameterized_arguments') { for (const named of directNamedChildren(child)) { - if (named.type !== "type_name") continue; - const id = directNamedChildren(named).find( - (n) => n.type === "type_identifier", - ); + if (named.type !== 'type_name') continue; + const id = directNamedChildren(named).find((n) => n.type === 'type_identifier'); if (id !== undefined) out.push(id.text); } } @@ -533,14 +467,11 @@ function directProtocolNames(node: SyntaxNode): string[] { return Array.from(new Set(out)); } -function parseContainer( - node: SyntaxNode, - filePath: string, -): ObjCContainerFact | null { +function parseContainer(node: SyntaxNode, filePath: string): ObjCContainerFact | null { if ( - node.type !== "class_interface" && - node.type !== "class_implementation" && - node.type !== "protocol_declaration" + node.type !== 'class_interface' && + node.type !== 'class_implementation' && + node.type !== 'protocol_declaration' ) { return null; } @@ -548,42 +479,38 @@ function parseContainer( const first = ids[0]; if (first === undefined) return null; const { startLine, endLine } = range(node); - if (node.type === "protocol_declaration") { + if (node.type === 'protocol_declaration') { const qualifiedName = objcProtocolQualifiedName(first.text); return { - kind: "protocol", - declarationRole: "interface", + kind: 'protocol', + declarationRole: 'interface', name: first.text, qualifiedName, - nodeId: graphNodeId("Protocol", qualifiedName), - label: "Protocol", + nodeId: graphNodeId('Protocol', qualifiedName), + label: 'Protocol', filePath, startLine, endLine, - protocols: directProtocolNames(node).filter( - (name) => name !== first.text, - ), + protocols: directProtocolNames(node).filter((name) => name !== first.text), }; } const children = directChildren(node); - const openParenIndex = children.findIndex((child) => child.text === "("); + const openParenIndex = children.findIndex((child) => child.text === '('); if (openParenIndex !== -1) { const categoryIdentifier = children .slice(openParenIndex + 1) - .find((child) => child.type === "identifier"); - const categoryName = categoryIdentifier?.text ?? "__extension__"; - const kind: ObjCContainerKind = - categoryIdentifier === undefined ? "extension" : "category"; + .find((child) => child.type === 'identifier'); + const categoryName = categoryIdentifier?.text ?? '__extension__'; + const kind: ObjCContainerKind = categoryIdentifier === undefined ? 'extension' : 'category'; const qualifiedName = objcCategoryQualifiedName(first.text, categoryName); return { kind, - declarationRole: - node.type === "class_interface" ? "interface" : "implementation", - name: `${first.text} (${categoryIdentifier?.text ?? ""})`, + declarationRole: node.type === 'class_interface' ? 'interface' : 'implementation', + name: `${first.text} (${categoryIdentifier?.text ?? ''})`, qualifiedName, - nodeId: graphNodeId("Category", qualifiedName), - label: "Category", + nodeId: graphNodeId('Category', qualifiedName), + label: 'Category', filePath, startLine, endLine, @@ -593,22 +520,19 @@ function parseContainer( }; } - const colonIndex = children.findIndex((child) => child.text === ":"); + const colonIndex = children.findIndex((child) => child.text === ':'); const superclass = colonIndex !== -1 - ? children - .slice(colonIndex + 1) - .find((child) => child.type === "identifier")?.text + ? children.slice(colonIndex + 1).find((child) => child.type === 'identifier')?.text : undefined; const qualifiedName = objcClassQualifiedName(first.text); return { - kind: "class", - declarationRole: - node.type === "class_interface" ? "interface" : "implementation", + kind: 'class', + declarationRole: node.type === 'class_interface' ? 'interface' : 'implementation', name: first.text, qualifiedName, - nodeId: graphNodeId("Class", qualifiedName), - label: "Class", + nodeId: graphNodeId('Class', qualifiedName), + label: 'Class', filePath, startLine, endLine, @@ -620,8 +544,7 @@ function parseContainer( function collectLocalTypes(methodNode: SyntaxNode): Map { const locals = new Map(); walkNamedTree(methodNode, (node) => { - if (node.type !== "declaration" && node.type !== "parameter_declaration") - return; + if (node.type !== 'declaration' && node.type !== 'parameter_declaration') return; const info = declarationNameAndType(node); if (info === null) return; const typeInfo = parseObjCType(info.type); @@ -632,10 +555,10 @@ function collectLocalTypes(methodNode: SyntaxNode): Map { function isReflectionSelector(selector: string, receiverText: string): boolean { return ( - selector.startsWith("performSelector:") || - selector === "methodForSelector:" || - selector === "forwardInvocation:" || - receiverText === "NSInvocation" + selector.startsWith('performSelector:') || + selector === 'methodForSelector:' || + selector === 'forwardInvocation:' || + receiverText === 'NSInvocation' ); } @@ -649,9 +572,7 @@ function messageReceiver(node: SyntaxNode): SyntaxNode | null { } function macroName(node: SyntaxNode): string | undefined { - return ( - node.childForFieldName("name")?.text ?? directIdentifiers(node)[0]?.text - ); + return node.childForFieldName('name')?.text ?? directIdentifiers(node)[0]?.text; } function classifyReceiver( @@ -663,84 +584,80 @@ function classifyReceiver( macroNames: ReadonlySet, classNames: ReadonlySet, ): { - kind: ObjCMessageFact["receiverKind"]; + kind: ObjCMessageFact['receiverKind']; type?: ObjCTypeInfo; unresolvedReason?: string; } { const text = receiver.text; if (isReflectionSelector(selector, text)) { return { - kind: "dynamic", - unresolvedReason: "reflection selector dispatch is dynamic", + kind: 'dynamic', + unresolvedReason: 'reflection selector dispatch is dynamic', }; } const receiverMacroName = - receiver.type === "call_expression" - ? receiver.childForFieldName("function")?.text - : receiver.type === "identifier" + receiver.type === 'call_expression' + ? receiver.childForFieldName('function')?.text + : receiver.type === 'identifier' ? text : undefined; if (receiverMacroName !== undefined && macroNames.has(receiverMacroName)) { return { - kind: "dynamic", - type: { kind: "dynamic", raw: text }, + kind: 'dynamic', + type: { kind: 'dynamic', raw: text }, unresolvedReason: `macro receiver ${receiverMacroName} is dynamic`, }; } - if (receiver.type === "identifier") { - if (text === "self") return { kind: "self" }; - if (text === "super") return { kind: "super" }; + if (receiver.type === 'identifier') { + if (text === 'self') return { kind: 'self' }; + if (text === 'super') return { kind: 'super' }; const bound = methodTypes.get(text); if (bound !== undefined) { - if (bound.kind === "dynamic" || bound.kind === "class-object") { + if (bound.kind === 'dynamic' || bound.kind === 'class-object') { return { - kind: "dynamic", + kind: 'dynamic', type: bound, unresolvedReason: `${bound.raw} receiver is dynamic`, }; } - return { kind: "local", type: bound }; + return { kind: 'local', type: bound }; } const hostOwner = method.hostClass !== undefined ? objcClassQualifiedName(method.hostClass) : method.ownerQualifiedName; const memberType = membersByOwner.get(hostOwner)?.get(text); - if (memberType !== undefined) return { kind: "ivar", type: memberType }; + if (memberType !== undefined) return { kind: 'ivar', type: memberType }; if (classNames.has(text) || isPascalCaseLike(text)) { - return { kind: "class", type: { kind: "class", name: text, raw: text } }; + return { kind: 'class', type: { kind: 'class', name: text, raw: text } }; } - return { kind: "unknown", unresolvedReason: "receiver type is unknown" }; + return { kind: 'unknown', unresolvedReason: 'receiver type is unknown' }; } - if (receiver.type === "field_expression") { + if (receiver.type === 'field_expression') { const parts = directNamedChildren(receiver); const base = parts[0]; const field = parts[1]; - if (base?.text === "self" && field !== undefined) { + if (base?.text === 'self' && field !== undefined) { const hostOwner = method.hostClass !== undefined ? objcClassQualifiedName(method.hostClass) : method.ownerQualifiedName; const memberType = membersByOwner.get(hostOwner)?.get(field.text); - if (memberType !== undefined) - return { kind: "property", type: memberType }; + if (memberType !== undefined) return { kind: 'property', type: memberType }; return { - kind: "property", + kind: 'property', unresolvedReason: `property ${field.text} has no static type`, }; } } return { - kind: "unknown", - unresolvedReason: "receiver expression is not statically typed", + kind: 'unknown', + unresolvedReason: 'receiver expression is not statically typed', }; } -export function collectObjectiveCFacts( - tree: Parser.Tree, - filePath: string, -): ObjCFileFacts { +export function collectObjectiveCFacts(tree: Parser.Tree, filePath: string): ObjCFileFacts { const containers: ObjCContainerFact[] = []; const methods: ObjCMethodFact[] = []; const members: ObjCMemberFact[] = []; @@ -753,15 +670,14 @@ export function collectObjectiveCFacts( const classNames = new Set(); walkNamedTree(tree.rootNode, (node) => { - if (node.type !== "preproc_def" && node.type !== "preproc_function_def") - return; + if (node.type !== 'preproc_def' && node.type !== 'preproc_function_def') return; const name = macroName(node); if (name !== undefined) macroNames.add(name); }); walkNamedTree(tree.rootNode, (node) => { const container = parseContainer(node, filePath); - if (container?.kind === "class") classNames.add(container.name); + if (container?.kind === 'class') classNames.add(container.name); }); const addMemberType = ( @@ -780,15 +696,13 @@ export function collectObjectiveCFacts( }; for (const child of directNamedChildren(tree.rootNode)) { - if (child.type === "preproc_include") { + if (child.type === 'preproc_include') { const rawNode = directNamedChildren(child).find( - (n) => n.type === "string_literal" || n.type === "system_lib_string", + (n) => n.type === 'string_literal' || n.type === 'system_lib_string', ); if (rawNode !== undefined) { const raw = rawNode.text; - const directive = child.text.trimStart().startsWith("#include") - ? "include" - : "import"; + const directive = child.text.trimStart().startsWith('#include') ? 'include' : 'import'; const { startLine, endLine } = range(child); imports.push({ kind: directive, @@ -801,13 +715,13 @@ export function collectObjectiveCFacts( } continue; } - if (child.type === "module_import") { + if (child.type === 'module_import') { const moduleName = directIdentifiers(child) .map((n) => n.text) - .join("."); + .join('.'); const { startLine, endLine } = range(child); imports.push({ - kind: "module", + kind: 'module', raw: moduleName, targetRaw: moduleName, filePath, @@ -821,19 +735,11 @@ export function collectObjectiveCFacts( if (container !== null) { containers.push(container); for (const inner of directNamedChildren(child)) { - if ( - inner.type === "method_declaration" || - inner.type === "method_definition" - ) { + if (inner.type === 'method_declaration' || inner.type === 'method_definition') { const selector = methodSelector(inner); - const { parameterTypes, parameterNames, typeBindings } = - methodParameterInfo(inner); + const { parameterTypes, parameterNames, typeBindings } = methodParameterInfo(inner); const kind = methodKind(inner); - const qualifiedName = objcMethodQualifiedName( - container.qualifiedName, - kind, - selector, - ); + const qualifiedName = objcMethodQualifiedName(container.qualifiedName, kind, selector); const { startLine, endLine } = range(inner); const method: ObjCMethodFact = { name: selector, @@ -842,21 +748,14 @@ export function collectObjectiveCFacts( ownerQualifiedName: container.qualifiedName, ownerName: container.name, ownerKind: container.kind, - ...(container.hostClass !== undefined - ? { hostClass: container.hostClass } - : {}), + ...(container.hostClass !== undefined ? { hostClass: container.hostClass } : {}), qualifiedName, - nodeId: graphNodeId("Method", qualifiedName), + nodeId: graphNodeId('Method', qualifiedName), filePath, startLine, endLine, - declarationRole: - inner.type === "method_definition" - ? "implementation" - : "declaration", - ...(methodTypeText(inner) !== undefined - ? { returnType: methodTypeText(inner) } - : {}), + declarationRole: inner.type === 'method_definition' ? 'implementation' : 'declaration', + ...(methodTypeText(inner) !== undefined ? { returnType: methodTypeText(inner) } : {}), parameterTypes, parameterNames, }; @@ -867,7 +766,7 @@ export function collectObjectiveCFacts( ...collectLocalTypes(inner), ]); walkNamedTree(inner, (messageNode) => { - if (messageNode.type !== "message_expression") return; + if (messageNode.type !== 'message_expression') return; const receiver = messageReceiver(messageNode); if (receiver === null) return; const messageSel = messageSelector(messageNode); @@ -885,9 +784,7 @@ export function collectObjectiveCFacts( selector: messageSel, receiverText: receiver.text, receiverKind: classified.kind, - ...(classified.type !== undefined - ? { receiverType: classified.type } - : {}), + ...(classified.type !== undefined ? { receiverType: classified.type } : {}), sourceMethodQualifiedName: method.qualifiedName, sourceMethodId: method.nodeId, sourceOwnerQualifiedName: method.ownerQualifiedName, @@ -910,26 +807,21 @@ export function collectObjectiveCFacts( } messages.push(fact); }); - } else if (inner.type === "property_declaration") { + } else if (inner.type === 'property_declaration') { const prop = propertyInfo(inner); if (prop === null) continue; - const qualifiedName = objcPropertyQualifiedName( - container.qualifiedName, - prop.name, - ); + const qualifiedName = objcPropertyQualifiedName(container.qualifiedName, prop.name); const { startLine, endLine } = range(inner); members.push({ - kind: "property", + kind: 'property', name: prop.name, qualifiedName, - nodeId: graphNodeId("Property", qualifiedName), + nodeId: graphNodeId('Property', qualifiedName), ownerQualifiedName: container.qualifiedName, ownerName: container.name, ownerKind: container.kind, ownerLabel: ownerLabel(container.kind), - ...(container.hostClass !== undefined - ? { hostClass: container.hostClass } - : {}), + ...(container.hostClass !== undefined ? { hostClass: container.hostClass } : {}), ...(prop.type !== undefined ? { declaredType: prop.type } : {}), filePath, startLine, @@ -937,34 +829,25 @@ export function collectObjectiveCFacts( }); addMemberType(container.qualifiedName, prop.name, prop.type); if (container.hostClass !== undefined) { - addMemberType( - objcClassQualifiedName(container.hostClass), - prop.name, - prop.type, - ); + addMemberType(objcClassQualifiedName(container.hostClass), prop.name, prop.type); } - } else if (inner.type === "instance_variables") { + } else if (inner.type === 'instance_variables') { walkNamedTree(inner, (ivarNode) => { - if (ivarNode.type !== "instance_variable") return; + if (ivarNode.type !== 'instance_variable') return; const ivar = declarationNameAndType(ivarNode); if (ivar === null) return; - const qualifiedName = objcIvarQualifiedName( - container.qualifiedName, - ivar.name, - ); + const qualifiedName = objcIvarQualifiedName(container.qualifiedName, ivar.name); const { startLine, endLine } = range(ivarNode); members.push({ - kind: "ivar", + kind: 'ivar', name: ivar.name, qualifiedName, - nodeId: graphNodeId("Variable", qualifiedName), + nodeId: graphNodeId('Variable', qualifiedName), ownerQualifiedName: container.qualifiedName, ownerName: container.name, ownerKind: container.kind, ownerLabel: ownerLabel(container.kind), - ...(container.hostClass !== undefined - ? { hostClass: container.hostClass } - : {}), + ...(container.hostClass !== undefined ? { hostClass: container.hostClass } : {}), ...(ivar.type !== undefined ? { declaredType: ivar.type } : {}), filePath, startLine, @@ -972,27 +855,17 @@ export function collectObjectiveCFacts( }); addMemberType(container.qualifiedName, ivar.name, ivar.type); if (container.hostClass !== undefined) { - addMemberType( - objcClassQualifiedName(container.hostClass), - ivar.name, - ivar.type, - ); + addMemberType(objcClassQualifiedName(container.hostClass), ivar.name, ivar.type); } }); - } else if (inner.type === "implementation_definition") { - const methodNode = directNamedChildren(inner).find( - (n) => n.type === "method_definition", - ); + } else if (inner.type === 'implementation_definition') { + const methodNode = directNamedChildren(inner).find((n) => n.type === 'method_definition'); if (methodNode !== undefined) { const selector = methodSelector(methodNode); const { parameterTypes, parameterNames, typeBindings } = methodParameterInfo(methodNode); const kind = methodKind(methodNode); - const qualifiedName = objcMethodQualifiedName( - container.qualifiedName, - kind, - selector, - ); + const qualifiedName = objcMethodQualifiedName(container.qualifiedName, kind, selector); const { startLine, endLine } = range(methodNode); const method: ObjCMethodFact = { name: selector, @@ -1001,15 +874,13 @@ export function collectObjectiveCFacts( ownerQualifiedName: container.qualifiedName, ownerName: container.name, ownerKind: container.kind, - ...(container.hostClass !== undefined - ? { hostClass: container.hostClass } - : {}), + ...(container.hostClass !== undefined ? { hostClass: container.hostClass } : {}), qualifiedName, - nodeId: graphNodeId("Method", qualifiedName), + nodeId: graphNodeId('Method', qualifiedName), filePath, startLine, endLine, - declarationRole: "implementation", + declarationRole: 'implementation', ...(methodTypeText(methodNode) !== undefined ? { returnType: methodTypeText(methodNode) } : {}), @@ -1022,7 +893,7 @@ export function collectObjectiveCFacts( ...collectLocalTypes(methodNode), ]); walkNamedTree(methodNode, (messageNode) => { - if (messageNode.type !== "message_expression") return; + if (messageNode.type !== 'message_expression') return; const receiver = messageReceiver(messageNode); if (receiver === null) return; const messageSel = messageSelector(messageNode); @@ -1040,9 +911,7 @@ export function collectObjectiveCFacts( selector: messageSel, receiverText: receiver.text, receiverKind: classified.kind, - ...(classified.type !== undefined - ? { receiverType: classified.type } - : {}), + ...(classified.type !== undefined ? { receiverType: classified.type } : {}), sourceMethodQualifiedName: method.qualifiedName, sourceMethodId: method.nodeId, sourceOwnerQualifiedName: method.ownerQualifiedName, @@ -1071,7 +940,7 @@ export function collectObjectiveCFacts( continue; } - if (child.type === "function_definition" || child.type === "declaration") { + if (child.type === 'function_definition' || child.type === 'declaration') { const info = functionInfo(child); if (info === null) continue; const qualifiedName = objcFunctionQualifiedName(info.name); @@ -1079,13 +948,11 @@ export function collectObjectiveCFacts( functions.push({ name: info.name, qualifiedName, - nodeId: graphNodeId("Function", qualifiedName), + nodeId: graphNodeId('Function', qualifiedName), filePath, startLine, endLine, - ...(info.returnType !== undefined - ? { returnType: info.returnType } - : {}), + ...(info.returnType !== undefined ? { returnType: info.returnType } : {}), parameterTypes: info.parameterTypes, }); } @@ -1135,7 +1002,7 @@ function semanticNode( } function relationship( - type: "DEFINES" | "DECLARES" | "HAS_METHOD" | "HAS_PROPERTY", + type: 'DEFINES' | 'DECLARES' | 'HAS_METHOD' | 'HAS_PROPERTY', sourceId: string, targetId: string, reason: string, @@ -1159,27 +1026,25 @@ function implementationEvidenceNode( extras: Record, ): ProviderSemanticNode { return semanticNode( - "CodeElement", - graphNodeId("CodeElement", qualifiedName), + 'CodeElement', + graphNodeId('CodeElement', qualifiedName), name, qualifiedName, filePath, startLine, endLine, { - objectiveCKind: "implementation-evidence", + objectiveCKind: 'implementation-evidence', ...extras, }, ); } -export function buildObjectiveCSemanticGraph( - facts: ObjCFileFacts, -): ProviderSemanticGraph { +export function buildObjectiveCSemanticGraph(facts: ObjCFileFacts): ProviderSemanticGraph { const nodes: ProviderSemanticNode[] = []; const relationships: ProviderSemanticRelationship[] = []; const symbols: ProviderSemanticSymbol[] = []; - const fileId = generateId("File", facts.filePath); + const fileId = generateId('File', facts.filePath); for (const container of facts.containers) { nodes.push( @@ -1194,27 +1059,17 @@ export function buildObjectiveCSemanticGraph( { objectiveCKind: container.kind, declarationRole: container.declarationRole, - ...(container.superclass !== undefined - ? { superclass: container.superclass } - : {}), - ...(container.protocols.length > 0 - ? { protocols: [...container.protocols] } - : {}), - ...(container.hostClass !== undefined - ? { hostClass: container.hostClass } - : {}), - ...(container.categoryName !== undefined - ? { categoryName: container.categoryName } - : {}), + ...(container.superclass !== undefined ? { superclass: container.superclass } : {}), + ...(container.protocols.length > 0 ? { protocols: [...container.protocols] } : {}), + ...(container.hostClass !== undefined ? { hostClass: container.hostClass } : {}), + ...(container.categoryName !== undefined ? { categoryName: container.categoryName } : {}), }, ), ); - relationships.push( - relationship("DEFINES", fileId, container.nodeId, "objc-definition"), - ); - if (container.declarationRole === "implementation") { + relationships.push(relationship('DEFINES', fileId, container.nodeId, 'objc-definition')); + if (container.declarationRole === 'implementation') { const qualifiedName = `objc:implementation:${container.qualifiedName}:${facts.filePath}:${container.startLine}`; - const evidenceId = graphNodeId("CodeElement", qualifiedName); + const evidenceId = graphNodeId('CodeElement', qualifiedName); nodes.push( implementationEvidenceNode( qualifiedName, @@ -1229,19 +1084,14 @@ export function buildObjectiveCSemanticGraph( ), ); relationships.push( - relationship( - "DEFINES", - fileId, - evidenceId, - "objc: implementation evidence", - ), + relationship('DEFINES', fileId, evidenceId, 'objc: implementation evidence'), ); relationships.push( relationship( - "DECLARES", + 'DECLARES', evidenceId, container.nodeId, - "objc: implementation of merged symbol", + 'objc: implementation of merged symbol', ), ); } @@ -1257,7 +1107,7 @@ export function buildObjectiveCSemanticGraph( for (const method of facts.methods) { nodes.push( semanticNode( - "Method", + 'Method', method.nodeId, method.selector, method.qualifiedName, @@ -1273,29 +1123,23 @@ export function buildObjectiveCSemanticGraph( declarationRole: method.declarationRole, parameterCount: method.parameterTypes.length, parameterTypes: [...method.parameterTypes], - ...(method.returnType !== undefined - ? { returnType: method.returnType } - : {}), - ...(method.hostClass !== undefined - ? { hostClass: method.hostClass } - : {}), + ...(method.returnType !== undefined ? { returnType: method.returnType } : {}), + ...(method.hostClass !== undefined ? { hostClass: method.hostClass } : {}), }, ), ); const ownerId = graphNodeId( - method.ownerKind === "class" - ? "Class" - : method.ownerKind === "protocol" - ? "Protocol" - : "Category", + method.ownerKind === 'class' + ? 'Class' + : method.ownerKind === 'protocol' + ? 'Protocol' + : 'Category', method.ownerQualifiedName, ); - relationships.push( - relationship("HAS_METHOD", ownerId, method.nodeId, "objc-owner-method"), - ); - if (method.declarationRole === "implementation") { + relationships.push(relationship('HAS_METHOD', ownerId, method.nodeId, 'objc-owner-method')); + if (method.declarationRole === 'implementation') { const qualifiedName = `objc:method-implementation:${method.qualifiedName}:${facts.filePath}:${method.startLine}`; - const evidenceId = graphNodeId("CodeElement", qualifiedName); + const evidenceId = graphNodeId('CodeElement', qualifiedName); nodes.push( implementationEvidenceNode( qualifiedName, @@ -1304,7 +1148,7 @@ export function buildObjectiveCSemanticGraph( method.startLine, method.endLine, { - implementationKind: "method", + implementationKind: 'method', targetQualifiedName: method.qualifiedName, selector: method.selector, methodKind: method.methodKind, @@ -1313,29 +1157,24 @@ export function buildObjectiveCSemanticGraph( ), ); relationships.push( - relationship( - "DEFINES", - fileId, - evidenceId, - "objc: implementation evidence", - ), + relationship('DEFINES', fileId, evidenceId, 'objc: implementation evidence'), ); relationships.push( relationship( - "DECLARES", + 'DECLARES', evidenceId, method.nodeId, - "objc: implementation of merged symbol", + 'objc: implementation of merged symbol', ), ); } if (method.hostClass !== undefined) { relationships.push( relationship( - "HAS_METHOD", - graphNodeId("Class", objcClassQualifiedName(method.hostClass)), + 'HAS_METHOD', + graphNodeId('Class', objcClassQualifiedName(method.hostClass)), method.nodeId, - "objc-category-host-method", + 'objc-category-host-method', ), ); } @@ -1343,22 +1182,19 @@ export function buildObjectiveCSemanticGraph( filePath: facts.filePath, name: method.selector, nodeId: method.nodeId, - type: "Method", + type: 'Method', qualifiedName: method.qualifiedName, parameterCount: method.parameterTypes.length, requiredParameterCount: method.parameterTypes.length, parameterTypes: [...method.parameterTypes], - ...(method.returnType !== undefined - ? { returnType: method.returnType } - : {}), + ...(method.returnType !== undefined ? { returnType: method.returnType } : {}), ownerId, - isStatic: method.methodKind === "+", + isStatic: method.methodKind === '+', }); } for (const member of facts.members) { - const label: NodeLabel = - member.kind === "property" ? "Property" : "Variable"; + const label: NodeLabel = member.kind === 'property' ? 'Property' : 'Variable'; nodes.push( semanticNode( label, @@ -1370,15 +1206,13 @@ export function buildObjectiveCSemanticGraph( member.endLine, { objectiveCKind: member.kind, - ...(member.declaredType !== undefined - ? { declaredType: member.declaredType } - : {}), + ...(member.declaredType !== undefined ? { declaredType: member.declaredType } : {}), }, ), ); relationships.push( relationship( - member.kind === "property" ? "HAS_PROPERTY" : "HAS_PROPERTY", + member.kind === 'property' ? 'HAS_PROPERTY' : 'HAS_PROPERTY', graphNodeId(member.ownerLabel, member.ownerQualifiedName), member.nodeId, `objc-${member.kind}`, @@ -1387,8 +1221,8 @@ export function buildObjectiveCSemanticGraph( if (member.hostClass !== undefined) { relationships.push( relationship( - "HAS_PROPERTY", - graphNodeId("Class", objcClassQualifiedName(member.hostClass)), + 'HAS_PROPERTY', + graphNodeId('Class', objcClassQualifiedName(member.hostClass)), member.nodeId, `objc-category-host-${member.kind}`, ), @@ -1400,9 +1234,7 @@ export function buildObjectiveCSemanticGraph( nodeId: member.nodeId, type: label, qualifiedName: member.qualifiedName, - ...(member.declaredType !== undefined - ? { declaredType: member.declaredType } - : {}), + ...(member.declaredType !== undefined ? { declaredType: member.declaredType } : {}), ownerId: graphNodeId(member.ownerLabel, member.ownerQualifiedName), }); } @@ -1410,7 +1242,7 @@ export function buildObjectiveCSemanticGraph( for (const fn of facts.functions) { nodes.push( semanticNode( - "Function", + 'Function', fn.nodeId, fn.name, fn.qualifiedName, @@ -1424,14 +1256,12 @@ export function buildObjectiveCSemanticGraph( }, ), ); - relationships.push( - relationship("DEFINES", fileId, fn.nodeId, "objc-c-function"), - ); + relationships.push(relationship('DEFINES', fileId, fn.nodeId, 'objc-c-function')); symbols.push({ filePath: facts.filePath, name: fn.name, nodeId: fn.nodeId, - type: "Function", + type: 'Function', qualifiedName: fn.qualifiedName, parameterCount: fn.parameterTypes.length, parameterTypes: [...fn.parameterTypes], @@ -1441,10 +1271,10 @@ export function buildObjectiveCSemanticGraph( for (const imp of facts.imports) { const qualifiedName = `objc:import:${facts.filePath}:${imp.startLine}:${imp.kind}:${imp.targetRaw}`; - const id = graphNodeId("Import", qualifiedName); + const id = graphNodeId('Import', qualifiedName); nodes.push( semanticNode( - "Import", + 'Import', id, imp.targetRaw, qualifiedName, @@ -1452,29 +1282,29 @@ export function buildObjectiveCSemanticGraph( imp.startLine, imp.endLine, { - objectiveCKind: "import", + objectiveCKind: 'import', importKind: imp.kind, raw: imp.raw, targetRaw: imp.targetRaw, }, ), ); - relationships.push(relationship("DEFINES", fileId, id, "objc-import")); + relationships.push(relationship('DEFINES', fileId, id, 'objc-import')); symbols.push({ filePath: facts.filePath, name: imp.targetRaw, nodeId: id, - type: "Import", + type: 'Import', qualifiedName, }); } for (const unresolved of facts.unresolvedMessages) { const qn = `objc:unresolved:${facts.filePath}:${unresolved.startLine}:${unresolved.startCol}:${unresolved.selector}`; - const id = graphNodeId("CodeElement", qn); + const id = graphNodeId('CodeElement', qn); nodes.push( semanticNode( - "CodeElement", + 'CodeElement', id, `[${unresolved.receiverText} ${unresolved.selector}]`, qn, @@ -1482,23 +1312,21 @@ export function buildObjectiveCSemanticGraph( unresolved.startLine, unresolved.startLine, { - objectiveCKind: "unresolved-message", + objectiveCKind: 'unresolved-message', selector: unresolved.selector, receiver: unresolved.receiverText, - resolution: "unresolved", + resolution: 'unresolved', reason: unresolved.reason, sourceMethod: unresolved.sourceMethodQualifiedName, }, ), ); - relationships.push( - relationship("DEFINES", fileId, id, "objc-unresolved-message"), - ); + relationships.push(relationship('DEFINES', fileId, id, 'objc-unresolved-message')); symbols.push({ filePath: facts.filePath, name: `[${unresolved.receiverText} ${unresolved.selector}]`, nodeId: id, - type: "CodeElement", + type: 'CodeElement', qualifiedName: qn, }); } @@ -1506,12 +1334,7 @@ export function buildObjectiveCSemanticGraph( return { nodes, relationships, symbols }; } -function captureAt( - name: string, - text: string, - startLine: number, - endLine: number, -): Capture { +function captureAt(name: string, text: string, startLine: number, endLine: number): Capture { return { name, text, @@ -1530,26 +1353,21 @@ export function buildObjectiveCScopeCaptures( ): readonly CaptureMatch[] { const captures: CaptureMatch[] = [ { - "@scope.module": nodeToCapture("@scope.module", root), + '@scope.module': nodeToCapture('@scope.module', root), }, ]; for (const imp of facts.imports) { - const anchor = captureAt( - "@import.statement", - imp.raw, - imp.startLine, - imp.endLine, - ); + const anchor = captureAt('@import.statement', imp.raw, imp.startLine, imp.endLine); captures.push({ - "@import.statement": anchor, - "@import.source": { + '@import.statement': anchor, + '@import.source': { ...anchor, - name: "@import.source", + name: '@import.source', text: imp.targetRaw, }, - "@import.name": { ...anchor, name: "@import.name", text: imp.targetRaw }, - "@import.kind": { ...anchor, name: "@import.kind", text: imp.kind }, + '@import.name': { ...anchor, name: '@import.name', text: imp.targetRaw }, + '@import.kind': { ...anchor, name: '@import.kind', text: imp.kind }, }); } diff --git a/gitnexus/src/core/ingestion/languages/objective-c/scope-resolver.ts b/gitnexus/src/core/ingestion/languages/objective-c/scope-resolver.ts index 8bbf5b0a9..fdf35a341 100644 --- a/gitnexus/src/core/ingestion/languages/objective-c/scope-resolver.ts +++ b/gitnexus/src/core/ingestion/languages/objective-c/scope-resolver.ts @@ -1,42 +1,28 @@ -import path from "path"; -import { - SupportedLanguages, - type ParsedFile, - type SymbolDefinition, - type Callsite, -} from "gitnexus-shared"; -import type { - GraphNode, - GraphRelationship, - RelationshipType, -} from "gitnexus-shared"; -import type { KnowledgeGraph } from "../../../graph/types.js"; -import type { ScopeResolver } from "../../scope-resolution/contract/scope-resolver.js"; -import { generateId } from "../../../../lib/utils.js"; -import { perFileSet } from "../../import-resolvers/per-file-set.js"; -import { objectiveCProvider } from "../objective-c.js"; +import path from 'path'; +import { SupportedLanguages, type SymbolDefinition, type Callsite } from 'gitnexus-shared'; +import type { GraphNode, RelationshipType } from 'gitnexus-shared'; +import type { KnowledgeGraph } from '../../../graph/types.js'; +import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js'; +import { generateId } from '../../../../lib/utils.js'; +import { perFileSet } from '../../import-resolvers/per-file-set.js'; +import { objectiveCProvider } from '../objective-c.js'; import { applyObjectiveCCaptureSideChannel, - objcCategoryQualifiedName, objcClassQualifiedName, - objcMethodQualifiedName, objcProtocolQualifiedName, objectiveCFactsFromParsedFiles, type ObjCContainerFact, type ObjCFileFacts, type ObjCMessageFact, type ObjCMethodFact, -} from "./facts.js"; +} from './facts.js'; interface ObjCWorkspaceFacts { readonly containersByQualifiedName: ReadonlyMap; readonly classByName: ReadonlyMap; readonly protocolsByName: ReadonlyMap; readonly categoriesByHost: ReadonlyMap; - readonly methodsByDispatchOwner: ReadonlyMap< - string, - readonly ObjCMethodFact[] - >; + readonly methodsByDispatchOwner: ReadonlyMap; readonly methodsByExactOwner: ReadonlyMap; readonly classProtocols: ReadonlyMap>; readonly superclassByClass: ReadonlyMap; @@ -45,7 +31,7 @@ interface ObjCWorkspaceFacts { export const objectiveCScopeResolver: ScopeResolver = { language: SupportedLanguages.ObjectiveC, languageProvider: objectiveCProvider, - importEdgeReason: "objective-c-scope: import", + importEdgeReason: 'objective-c-scope: import', resolveImportTarget: (targetRaw, fromFile, allFilePaths) => resolveObjectiveCImportTarget(targetRaw, fromFile, allFilePaths), @@ -53,18 +39,15 @@ export const objectiveCScopeResolver: ScopeResolver = { mergeBindings: (existing, incoming) => [...existing, ...incoming], arityCompatibility: (callsite: Callsite, def: SymbolDefinition) => { - if (callsite.arity === undefined || def.parameterCount === undefined) - return "unknown"; - return callsite.arity === def.parameterCount - ? "compatible" - : "incompatible"; + if (callsite.arity === undefined || def.parameterCount === undefined) return 'unknown'; + return callsite.arity === def.parameterCount ? 'compatible' : 'incompatible'; }, buildMro: () => new Map(), applyCaptureSideChannel: applyObjectiveCCaptureSideChannel, populateOwners: () => {}, - isSuperReceiver: (receiverText) => receiverText.trim() === "super", + isSuperReceiver: (receiverText) => receiverText.trim() === 'super', fieldFallbackOnMethodLookup: false, propagatesReturnTypesAcrossImports: false, @@ -115,15 +98,11 @@ function addRelationship( }); } -function labelForContainer( - container: ObjCContainerFact, -): "Class" | "Protocol" | "Category" { +function labelForContainer(container: ObjCContainerFact): 'Class' | 'Protocol' | 'Category' { return container.label; } -function buildObjectiveCWorkspaceFacts( - facts: readonly ObjCFileFacts[], -): ObjCWorkspaceFacts { +function buildObjectiveCWorkspaceFacts(facts: readonly ObjCFileFacts[]): ObjCWorkspaceFacts { const containersByQualifiedName = new Map(); const classByName = new Map(); const protocolsByName = new Map(); @@ -140,7 +119,7 @@ function buildObjectiveCWorkspaceFacts( container.qualifiedName, mergeContainerFacts(existing, container), ); - if (container.kind === "class") { + if (container.kind === 'class') { classByName.set(container.name, container); if (container.superclass !== undefined) superclassByClass.set(container.name, container.superclass); @@ -152,7 +131,7 @@ function buildObjectiveCWorkspaceFacts( } for (const protocol of container.protocols) protocols.add(protocol); } - } else if (container.kind === "protocol") { + } else if (container.kind === 'protocol') { protocolsByName.set(container.name, container); } else if (container.hostClass !== undefined) { let categories = categoriesByHost.get(container.hostClass); @@ -176,11 +155,7 @@ function buildObjectiveCWorkspaceFacts( appendMap(methodsByExactOwner, method.ownerQualifiedName, method); appendMap(methodsByDispatchOwner, method.ownerQualifiedName, method); if (method.hostClass !== undefined) { - appendMap( - methodsByDispatchOwner, - objcClassQualifiedName(method.hostClass), - method, - ); + appendMap(methodsByDispatchOwner, objcClassQualifiedName(method.hostClass), method); } } } @@ -202,16 +177,13 @@ function mergeContainerFacts( incoming: ObjCContainerFact, ): ObjCContainerFact { if (existing === undefined) return incoming; - const protocols = Array.from( - new Set([...existing.protocols, ...incoming.protocols]), - ).sort(); + const protocols = Array.from(new Set([...existing.protocols, ...incoming.protocols])).sort(); return { ...existing, declarationRole: - existing.declarationRole === "implementation" || - incoming.declarationRole === "implementation" - ? "implementation" - : "interface", + existing.declarationRole === 'implementation' || incoming.declarationRole === 'implementation' + ? 'implementation' + : 'interface', startLine: Math.min(existing.startLine, incoming.startLine), endLine: Math.max(existing.endLine, incoming.endLine), ...(existing.superclass !== undefined || incoming.superclass !== undefined @@ -233,67 +205,55 @@ function emitObjectiveCHeritageEdges( workspace: ObjCWorkspaceFacts, ): void { for (const container of facts.containers) { - const sourceId = graphNodeId( - labelForContainer(container), - container.qualifiedName, - ); - if (container.kind === "class" && container.superclass !== undefined) { + const sourceId = graphNodeId(labelForContainer(container), container.qualifiedName); + if (container.kind === 'class' && container.superclass !== undefined) { const superclass = workspace.classByName.get(container.superclass); if (superclass !== undefined) { addRelationship( graph, - "EXTENDS", + 'EXTENDS', sourceId, - graphNodeId("Class", superclass.qualifiedName), - "objc: superclass", + graphNodeId('Class', superclass.qualifiedName), + 'objc: superclass', ); } } const protocolSourceId = container.hostClass !== undefined - ? graphNodeId("Class", objcClassQualifiedName(container.hostClass)) + ? graphNodeId('Class', objcClassQualifiedName(container.hostClass)) : sourceId; for (const protocolName of container.protocols) { const protocol = workspace.protocolsByName.get(protocolName); if (protocol === undefined) continue; addRelationship( graph, - "IMPLEMENTS", + 'IMPLEMENTS', protocolSourceId, - graphNodeId("Protocol", protocol.qualifiedName), - "objc: protocol conformance", + graphNodeId('Protocol', protocol.qualifiedName), + 'objc: protocol conformance', ); } } } -function emitObjectiveCCategoryEdges( - graph: KnowledgeGraph, - facts: ObjCFileFacts, -): void { +function emitObjectiveCCategoryEdges(graph: KnowledgeGraph, facts: ObjCFileFacts): void { for (const container of facts.containers) { if (container.hostClass === undefined) continue; addRelationship( graph, - "MEMBER_OF", - graphNodeId("Category", container.qualifiedName), - graphNodeId("Class", objcClassQualifiedName(container.hostClass)), - "objc: category host class", + 'MEMBER_OF', + graphNodeId('Category', container.qualifiedName), + graphNodeId('Class', objcClassQualifiedName(container.hostClass)), + 'objc: category host class', ); } } -function emitObjectiveCImplementationEvidence( - graph: KnowledgeGraph, - facts: ObjCFileFacts, -): void { +function emitObjectiveCImplementationEvidence(graph: KnowledgeGraph, facts: ObjCFileFacts): void { for (const container of facts.containers) { - if (container.declarationRole !== "implementation") continue; - const targetId = graphNodeId( - labelForContainer(container), - container.qualifiedName, - ); + if (container.declarationRole !== 'implementation') continue; + const targetId = graphNodeId(labelForContainer(container), container.qualifiedName); emitImplementationEvidence( graph, facts.filePath, @@ -303,7 +263,7 @@ function emitObjectiveCImplementationEvidence( container.startLine, container.endLine, { - objectiveCKind: "implementation-evidence", + objectiveCKind: 'implementation-evidence', implementationKind: container.kind, targetQualifiedName: container.qualifiedName, }, @@ -311,7 +271,7 @@ function emitObjectiveCImplementationEvidence( } for (const method of facts.methods) { - if (method.declarationRole !== "implementation") continue; + if (method.declarationRole !== 'implementation') continue; emitImplementationEvidence( graph, facts.filePath, @@ -321,8 +281,8 @@ function emitObjectiveCImplementationEvidence( method.startLine, method.endLine, { - objectiveCKind: "implementation-evidence", - implementationKind: "method", + objectiveCKind: 'implementation-evidence', + implementationKind: 'method', targetQualifiedName: method.qualifiedName, selector: method.selector, methodKind: method.methodKind, @@ -342,10 +302,10 @@ function emitImplementationEvidence( endLine: number, extras: Record, ): void { - const nodeId = graphNodeId("CodeElement", qualifiedName); + const nodeId = graphNodeId('CodeElement', qualifiedName); graph.addNode({ id: nodeId, - label: "CodeElement", + label: 'CodeElement', properties: { name, qualifiedName, @@ -359,20 +319,13 @@ function emitImplementationEvidence( }); addRelationship( graph, - "DEFINES", - graphNodeId("File", filePath), + 'DEFINES', + graphNodeId('File', filePath), nodeId, - "objc: implementation evidence", - 1, - ); - addRelationship( - graph, - "DECLARES", - nodeId, - targetId, - "objc: implementation of merged symbol", + 'objc: implementation evidence', 1, ); + addRelationship(graph, 'DECLARES', nodeId, targetId, 'objc: implementation of merged symbol', 1); } function emitObjectiveCMessageEdges( @@ -382,37 +335,31 @@ function emitObjectiveCMessageEdges( ): void { for (const message of facts.messages) { const targets = resolveMessageTargets(message, workspace); - if (targets.kind === "none") continue; - if (targets.kind === "protocol") { - emitProtocolMessageEvidence( - graph, - facts, - message, - targets.protocolName, - targets.candidates, - ); + if (targets.kind === 'none') continue; + if (targets.kind === 'protocol') { + emitProtocolMessageEvidence(graph, facts, message, targets.protocolName, targets.candidates); } for (const target of targets.methods) { if (graph.getNode(target.nodeId) === undefined) continue; addRelationship( graph, - "CALLS", + 'CALLS', message.sourceMethodId, target.nodeId, - targets.kind === "protocol" - ? "objc-message: protocol receiver" + targets.kind === 'protocol' + ? 'objc-message: protocol receiver' : `objc-message: ${message.receiverKind} receiver`, - targets.kind === "protocol" ? 0.8 : 0.9, + targets.kind === 'protocol' ? 0.8 : 0.9, ); } } } type MessageTargets = - | { readonly kind: "none"; readonly methods: readonly ObjCMethodFact[] } - | { readonly kind: "direct"; readonly methods: readonly ObjCMethodFact[] } + | { readonly kind: 'none'; readonly methods: readonly ObjCMethodFact[] } + | { readonly kind: 'direct'; readonly methods: readonly ObjCMethodFact[] } | { - readonly kind: "protocol"; + readonly kind: 'protocol'; readonly protocolName: string; readonly methods: readonly ObjCMethodFact[]; readonly candidates: readonly ObjCMethodFact[]; @@ -422,55 +369,41 @@ function resolveMessageTargets( message: ObjCMessageFact, workspace: ObjCWorkspaceFacts, ): MessageTargets { - if ( - message.receiverKind === "dynamic" || - message.receiverKind === "unknown" - ) { - return { kind: "none", methods: [] }; + if (message.receiverKind === 'dynamic' || message.receiverKind === 'unknown') { + return { kind: 'none', methods: [] }; } - if (message.receiverKind === "class") { + if (message.receiverKind === 'class') { const className = message.receiverType?.name ?? message.receiverText; return { - kind: "direct", - methods: findDispatchMethods(workspace, className, "+", message.selector), + kind: 'direct', + methods: findDispatchMethods(workspace, className, '+', message.selector), }; } - if (message.receiverKind === "self") { - const owner = workspace.containersByQualifiedName.get( - message.sourceOwnerQualifiedName, - ); - const className = - owner?.hostClass ?? owner?.name ?? message.sourceOwnerName; + if (message.receiverKind === 'self') { + const owner = workspace.containersByQualifiedName.get(message.sourceOwnerQualifiedName); + const className = owner?.hostClass ?? owner?.name ?? message.sourceOwnerName; const methods = - owner?.kind === "protocol" + owner?.kind === 'protocol' ? findExactOwnerMethods( workspace, owner.qualifiedName, message.sourceMethodKind, message.selector, ) - : findDispatchMethods( - workspace, - className, - message.sourceMethodKind, - message.selector, - ); - return { kind: "direct", methods }; + : findDispatchMethods(workspace, className, message.sourceMethodKind, message.selector); + return { kind: 'direct', methods }; } - if (message.receiverKind === "super") { - const owner = workspace.containersByQualifiedName.get( - message.sourceOwnerQualifiedName, - ); - const className = - owner?.hostClass ?? owner?.name ?? message.sourceOwnerName; + if (message.receiverKind === 'super') { + const owner = workspace.containersByQualifiedName.get(message.sourceOwnerQualifiedName); + const className = owner?.hostClass ?? owner?.name ?? message.sourceOwnerName; const superclass = workspace.superclassByClass.get(className); return superclass === undefined - ? { kind: "none", methods: [] } + ? { kind: 'none', methods: [] } : { - kind: "direct", + kind: 'direct', methods: findDispatchMethods( workspace, superclass, @@ -481,46 +414,36 @@ function resolveMessageTargets( } const receiverType = message.receiverType; - if (receiverType?.kind === "class" && receiverType.name !== undefined) { + if (receiverType?.kind === 'class' && receiverType.name !== undefined) { return { - kind: "direct", - methods: findDispatchMethods( - workspace, - receiverType.name, - "-", - message.selector, - ), + kind: 'direct', + methods: findDispatchMethods(workspace, receiverType.name, '-', message.selector), }; } - if (receiverType?.kind === "protocol" && receiverType.name !== undefined) { + if (receiverType?.kind === 'protocol' && receiverType.name !== undefined) { const protocolQn = objcProtocolQualifiedName(receiverType.name); - const methods = findExactOwnerMethods( - workspace, - protocolQn, - "-", - message.selector, - ); + const methods = findExactOwnerMethods(workspace, protocolQn, '-', message.selector); const candidates = findProtocolImplementationCandidates( workspace, receiverType.name, message.selector, ); return { - kind: "protocol", + kind: 'protocol', protocolName: receiverType.name, methods, candidates, }; } - return { kind: "none", methods: [] }; + return { kind: 'none', methods: [] }; } function findDispatchMethods( workspace: ObjCWorkspaceFacts, className: string, - methodKind: "-" | "+", + methodKind: '-' | '+', selector: string, ): readonly ObjCMethodFact[] { const seen = new Set(); @@ -528,11 +451,8 @@ function findDispatchMethods( while (currentClass !== undefined && !seen.has(currentClass)) { seen.add(currentClass); const ownerQn = objcClassQualifiedName(currentClass); - const methods = ( - workspace.methodsByDispatchOwner.get(ownerQn) ?? [] - ).filter( - (method) => - method.methodKind === methodKind && method.selector === selector, + const methods = (workspace.methodsByDispatchOwner.get(ownerQn) ?? []).filter( + (method) => method.methodKind === methodKind && method.selector === selector, ); if (methods.length > 0) return methods; currentClass = workspace.superclassByClass.get(currentClass); @@ -543,12 +463,11 @@ function findDispatchMethods( function findExactOwnerMethods( workspace: ObjCWorkspaceFacts, ownerQualifiedName: string, - methodKind: "-" | "+", + methodKind: '-' | '+', selector: string, ): readonly ObjCMethodFact[] { return (workspace.methodsByExactOwner.get(ownerQualifiedName) ?? []).filter( - (method) => - method.methodKind === methodKind && method.selector === selector, + (method) => method.methodKind === methodKind && method.selector === selector, ); } @@ -560,7 +479,7 @@ function findProtocolImplementationCandidates( const out: ObjCMethodFact[] = []; for (const [className, protocols] of workspace.classProtocols) { if (!protocols.has(protocolName)) continue; - out.push(...findDispatchMethods(workspace, className, "-", selector)); + out.push(...findDispatchMethods(workspace, className, '-', selector)); } return out; } @@ -574,10 +493,10 @@ function emitProtocolMessageEvidence( ): void { if (candidates.length === 0) return; const qualifiedName = `objc:protocol-candidates:${facts.filePath}:${message.startLine}:${message.startCol}:${message.selector}`; - const nodeId = graphNodeId("CodeElement", qualifiedName); + const nodeId = graphNodeId('CodeElement', qualifiedName); const node: GraphNode = { id: nodeId, - label: "CodeElement", + label: 'CodeElement', properties: { name: `[${message.receiverText} ${message.selector}] candidates`, qualifiedName, @@ -586,30 +505,28 @@ function emitProtocolMessageEvidence( endLine: message.startLine, language: SupportedLanguages.ObjectiveC, isExported: false, - objectiveCKind: "protocol-candidate-implementations", + objectiveCKind: 'protocol-candidate-implementations', protocolName, selector: message.selector, sourceMethod: message.sourceMethodQualifiedName, - candidateImplementations: candidates - .map((candidate) => candidate.qualifiedName) - .sort(), + candidateImplementations: candidates.map((candidate) => candidate.qualifiedName).sort(), }, }; graph.addNode(node); addRelationship( graph, - "DEFINES", - graphNodeId("File", facts.filePath), + 'DEFINES', + graphNodeId('File', facts.filePath), nodeId, - "objc: protocol receiver candidate evidence", + 'objc: protocol receiver candidate evidence', 1, ); addRelationship( graph, - "USES", + 'USES', message.sourceMethodId, nodeId, - "objc-message: protocol receiver candidates", + 'objc-message: protocol receiver candidates', 0.7, ); } @@ -623,9 +540,7 @@ function resolveObjectiveCImportTarget( const target = targetRaw.trim(); if (target.length === 0) return null; const looksLikeFileImport = - target.startsWith(".") || - target.includes("/") || - path.posix.extname(target).length > 0; + target.startsWith('.') || target.includes('/') || path.posix.extname(target).length > 0; if (!looksLikeFileImport) return null; return findImportCandidate(target, fromFile, importIndex); } @@ -648,9 +563,7 @@ function findImportCandidate( importIndex: ObjectiveCImportIndex, ): string | null { const normalizedTarget = normalizeRepoPath(targetRaw); - const fromDir = normalizeRepoPath( - path.posix.dirname(normalizeRepoPath(fromFile)), - ); + const fromDir = normalizeRepoPath(path.posix.dirname(normalizeRepoPath(fromFile))); const spelledCandidates = new Set([ normalizeRepoPath(path.posix.join(fromDir, normalizedTarget)), normalizedTarget, @@ -669,12 +582,11 @@ function findImportCandidate( const suffixes = [...spelledCandidates].map((candidate) => `/${candidate}`); for (const filePath of importIndex.filePaths) { const normalizedFilePath = normalizeRepoPath(filePath); - if (suffixes.some((suffix) => normalizedFilePath.endsWith(suffix))) - return filePath; + if (suffixes.some((suffix) => normalizedFilePath.endsWith(suffix))) return filePath; } return null; } function normalizeRepoPath(value: string): string { - return value.replaceAll("\\", "/").replace(/^\.\//, ""); + return value.replaceAll('\\', '/').replace(/^\.\//, ''); } diff --git a/gitnexus/test/fixtures/objective-c/SYModuleBridge.mm b/gitnexus/test/fixtures/objective-c/SYModuleBridge.mm new file mode 100644 index 000000000..ffbc923cd --- /dev/null +++ b/gitnexus/test/fixtures/objective-c/SYModuleBridge.mm @@ -0,0 +1,9 @@ +#import "SYModuleCaller.h" + +@interface SYModuleBridge : NSObject +- (void)bridgeValue:(NSInteger)value; +@end + +@implementation SYModuleBridge +- (void)bridgeValue:(NSInteger)value {} +@end diff --git a/gitnexus/test/helpers/grammar-introspection.ts b/gitnexus/test/helpers/grammar-introspection.ts index ce181eb48..3c422130a 100644 --- a/gitnexus/test/helpers/grammar-introspection.ts +++ b/gitnexus/test/helpers/grammar-introspection.ts @@ -105,7 +105,7 @@ interface NodeTypeEntry { /** Resolve the on-disk directory of an installed package, or null if absent. */ function resolvePackageDir(pkg: string): string | null { - // Vendored grammars (c/dart/proto/swift/kotlin) are NOT in node_modules — they + // Vendored grammars (c/dart/proto/swift/kotlin/objc) are NOT in node_modules — they // load from vendor/ by absolute path (vendored-grammars.ts / #2111), so resolve // their node-types.json from there rather than via _require.resolve. if (VENDORED_GRAMMAR_PACKAGES.has(pkg)) { diff --git a/gitnexus/test/integration/objective-c-provider.test.ts b/gitnexus/test/integration/objective-c-provider.test.ts index be47ad563..7380e8932 100644 --- a/gitnexus/test/integration/objective-c-provider.test.ts +++ b/gitnexus/test/integration/objective-c-provider.test.ts @@ -1,67 +1,62 @@ -import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { execSync } from "child_process"; -import fs from "fs"; -import os from "os"; -import path from "path"; -import { fileURLToPath } from "url"; -import type { GraphNode, RelationshipType } from "gitnexus-shared"; -import { runPipelineFromRepo } from "../../src/core/ingestion/pipeline.js"; -import { runFullAnalysis } from "../../src/core/run-analyze.js"; -import { LocalBackend } from "../../src/mcp/local/local-backend.js"; -import type { PipelineResult } from "../../src/types/pipeline.js"; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { execSync } from 'child_process'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import type { GraphNode, RelationshipType } from 'gitnexus-shared'; +import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js'; +import { runFullAnalysis } from '../../src/core/run-analyze.js'; +import { LocalBackend } from '../../src/mcp/local/local-backend.js'; +import type { PipelineResult } from '../../src/types/pipeline.js'; const FIXTURE_DIR = path.resolve( path.dirname(fileURLToPath(import.meta.url)), - "../fixtures/objective-c", + '../fixtures/objective-c', ); function readFixture(name: string): string { - return fs.readFileSync(path.join(FIXTURE_DIR, name), "utf8"); + return fs.readFileSync(path.join(FIXTURE_DIR, name), 'utf8'); } -const HEADER = readFixture("SYModuleCaller.h"); -const IMPL = readFixture("SYModuleCaller.m"); -const PLAIN_C_HEADER = readFixture("SYModuleSupport.h"); +const HEADER = readFixture('SYModuleCaller.h'); +const IMPL = readFixture('SYModuleCaller.m'); +const MM_IMPL = readFixture('SYModuleBridge.mm'); +const PLAIN_C_HEADER = readFixture('SYModuleSupport.h'); const HEADER_V2 = HEADER.replace( - "- (void)runTask:(NSString *)name completion:(void (^)(BOOL ok))completion;\n@end", - "- (void)runTask:(NSString *)name completion:(void (^)(BOOL ok))completion;\n- (void)cancelTask;\n@end", + '- (void)runTask:(NSString *)name completion:(void (^)(BOOL ok))completion;\n@end', + '- (void)runTask:(NSString *)name completion:(void (^)(BOOL ok))completion;\n- (void)cancelTask;\n@end', ).replace( - "- (void)traceEvent:(NSString *)name;\n@end", - "- (void)traceEvent:(NSString *)name;\n- (void)traceDetail:(NSString *)name level:(NSInteger)level;\n@end", + '- (void)traceEvent:(NSString *)name;\n@end', + '- (void)traceEvent:(NSString *)name;\n- (void)traceDetail:(NSString *)name level:(NSInteger)level;\n@end', ); const IMPL_V2 = IMPL.replace( - "[self traceEvent:name];", - "[self traceEvent:name];\n [self traceDetail:name level:1];", + '[self traceEvent:name];', + '[self traceEvent:name];\n [self traceDetail:name level:1];', ).replace( - "@implementation SYModuleCaller (Tracing)\n- (void)traceEvent:(NSString *)name {}\n@end", - "@implementation SYModuleCaller (Tracing)\n- (void)traceEvent:(NSString *)name {}\n- (void)traceDetail:(NSString *)name level:(NSInteger)level {}\n@end", + '@implementation SYModuleCaller (Tracing)\n- (void)traceEvent:(NSString *)name {}\n@end', + '@implementation SYModuleCaller (Tracing)\n- (void)traceEvent:(NSString *)name {}\n- (void)traceDetail:(NSString *)name level:(NSInteger)level {}\n@end', ); function git(repoRoot: string, command: string): void { - execSync(command, { cwd: repoRoot, stdio: "pipe" }); + execSync(command, { cwd: repoRoot, stdio: 'pipe' }); } function gitCommitAll(repoRoot: string, message: string): void { - git( - repoRoot, - "git -c user.name=test -c user.email=t@t -c commit.gpgsign=false add -A", - ); + git(repoRoot, 'git -c user.name=test -c user.email=t@t -c commit.gpgsign=false add -A'); git( repoRoot, `git -c user.name=test -c user.email=t@t -c commit.gpgsign=false commit -q -m "${message}"`, ); } -function writeObjectiveCRepo( - repoRoot: string, - header = HEADER, - impl = IMPL, -): void { - fs.writeFileSync(path.join(repoRoot, "SYModuleCaller.h"), header); - fs.writeFileSync(path.join(repoRoot, "SYModuleCaller.m"), impl); - fs.writeFileSync(path.join(repoRoot, "SYModuleSupport.h"), PLAIN_C_HEADER); +function writeObjectiveCRepo(repoRoot: string, header = HEADER, impl = IMPL): void { + fs.writeFileSync(path.join(repoRoot, 'SYModuleCaller.h'), header); + fs.writeFileSync(path.join(repoRoot, 'SYModuleCaller.m'), impl); + fs.writeFileSync(path.join(repoRoot, 'SYModuleBridge.mm'), MM_IMPL); + fs.writeFileSync(path.join(repoRoot, 'SYModuleSupport.h'), PLAIN_C_HEADER); } function normalizeRows(rows: unknown): unknown[] { @@ -77,8 +72,8 @@ function normalizeRows(rows: unknown): unknown[] { }; }) .sort((left, right) => - `${left.uid ?? ""}:${left.name ?? ""}:${left.filePath ?? ""}`.localeCompare( - `${right.uid ?? ""}:${right.name ?? ""}:${right.filePath ?? ""}`, + `${left.uid ?? ''}:${left.name ?? ''}:${left.filePath ?? ''}`.localeCompare( + `${right.uid ?? ''}:${right.name ?? ''}:${right.filePath ?? ''}`, ), ); } @@ -108,37 +103,35 @@ function normalizeContext(value: unknown): Record { }; } -async function readPersistedObjectiveCSurface( - repoRoot: string, -): Promise> { +async function readPersistedObjectiveCSurface(repoRoot: string): Promise> { const backend = new LocalBackend(); try { - const classContext = await backend.callTool("context", { - name: "SYModuleCaller", - file_path: "SYModuleCaller.h", + const classContext = await backend.callTool('context', { + name: 'SYModuleCaller', + file_path: 'SYModuleCaller.h', repo: repoRoot, }); - const runTaskContext = await backend.callTool("context", { - uid: "Method:objc:method:objc:class:SYModuleCaller:-:runTask:completion:", + const runTaskContext = await backend.callTool('context', { + uid: 'Method:objc:method:objc:class:SYModuleCaller:-:runTask:completion:', repo: repoRoot, }); - const queryResult = (await backend.callTool("query", { - search_query: "SYModuleCaller", + const queryResult = (await backend.callTool('query', { + search_query: 'SYModuleCaller', repo: repoRoot, limit: 5, include_content: false, })) as Record; - const protocolAndCategoryResult = await backend.callTool("cypher", { + const protocolAndCategoryResult = await backend.callTool('cypher', { query: "MATCH (n) WHERE labels(n) IN ['Protocol', 'Category'] " + - "RETURN n.id AS id, labels(n)[0] AS kind ORDER BY kind, id", + 'RETURN n.id AS id, labels(n)[0] AS kind ORDER BY kind, id', repo: repoRoot, }); - const categoryHostResult = await backend.callTool("cypher", { + const categoryHostResult = await backend.callTool('cypher', { query: - "MATCH (category:Category)-[r:CodeRelation]->(host:Class) " + + 'MATCH (category:Category)-[r:CodeRelation]->(host:Class) ' + "WHERE r.type = 'MEMBER_OF' " + - "RETURN category.id AS category, host.id AS host", + 'RETURN category.id AS category, host.id AS host', repo: repoRoot, }); return { @@ -174,17 +167,16 @@ async function analyzeObjectiveCRepo( return logs; } -describe("Objective-C provider integration", () => { +describe('Objective-C provider integration', () => { let repoRoot: string; let result: PipelineResult; beforeAll(async () => { - repoRoot = fs.mkdtempSync( - path.join(os.tmpdir(), "gitnexus-objc-provider-"), - ); - fs.writeFileSync(path.join(repoRoot, "SYModuleCaller.h"), HEADER); - fs.writeFileSync(path.join(repoRoot, "SYModuleCaller.m"), IMPL); - fs.writeFileSync(path.join(repoRoot, "SYModuleSupport.h"), PLAIN_C_HEADER); + repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-objc-provider-')); + fs.writeFileSync(path.join(repoRoot, 'SYModuleCaller.h'), HEADER); + fs.writeFileSync(path.join(repoRoot, 'SYModuleCaller.m'), IMPL); + fs.writeFileSync(path.join(repoRoot, 'SYModuleBridge.mm'), MM_IMPL); + fs.writeFileSync(path.join(repoRoot, 'SYModuleSupport.h'), PLAIN_C_HEADER); result = await runPipelineFromRepo(repoRoot, () => undefined, { workerPoolSize: 1, }); @@ -195,19 +187,15 @@ describe("Objective-C provider integration", () => { }); function nodeByQualifiedName(qualifiedName: string): GraphNode | undefined { - return result.graph.nodes.find( - (node) => node.properties.qualifiedName === qualifiedName, - ); + return result.graph.nodes.find((node) => node.properties.qualifiedName === qualifiedName); } - function expectNode( - qualifiedName: string, - label: GraphNode["label"], - ): GraphNode { + function expectNode(qualifiedName: string, label: GraphNode['label']): GraphNode { const node = nodeByQualifiedName(qualifiedName); expect(node, qualifiedName).toBeDefined(); expect(node?.label).toBe(label); - return node!; + if (node === undefined) throw new Error(`Missing expected node ${qualifiedName}`); + return node; } function hasRelationship( @@ -217,280 +205,208 @@ describe("Objective-C provider integration", () => { reason?: string | RegExp, ): boolean { return result.graph.relationships.some((rel) => { - if ( - rel.type !== type || - rel.sourceId !== sourceId || - rel.targetId !== targetId - ) - return false; + if (rel.type !== type || rel.sourceId !== sourceId || rel.targetId !== targetId) return false; if (reason === undefined) return true; - return typeof reason === "string" - ? rel.reason === reason - : reason.test(rel.reason); + return typeof reason === 'string' ? rel.reason === reason : reason.test(rel.reason); }); } - it("indexes Objective-C semantic nodes beyond File nodes", () => { - expectNode("objc:protocol:SYModuleRunnable", "Protocol"); - expectNode("objc:class:SYBaseCaller", "Class"); - expectNode("objc:class:SYModuleCaller", "Class"); - expectNode("objc:category:SYModuleCaller:Tracing", "Category"); - expectNode( - "objc:method:objc:class:SYModuleCaller:-:runTask:completion:", - "Method", - ); - expectNode( - "objc:method:objc:class:SYModuleCaller:+:sharedCaller", - "Method", - ); - expectNode( - "objc:method:objc:category:SYModuleCaller:Tracing:-:traceEvent:", - "Method", - ); - expectNode("objc:property:objc:class:SYModuleCaller:helper", "Property"); - expectNode("objc:ivar:objc:class:SYModuleCaller:_base", "Variable"); - expectNode("objc:function:SYModuleSupportAdd", "Function"); - expectNode("objc:function:SYModuleCompute", "Function"); + it('indexes Objective-C semantic nodes beyond File nodes', () => { + expectNode('objc:protocol:SYModuleRunnable', 'Protocol'); + expectNode('objc:class:SYBaseCaller', 'Class'); + expectNode('objc:class:SYModuleCaller', 'Class'); + expectNode('objc:class:SYModuleBridge', 'Class'); + expectNode('objc:category:SYModuleCaller:Tracing', 'Category'); + expectNode('objc:method:objc:class:SYModuleCaller:-:runTask:completion:', 'Method'); + expectNode('objc:method:objc:class:SYModuleBridge:-:bridgeValue:', 'Method'); + expectNode('objc:method:objc:class:SYModuleCaller:+:sharedCaller', 'Method'); + expectNode('objc:method:objc:category:SYModuleCaller:Tracing:-:traceEvent:', 'Method'); + expectNode('objc:property:objc:class:SYModuleCaller:helper', 'Property'); + expectNode('objc:ivar:objc:class:SYModuleCaller:_base', 'Variable'); + expectNode('objc:function:SYModuleSupportAdd', 'Function'); + expectNode('objc:function:SYModuleCompute', 'Function'); }); - it("emits imports, inheritance, protocol, and category host relationships", () => { - const caller = expectNode("objc:class:SYModuleCaller", "Class"); - const base = expectNode("objc:class:SYBaseCaller", "Class"); - const protocol = expectNode("objc:protocol:SYModuleRunnable", "Protocol"); - const category = expectNode( - "objc:category:SYModuleCaller:Tracing", - "Category", - ); + it('emits imports, inheritance, protocol, and category host relationships', () => { + const caller = expectNode('objc:class:SYModuleCaller', 'Class'); + const base = expectNode('objc:class:SYBaseCaller', 'Class'); + const protocol = expectNode('objc:protocol:SYModuleRunnable', 'Protocol'); + const category = expectNode('objc:category:SYModuleCaller:Tracing', 'Category'); - expect(hasRelationship("EXTENDS", caller.id, base.id)).toBe(true); - expect(hasRelationship("IMPLEMENTS", caller.id, protocol.id)).toBe(true); - expect(hasRelationship("MEMBER_OF", category.id, caller.id)).toBe(true); + expect(hasRelationship('EXTENDS', caller.id, base.id)).toBe(true); + expect(hasRelationship('IMPLEMENTS', caller.id, protocol.id)).toBe(true); + expect(hasRelationship('MEMBER_OF', category.id, caller.id)).toBe(true); - const importNodes = result.graph.nodes.filter( - (node) => node.label === "Import", - ); + const importNodes = result.graph.nodes.filter((node) => node.label === 'Import'); expect(importNodes.map((node) => node.properties.targetRaw)).toEqual( - expect.arrayContaining([ - "SYModuleCaller.h", - "SYModuleSupport.h", - "Foundation", - ]), + expect.arrayContaining(['SYModuleCaller.h', 'SYModuleSupport.h', 'Foundation']), ); const mFile = result.graph.nodes.find( - (node) => - node.label === "File" && - node.properties.filePath === "SYModuleCaller.m", + (node) => node.label === 'File' && node.properties.filePath === 'SYModuleCaller.m', ); const hFile = result.graph.nodes.find( - (node) => - node.label === "File" && - node.properties.filePath === "SYModuleCaller.h", + (node) => node.label === 'File' && node.properties.filePath === 'SYModuleCaller.h', ); expect(mFile).toBeDefined(); expect(hFile).toBeDefined(); - expect(hasRelationship("IMPORTS", mFile!.id, hFile!.id)).toBe(true); + if (mFile === undefined || hFile === undefined) { + throw new Error('Missing Objective-C fixture file nodes'); + } + expect(hasRelationship('IMPORTS', mFile.id, hFile.id)).toBe(true); }); - it("records implementation evidence for merged declarations", () => { - const caller = expectNode("objc:class:SYModuleCaller", "Class"); + it('records implementation evidence for merged declarations', () => { + const caller = expectNode('objc:class:SYModuleCaller', 'Class'); const runTask = expectNode( - "objc:method:objc:class:SYModuleCaller:-:runTask:completion:", - "Method", + 'objc:method:objc:class:SYModuleCaller:-:runTask:completion:', + 'Method', ); const implementationEvidence = result.graph.nodes.filter( (node) => - node.label === "CodeElement" && - node.properties.objectiveCKind === "implementation-evidence" && - node.properties.filePath === "SYModuleCaller.m", + node.label === 'CodeElement' && + node.properties.objectiveCKind === 'implementation-evidence' && + node.properties.filePath === 'SYModuleCaller.m', ); - expect( - implementationEvidence.map((node) => node.properties.targetQualifiedName), - ).toEqual( + expect(implementationEvidence.map((node) => node.properties.targetQualifiedName)).toEqual( expect.arrayContaining([ - "objc:class:SYModuleCaller", - "objc:method:objc:class:SYModuleCaller:-:runTask:completion:", + 'objc:class:SYModuleCaller', + 'objc:method:objc:class:SYModuleCaller:-:runTask:completion:', ]), ); expect( implementationEvidence.some((node) => - hasRelationship( - "DECLARES", - node.id, - caller.id, - "objc: implementation of merged symbol", - ), + hasRelationship('DECLARES', node.id, caller.id, 'objc: implementation of merged symbol'), ), ).toBe(true); expect( implementationEvidence.some((node) => - hasRelationship( - "DECLARES", - node.id, - runTask.id, - "objc: implementation of merged symbol", - ), + hasRelationship('DECLARES', node.id, runTask.id, 'objc: implementation of merged symbol'), ), ).toBe(true); }); - it("emits conservative Objective-C message-send call edges and unresolved evidence", () => { + it('emits conservative Objective-C message-send call edges and unresolved evidence', () => { const runTask = expectNode( - "objc:method:objc:class:SYModuleCaller:-:runTask:completion:", - "Method", + 'objc:method:objc:class:SYModuleCaller:-:runTask:completion:', + 'Method', ); const loadData = expectNode( - "objc:method:objc:class:SYBaseCaller:-:loadData:completion:", - "Method", + 'objc:method:objc:class:SYBaseCaller:-:loadData:completion:', + 'Method', ); const traceEvent = expectNode( - "objc:method:objc:category:SYModuleCaller:Tracing:-:traceEvent:", - "Method", + 'objc:method:objc:category:SYModuleCaller:Tracing:-:traceEvent:', + 'Method', ); const runProtocol = expectNode( - "objc:method:objc:class:SYModuleCaller:-:runProtocol:", - "Method", + 'objc:method:objc:class:SYModuleCaller:-:runProtocol:', + 'Method', ); const protocolRun = expectNode( - "objc:method:objc:protocol:SYModuleRunnable:-:runTask:completion:", - "Method", + 'objc:method:objc:protocol:SYModuleRunnable:-:runTask:completion:', + 'Method', ); expect( - hasRelationship( - "CALLS", - runTask.id, - loadData.id, - /objc-message: (super|local) receiver/, - ), + hasRelationship('CALLS', runTask.id, loadData.id, /objc-message: (super|local) receiver/), ).toBe(true); + expect(hasRelationship('CALLS', runTask.id, loadData.id, 'objc-message: self receiver')).toBe( + true, + ); + expect(hasRelationship('CALLS', runTask.id, traceEvent.id, 'objc-message: self receiver')).toBe( + true, + ); expect( - hasRelationship( - "CALLS", - runTask.id, - loadData.id, - "objc-message: self receiver", - ), - ).toBe(true); - expect( - hasRelationship( - "CALLS", - runTask.id, - traceEvent.id, - "objc-message: self receiver", - ), - ).toBe(true); - expect( - hasRelationship( - "CALLS", - runProtocol.id, - protocolRun.id, - "objc-message: protocol receiver", - ), + hasRelationship('CALLS', runProtocol.id, protocolRun.id, 'objc-message: protocol receiver'), ).toBe(true); const unresolved = result.graph.nodes.find( (node) => - node.label === "CodeElement" && - node.properties.objectiveCKind === "unresolved-message" && - node.properties.receiver === "dynamic", + node.label === 'CodeElement' && + node.properties.objectiveCKind === 'unresolved-message' && + node.properties.receiver === 'dynamic', ); expect(unresolved).toBeDefined(); + if (unresolved === undefined) throw new Error('Missing unresolved dynamic message evidence'); expect( - hasRelationship("CALLS", runTask.id, unresolved!.id), - "dynamic id receiver must not become a certain CALLS edge", + hasRelationship('CALLS', runTask.id, unresolved.id), + 'dynamic id receiver must not become a certain CALLS edge', ).toBe(false); const macroUnresolved = result.graph.nodes.find( (node) => - node.label === "CodeElement" && - node.properties.objectiveCKind === "unresolved-message" && - node.properties.receiver === "SY_OBJC_RECEIVER(self)", - ); - expect(macroUnresolved?.properties.reason).toBe( - "macro receiver SY_OBJC_RECEIVER is dynamic", + node.label === 'CodeElement' && + node.properties.objectiveCKind === 'unresolved-message' && + node.properties.receiver === 'SY_OBJC_RECEIVER(self)', ); + expect(macroUnresolved?.properties.reason).toBe('macro receiver SY_OBJC_RECEIVER is dynamic'); const candidates = result.graph.nodes.find( (node) => - node.label === "CodeElement" && - node.properties.objectiveCKind === "protocol-candidate-implementations", + node.label === 'CodeElement' && + node.properties.objectiveCKind === 'protocol-candidate-implementations', ); expect(candidates?.properties.candidateImplementations).toEqual( - expect.arrayContaining([ - "objc:method:objc:class:SYModuleCaller:-:runTask:completion:", - ]), + expect.arrayContaining(['objc:method:objc:class:SYModuleCaller:-:runTask:completion:']), ); }); }); -describe("Objective-C provider persisted index behavior", () => { - it("surfaces query/context semantics and keeps incremental results aligned with force rebuild", async () => { - const repoRoot = fs.mkdtempSync( - path.join(os.tmpdir(), "gitnexus-objc-provider-index-"), - ); +describe('Objective-C provider persisted index behavior', () => { + it('surfaces query/context semantics and keeps incremental results aligned with force rebuild', async () => { + const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-objc-provider-index-')); try { writeObjectiveCRepo(repoRoot); - git(repoRoot, "git init"); - gitCommitAll(repoRoot, "initial Objective-C fixture"); + git(repoRoot, 'git init'); + gitCommitAll(repoRoot, 'initial Objective-C fixture'); await analyzeObjectiveCRepo(repoRoot); const initialSurface = await readPersistedObjectiveCSurface(repoRoot); expect(initialSurface).toMatchObject({ classContext: { - status: "found", + status: 'found', symbol: { - uid: "Class:objc:class:SYModuleCaller", - kind: "Class", - filePath: "SYModuleCaller.h", + uid: 'Class:objc:class:SYModuleCaller', + kind: 'Class', + filePath: 'SYModuleCaller.h', }, incoming: { declares: expect.arrayContaining([ expect.objectContaining({ uid: expect.stringContaining( - "CodeElement:objc:implementation:objc:class:SYModuleCaller:SYModuleCaller.m:", + 'CodeElement:objc:implementation:objc:class:SYModuleCaller:SYModuleCaller.m:', ), - filePath: "SYModuleCaller.m", + filePath: 'SYModuleCaller.m', }), ]), imports: expect.arrayContaining([ expect.objectContaining({ - uid: "File:SYModuleCaller.m", - filePath: "SYModuleCaller.m", + uid: 'File:SYModuleCaller.m', + filePath: 'SYModuleCaller.m', }), ]), }, }, queryDefinitions: expect.arrayContaining([ - expect.objectContaining({ uid: "Class:objc:class:SYModuleCaller" }), + expect.objectContaining({ uid: 'Class:objc:class:SYModuleCaller' }), expect.objectContaining({ - uid: expect.stringMatching( - /^Method:objc:method:objc:class:SYModuleCaller:/, - ), + uid: expect.stringMatching(/^Method:objc:method:objc:class:SYModuleCaller:/), }), ]), protocolAndCategoryResult: expect.objectContaining({ - markdown: expect.stringContaining( - "Protocol:objc:protocol:SYModuleRunnable", - ), + markdown: expect.stringContaining('Protocol:objc:protocol:SYModuleRunnable'), }), categoryHostResult: expect.objectContaining({ - markdown: expect.stringContaining( - "Category:objc:category:SYModuleCaller:Tracing", - ), + markdown: expect.stringContaining('Category:objc:category:SYModuleCaller:Tracing'), }), }); writeObjectiveCRepo(repoRoot, HEADER_V2, IMPL_V2); - gitCommitAll( - repoRoot, - "change Objective-C declarations and implementations", - ); + gitCommitAll(repoRoot, 'change Objective-C declarations and implementations'); const incrementalLogs = await analyzeObjectiveCRepo(repoRoot); - expect(incrementalLogs).toContainEqual( - expect.stringContaining("Incremental: changed="), - ); + expect(incrementalLogs).toContainEqual(expect.stringContaining('Incremental: changed=')); const incrementalSurface = await readPersistedObjectiveCSurface(repoRoot); await analyzeObjectiveCRepo(repoRoot, { force: true }); @@ -501,7 +417,7 @@ describe("Objective-C provider persisted index behavior", () => { outgoing: { has_method: expect.arrayContaining([ expect.objectContaining({ - uid: "Method:objc:method:objc:category:SYModuleCaller:Tracing:-:traceDetail:level:", + uid: 'Method:objc:method:objc:category:SYModuleCaller:Tracing:-:traceDetail:level:', }), ]), }, @@ -510,7 +426,7 @@ describe("Objective-C provider persisted index behavior", () => { outgoing: { calls: expect.arrayContaining([ expect.objectContaining({ - uid: "Method:objc:method:objc:category:SYModuleCaller:Tracing:-:traceDetail:level:", + uid: 'Method:objc:method:objc:category:SYModuleCaller:Tracing:-:traceDetail:level:', }), ]), }, diff --git a/gitnexus/test/unit/grammar-update-monitor.test.ts b/gitnexus/test/unit/grammar-update-monitor.test.ts index f60752e6b..954a8ee45 100644 --- a/gitnexus/test/unit/grammar-update-monitor.test.ts +++ b/gitnexus/test/unit/grammar-update-monitor.test.ts @@ -74,7 +74,14 @@ describe('COMPATIBLE_ABI gate', () => { describe('GRAMMARS registry', () => { it('covers all six grammars, including the vendored Objective-C grammar', () => { - expect(Object.keys(mod.GRAMMARS).sort()).toEqual(['c', 'dart', 'kotlin', 'objc', 'proto', 'swift']); + expect(Object.keys(mod.GRAMMARS).sort()).toEqual([ + 'c', + 'dart', + 'kotlin', + 'objc', + 'proto', + 'swift', + ]); expect(mod.GRAMMARS.swift.npm).toBe('tree-sitter-swift'); expect(mod.GRAMMARS.dart.github).toContain('tree-sitter-dart'); }); diff --git a/gitnexus/test/unit/objective-c-parser-loader.test.ts b/gitnexus/test/unit/objective-c-parser-loader.test.ts index 150c39805..0d60378e2 100644 --- a/gitnexus/test/unit/objective-c-parser-loader.test.ts +++ b/gitnexus/test/unit/objective-c-parser-loader.test.ts @@ -24,9 +24,8 @@ describe('Objective-C parser-loader failure path', () => { }, })); - const { getLanguageGrammar, isGrammarRuntimeSkipped, isLanguageAvailable } = await import( - '../../src/core/tree-sitter/parser-loader.js' - ); + const { getLanguageGrammar, isGrammarRuntimeSkipped, isLanguageAvailable } = + await import('../../src/core/tree-sitter/parser-loader.js'); expect(isLanguageAvailable(SupportedLanguages.ObjectiveC)).toBe(false); expect(isGrammarRuntimeSkipped(SupportedLanguages.ObjectiveC)).toBe(false); diff --git a/gitnexus/test/unit/objective-c-provider.test.ts b/gitnexus/test/unit/objective-c-provider.test.ts index cef5c85c2..9e999fe0e 100644 --- a/gitnexus/test/unit/objective-c-provider.test.ts +++ b/gitnexus/test/unit/objective-c-provider.test.ts @@ -1,22 +1,22 @@ -import { describe, expect, it } from "vitest"; -import Parser from "tree-sitter"; +import { describe, expect, it } from 'vitest'; +import Parser from 'tree-sitter'; import { getLanguageFromFilename, getSyntaxLanguageFromFilename, SupportedLanguages, -} from "gitnexus-shared"; -import { getLanguageForFileContent } from "../../src/core/ingestion/languages/index.js"; -import { classifyObjectiveCFileContent } from "../../src/core/ingestion/languages/objective-c.js"; +} from 'gitnexus-shared'; +import { getLanguageForFileContent } from '../../src/core/ingestion/languages/index.js'; +import { classifyObjectiveCFileContent } from '../../src/core/ingestion/languages/objective-c.js'; import { buildObjectiveCSemanticGraph, collectObjectiveCFacts, objcCategoryQualifiedName, objcClassQualifiedName, objcMethodQualifiedName, -} from "../../src/core/ingestion/languages/objective-c/facts.js"; -import { isLanguageAvailable } from "../../src/core/tree-sitter/parser-loader.js"; -import { requireVendoredGrammar } from "../../src/core/tree-sitter/vendored-grammars.js"; -import { objectiveCScopeResolver } from "../../src/core/ingestion/languages/objective-c/scope-resolver.js"; +} from '../../src/core/ingestion/languages/objective-c/facts.js'; +import { isLanguageAvailable } from '../../src/core/tree-sitter/parser-loader.js'; +import { requireVendoredGrammar } from '../../src/core/tree-sitter/vendored-grammars.js'; +import { objectiveCScopeResolver } from '../../src/core/ingestion/languages/objective-c/scope-resolver.js'; const FIXTURE = `#import "SYModuleCaller.h" #include "SYModuleSupport.h" @@ -70,83 +70,63 @@ static int SYModuleCompute(int value) { return value + 1; } function parseFixture() { const parser = new Parser(); - parser.setLanguage(requireVendoredGrammar("tree-sitter-objc")); + parser.setLanguage(requireVendoredGrammar('tree-sitter-objc')); return parser.parse(FIXTURE); } function parseSource(source: string) { const parser = new Parser(); - parser.setLanguage(requireVendoredGrammar("tree-sitter-objc")); + parser.setLanguage(requireVendoredGrammar('tree-sitter-objc')); return parser.parse(source); } -describe("Objective-C provider", () => { - it("loads the vendored grammar and maps unambiguous Objective-C extensions", () => { +describe('Objective-C provider', () => { + it('loads the vendored grammar and maps unambiguous Objective-C extensions', () => { expect(isLanguageAvailable(SupportedLanguages.ObjectiveC)).toBe(true); - expect(getLanguageFromFilename("SYModuleCaller.m")).toBe( - SupportedLanguages.ObjectiveC, - ); - expect(getLanguageFromFilename("SYModuleCaller.mm")).toBe( - SupportedLanguages.ObjectiveC, - ); - expect(getSyntaxLanguageFromFilename("SYModuleCaller.m")).toBe( - "objectivec", - ); + expect(getLanguageFromFilename('SYModuleCaller.m')).toBe(SupportedLanguages.ObjectiveC); + expect(getLanguageFromFilename('SYModuleCaller.mm')).toBe(SupportedLanguages.ObjectiveC); + expect(getSyntaxLanguageFromFilename('SYModuleCaller.m')).toBe('objectivec'); }); - it("classifies Objective-C headers by content without stealing plain C headers", () => { + it('classifies Objective-C headers by content without stealing plain C headers', () => { expect( classifyObjectiveCFileContent( - "SYModuleCaller.h", - "@interface SYModuleCaller : NSObject\n@end", + 'SYModuleCaller.h', + '@interface SYModuleCaller : NSObject\n@end', ), ).toBe(true); + expect(getLanguageForFileContent('SYModuleCaller.h', '@protocol SYModuleRunnable\n@end')).toBe( + SupportedLanguages.ObjectiveC, + ); expect( - getLanguageForFileContent( - "SYModuleCaller.h", - "@protocol SYModuleRunnable\n@end", - ), - ).toBe(SupportedLanguages.ObjectiveC); - expect( - getLanguageForFileContent( - "plain.h", - "#ifndef PLAIN_H\nint add(int a, int b);\n#endif\n", - ), + getLanguageForFileContent('plain.h', '#ifndef PLAIN_H\nint add(int a, int b);\n#endif\n'), ).toBe(SupportedLanguages.CPlusPlus); expect( - classifyObjectiveCFileContent( - "framework.h", - "#import \n", - ), + classifyObjectiveCFileContent('framework.h', '#import \n'), ).toBe(true); - expect( - classifyObjectiveCFileContent( - "plain-cpp.h", - "class Widget { int value; };\n", - ), - ).toBe(false); - expect(classifyObjectiveCFileContent("forward.h", "@class Widget;\n")).toBe( - true, + expect(classifyObjectiveCFileContent('plain-cpp.h', 'class Widget { int value; };\n')).toBe( + false, ); + expect(classifyObjectiveCFileContent('forward.h', '@class Widget;\n')).toBe(true); }); - it("extracts nested C function declarators without claiming function pointers", () => { + it('extracts nested C function declarators without claiming function pointers', () => { const facts = collectObjectiveCFacts( parseSource(` int add(int value); int *returnsPointer(int value); int (*callback)(int value); `), - "functions.h", + 'functions.h', ); expect(facts.functions.map((fn) => fn.name)).toEqual( - expect.arrayContaining(["add", "returnsPointer"]), + expect.arrayContaining(['add', 'returnsPointer']), ); - expect(facts.functions.map((fn) => fn.name)).not.toContain("callback"); + expect(facts.functions.map((fn) => fn.name)).not.toContain('callback'); }); - it("does not treat protocol-qualified parameter types as conformance", () => { + it('does not treat protocol-qualified parameter types as conformance', () => { const facts = collectObjectiveCFacts( parseSource(` @protocol P @@ -156,19 +136,18 @@ int (*callback)(int value); - (void)run:(id)value; @end `), - "protocols.h", + 'protocols.h', ); - expect( - facts.containers.find((container) => container.name === "P")?.protocols, - ).toEqual(["NSObject"]); - expect( - facts.containers.find((container) => container.name === "Child") - ?.protocols, - ).toEqual(["P"]); + expect(facts.containers.find((container) => container.name === 'P')?.protocols).toEqual([ + 'NSObject', + ]); + expect(facts.containers.find((container) => container.name === 'Child')?.protocols).toEqual([ + 'P', + ]); }); - it("keeps explicit class receivers and macro receivers separate", () => { + it('keeps explicit class receivers and macro receivers separate', () => { const facts = collectObjectiveCFacts( parseSource(` #define RECEIVER_MACRO(x) x @@ -189,73 +168,63 @@ int (*callback)(int value); } @end `), - "receivers.m", + 'receivers.m', ); expect( - facts.messages.map( - (message) => `${message.receiverKind}:${message.receiverText}`, - ), - ).toEqual( - expect.arrayContaining([ - "class:A", - "self:self", - "dynamic:RECEIVER_MACRO(self)", - ]), - ); + facts.messages.map((message) => `${message.receiverKind}:${message.receiverText}`), + ).toEqual(expect.arrayContaining(['class:A', 'self:self', 'dynamic:RECEIVER_MACRO(self)'])); expect(facts.unresolvedMessages).toEqual( expect.arrayContaining([ expect.objectContaining({ - receiverText: "RECEIVER_MACRO(self)", - reason: "macro receiver RECEIVER_MACRO is dynamic", + receiverText: 'RECEIVER_MACRO(self)', + reason: 'macro receiver RECEIVER_MACRO is dynamic', }), ]), ); }); - it("resolves extensionless local imports to Objective-C source/header files", () => { + it('resolves extensionless local imports to Objective-C source/header files', () => { expect( objectiveCScopeResolver.resolveImportTarget( - "./NestedHeader", - "src/Caller.m", - new Set(["src/NestedHeader.h"]), + './NestedHeader', + 'src/Caller.m', + new Set(['src/NestedHeader.h']), ), - ).toBe("src/NestedHeader.h"); + ).toBe('src/NestedHeader.h'); expect( objectiveCScopeResolver.resolveImportTarget( - "./NestedImpl", - "src/Caller.m", - new Set(["src/NestedImpl.mm"]), + './NestedImpl', + 'src/Caller.m', + new Set(['src/NestedImpl.mm']), ), - ).toBe("src/NestedImpl.mm"); + ).toBe('src/NestedImpl.mm'); expect( objectiveCScopeResolver.resolveImportTarget( - "Foundation", - "src/Caller.m", - new Set(["src/Foundation.h"]), + 'Foundation', + 'src/Caller.m', + new Set(['src/Foundation.h']), ), ).toBeNull(); }); - it("extracts first-version Objective-C semantic facts and unresolved evidence", () => { - const facts = collectObjectiveCFacts(parseFixture(), "SYModuleCaller.m"); + it('extracts first-version Objective-C semantic facts and unresolved evidence', () => { + const facts = collectObjectiveCFacts(parseFixture(), 'SYModuleCaller.m'); expect(facts.containers.map((c) => `${c.kind}:${c.name}`)).toEqual( expect.arrayContaining([ - "protocol:SYModuleRunnable", - "class:SYBaseCaller", - "class:SYModuleCaller", - "extension:SYModuleCaller ()", - "category:SYModuleCaller (Tracing)", + 'protocol:SYModuleRunnable', + 'class:SYBaseCaller', + 'class:SYModuleCaller', + 'extension:SYModuleCaller ()', + 'category:SYModuleCaller (Tracing)', ]), ); expect( - facts.containers.find( - (c) => c.name === "SYModuleCaller" && c.kind === "class", - ), + facts.containers.find((c) => c.name === 'SYModuleCaller' && c.kind === 'class'), ).toMatchObject({ - superclass: "SYBaseCaller", - protocols: ["SYModuleRunnable"], + superclass: 'SYBaseCaller', + protocols: ['SYModuleRunnable'], }); expect( @@ -267,87 +236,78 @@ int (*callback)(int value); ).toEqual( expect.arrayContaining([ { - kind: "-", - selector: "runTask:completion:", - owner: objcClassQualifiedName("SYModuleCaller"), + kind: '-', + selector: 'runTask:completion:', + owner: objcClassQualifiedName('SYModuleCaller'), }, { - kind: "+", - selector: "sharedCaller", - owner: objcClassQualifiedName("SYModuleCaller"), + kind: '+', + selector: 'sharedCaller', + owner: objcClassQualifiedName('SYModuleCaller'), }, { - kind: "-", - selector: "traceEvent:", - owner: objcCategoryQualifiedName("SYModuleCaller", "Tracing"), + kind: '-', + selector: 'traceEvent:', + owner: objcCategoryQualifiedName('SYModuleCaller', 'Tracing'), }, { - kind: "-", - selector: "runTask:completion:", - owner: "objc:protocol:SYModuleRunnable", + kind: '-', + selector: 'runTask:completion:', + owner: 'objc:protocol:SYModuleRunnable', }, ]), ); - expect( - facts.members.map((m) => `${m.kind}:${m.name}:${m.declaredType ?? ""}`), - ).toEqual( - expect.arrayContaining([ - "property:helper:SYBaseCaller", - "ivar:_base:SYBaseCaller", - ]), + expect(facts.members.map((m) => `${m.kind}:${m.name}:${m.declaredType ?? ''}`)).toEqual( + expect.arrayContaining(['property:helper:SYBaseCaller', 'ivar:_base:SYBaseCaller']), ); - expect(facts.functions.map((fn) => fn.name)).toContain("SYModuleCompute"); + expect(facts.functions.map((fn) => fn.name)).toContain('SYModuleCompute'); expect(facts.imports.map((imp) => `${imp.kind}:${imp.targetRaw}`)).toEqual( expect.arrayContaining([ - "import:SYModuleCaller.h", - "include:SYModuleSupport.h", - "module:Foundation", + 'import:SYModuleCaller.h', + 'include:SYModuleSupport.h', + 'module:Foundation', ]), ); expect( - facts.messages.map( - (msg) => `${msg.receiverKind}:${msg.receiverText}:${msg.selector}`, - ), + facts.messages.map((msg) => `${msg.receiverKind}:${msg.receiverText}:${msg.selector}`), ).toEqual( expect.arrayContaining([ - "self:self:traceEvent:", - "super:super:loadData:completion:", - "local:typed:loadData:completion:", - "dynamic:dynamic:loadData:completion:", - "local:runner:runTask:completion:", + 'self:self:traceEvent:', + 'super:super:loadData:completion:', + 'local:typed:loadData:completion:', + 'dynamic:dynamic:loadData:completion:', + 'local:runner:runTask:completion:', ]), ); expect(facts.unresolvedMessages).toEqual( expect.arrayContaining([ expect.objectContaining({ - receiverText: "dynamic", - selector: "loadData:completion:", - reason: "id receiver is dynamic", + receiverText: 'dynamic', + selector: 'loadData:completion:', + reason: 'id receiver is dynamic', }), ]), ); }); - it("uses owner, selector, and method kind in stable method identities", () => { - const facts = collectObjectiveCFacts(parseFixture(), "SYModuleCaller.m"); + it('uses owner, selector, and method kind in stable method identities', () => { + const facts = collectObjectiveCFacts(parseFixture(), 'SYModuleCaller.m'); const graph = buildObjectiveCSemanticGraph(facts); const methodIds = new Set( - graph.nodes - .filter((node) => node.label === "Method") - .map((node) => node.id), + graph.nodes.filter((node) => node.label === 'Method').map((node) => node.id), ); expect(methodIds).toContain( - `Method:${objcMethodQualifiedName(objcClassQualifiedName("SYModuleCaller"), "-", "runTask:completion:")}`, + `Method:${objcMethodQualifiedName(objcClassQualifiedName('SYModuleCaller'), '-', 'runTask:completion:')}`, ); expect(methodIds).toContain( - `Method:${objcMethodQualifiedName(objcClassQualifiedName("SYModuleCaller"), "+", "sharedCaller")}`, + `Method:${objcMethodQualifiedName(objcClassQualifiedName('SYModuleCaller'), '+', 'sharedCaller')}`, ); expect(methodIds).toContain( `Method:${objcMethodQualifiedName( - objcCategoryQualifiedName("SYModuleCaller", "Tracing"), - "-", - "traceEvent:", + objcCategoryQualifiedName('SYModuleCaller', 'Tracing'), + '-', + 'traceEvent:', )}`, ); expect(methodIds.size).toBeGreaterThan(4); diff --git a/gitnexus/test/unit/parser-loader-abi.test.ts b/gitnexus/test/unit/parser-loader-abi.test.ts index 8c37047a9..f75b88c5f 100644 --- a/gitnexus/test/unit/parser-loader-abi.test.ts +++ b/gitnexus/test/unit/parser-loader-abi.test.ts @@ -74,7 +74,8 @@ const SMOKE_CASES: Record = { }, [SupportedLanguages.ObjectiveC]: { language: SupportedLanguages.ObjectiveC, - snippet: '@interface ObjcSmoke\n- (void)run;\n@end\n@implementation ObjcSmoke\n- (void)run {}\n@end\n', + snippet: + '@interface ObjcSmoke\n- (void)run;\n@end\n@implementation ObjcSmoke\n- (void)run {}\n@end\n', rootType: 'translation_unit', }, [SupportedLanguages.Go]: {