mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-10 03:27:59 +00:00
feat(objective-c): finalize provider MVP
This commit is contained in:
parent
236568b8f9
commit
545537e3c1
15 changed files with 708 additions and 1102 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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/<name>/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).
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
<details>
|
||||
<summary><strong>Install problems?</strong> npm 11 crash · slow cold install · no C++ toolchain</summary>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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<string, ObjCContainerFact>;
|
||||
readonly classByName: ReadonlyMap<string, ObjCContainerFact>;
|
||||
readonly protocolsByName: ReadonlyMap<string, ObjCContainerFact>;
|
||||
readonly categoriesByHost: ReadonlyMap<string, readonly ObjCContainerFact[]>;
|
||||
readonly methodsByDispatchOwner: ReadonlyMap<
|
||||
string,
|
||||
readonly ObjCMethodFact[]
|
||||
>;
|
||||
readonly methodsByDispatchOwner: ReadonlyMap<string, readonly ObjCMethodFact[]>;
|
||||
readonly methodsByExactOwner: ReadonlyMap<string, readonly ObjCMethodFact[]>;
|
||||
readonly classProtocols: ReadonlyMap<string, ReadonlySet<string>>;
|
||||
readonly superclassByClass: ReadonlyMap<string, string>;
|
||||
|
|
@ -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<string, ObjCContainerFact>();
|
||||
const classByName = new Map<string, ObjCContainerFact>();
|
||||
const protocolsByName = new Map<string, ObjCContainerFact>();
|
||||
|
|
@ -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<string, unknown>,
|
||||
): 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<string>();
|
||||
|
|
@ -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<string>([
|
||||
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(/^\.\//, '');
|
||||
}
|
||||
|
|
|
|||
9
gitnexus/test/fixtures/objective-c/SYModuleBridge.mm
vendored
Normal file
9
gitnexus/test/fixtures/objective-c/SYModuleBridge.mm
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#import "SYModuleCaller.h"
|
||||
|
||||
@interface SYModuleBridge : NSObject
|
||||
- (void)bridgeValue:(NSInteger)value;
|
||||
@end
|
||||
|
||||
@implementation SYModuleBridge
|
||||
- (void)bridgeValue:(NSInteger)value {}
|
||||
@end
|
||||
|
|
@ -111,7 +111,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)) {
|
||||
|
|
|
|||
|
|
@ -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<string, unknown> {
|
|||
};
|
||||
}
|
||||
|
||||
async function readPersistedObjectiveCSurface(
|
||||
repoRoot: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
async function readPersistedObjectiveCSurface(repoRoot: string): Promise<Record<string, unknown>> {
|
||||
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<string, unknown>;
|
||||
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:',
|
||||
}),
|
||||
]),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 <Foundation/Foundation.h>\n",
|
||||
),
|
||||
classifyObjectiveCFileContent('framework.h', '#import <Foundation/Foundation.h>\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 <NSObject>
|
||||
|
|
@ -156,19 +136,18 @@ int (*callback)(int value);
|
|||
- (void)run:(id<Q>)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);
|
||||
|
|
|
|||
|
|
@ -75,7 +75,8 @@ const SMOKE_CASES: Record<string, SmokeCase> = {
|
|||
},
|
||||
[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]: {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue