feat(objective-c): add deterministic provider and grammar

This commit is contained in:
ximengkai 2026-08-17 17:13:43 +08:00
parent 9f16eae2d4
commit 236568b8f9
83 changed files with 712883 additions and 25 deletions

View file

@ -21,7 +21,7 @@
* node update-vendored-grammars.mjs # detect only → JSON report on stdout
* node update-vendored-grammars.mjs --apply X # re-vendor grammar X in place
*
* tree-sitter-c is MONITORED but report-only (`hold`): it is ABI-pinned at 0.21.4
* tree-sitter-c and tree-sitter-objc are MONITORED but report-only (`hold`): c is ABI-pinned at 0.21.4
* (#1242/#858) and must not auto-bump without a tree-sitter runtime upgrade, so an
* available c update is detected + reported but never auto-applied — even if it is
* ABI-13/14. A maintainer re-vendors it deliberately.

View file

@ -6,6 +6,11 @@
"upstream": { "npm": "tree-sitter-c" },
"hold": "ABI-pinned at 0.21.4 (#1242/#858) — needs a tree-sitter runtime upgrade before bumping"
},
"objc": {
"name": "tree-sitter-objc",
"upstream": { "npm": "tree-sitter-objc" },
"hold": "Pinned at 3.0.2 for the Objective-C provider MVP; carries darwin/linux arm64+x64 prebuilds compatible with the current tree-sitter runtime (linux-arm64 built from vendored source because the upstream npm artifact is mislabeled)"
},
"swift": {
"name": "tree-sitter-swift",
"upstream": { "npm": "tree-sitter-swift" }

View file

@ -86,7 +86,7 @@ on:
- '!gitnexus/vendor/tree-sitter-*/prebuilds/**'
# Self-test: re-run the guard if a future grammar pin is reintroduced in
# the main package.json (optionalDependencies fallback). No-op otherwise —
# all five grammars are now fully vendored (kotlin included).
# all six grammars are now fully vendored (kotlin and objc included).
- 'gitnexus/package.json'
# Self-test: re-run the guard (normally a no-op) when the recipe changes.
- '.github/workflows/build-tree-sitter-prebuilds.yml'

View file

@ -2,6 +2,11 @@
Status: planned
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.
## Goal
Add deterministic, symbol-level Objective-C analysis to GitNexus. The first release must support high-confidence code navigation and direct static dependency analysis for `.m`, `.mm`, and Objective-C `.h` files. It must not imply that Objective-C runtime dispatch is fully resolved.
@ -82,3 +87,21 @@ The acceptance bar is:
## Non-goals
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`
Implemented in the branch:
- Vendored `tree-sitter-objc` grammar, registered through the existing Tree-sitter loader.
- `.m` and `.mm` language mapping plus content-based `.h` classification so plain C/C++ headers are not unconditionally claimed.
- LanguageProvider extraction for classes, protocols, categories, extensions, methods, properties, ivars, C functions, imports, unresolved message evidence, stable Objective-C qualified names, and provider/grammar metadata.
- ScopeResolver edges for imports, inheritance, protocol conformance, category host membership, implementation evidence, and conservative static message sends.
- 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:
- 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.

View file

@ -14,6 +14,8 @@ export type NodeLabel =
| 'Folder'
| 'File'
| 'Class'
| 'Protocol'
| 'Category'
| 'Function'
| 'Method'
| 'Variable'

View file

@ -32,6 +32,7 @@ const EXTENSION_MAP: Record<SupportedLanguages, readonly string[]> = {
[SupportedLanguages.Python]: ['.py'],
[SupportedLanguages.Java]: ['.java'],
[SupportedLanguages.C]: ['.c'],
[SupportedLanguages.ObjectiveC]: ['.m', '.mm'],
[SupportedLanguages.CPlusPlus]: [
'.cpp',
'.cc',
@ -111,6 +112,7 @@ const SYNTAX_MAP: Record<SupportedLanguages, string> = {
[SupportedLanguages.Python]: 'python',
[SupportedLanguages.Java]: 'java',
[SupportedLanguages.C]: 'c',
[SupportedLanguages.ObjectiveC]: 'objectivec',
[SupportedLanguages.CPlusPlus]: 'cpp',
[SupportedLanguages.CSharp]: 'csharp',
[SupportedLanguages.Go]: 'go',

View file

@ -11,6 +11,7 @@ export enum SupportedLanguages {
Java = 'java',
C = 'c',
CPlusPlus = 'cpp',
ObjectiveC = 'objective-c',
CSharp = 'csharp',
Go = 'go',
Ruby = 'ruby',

View file

@ -13,6 +13,8 @@ export const NODE_TABLES = [
'Folder',
'Function',
'Class',
'Protocol',
'Category',
'Interface',
'Method',
'CodeElement',

View file

@ -9,7 +9,8 @@
* Initial classification (locked in Ring 1 #910):
* - production: javascript, typescript, python, java, c, cpp, csharp, go,
* ruby, rust, php, kotlin, swift, dart
* - experimental: vue (embedded-language / SFC complexity),
* - experimental: objective-c (fork provider MVP),
* vue (embedded-language / SFC complexity),
* cobol (regex-provider path)
* - quarantined: (none)
*
@ -34,6 +35,7 @@ export const LanguageClassifications: Readonly<Record<SupportedLanguages, Langua
[SupportedLanguages.Java]: 'production',
[SupportedLanguages.C]: 'production',
[SupportedLanguages.CPlusPlus]: 'production',
[SupportedLanguages.ObjectiveC]: 'experimental',
[SupportedLanguages.CSharp]: 'production',
[SupportedLanguages.Go]: 'production',
[SupportedLanguages.Ruby]: 'production',

View file

@ -136,6 +136,8 @@ export interface RegistryContext {
export const CLASS_KINDS: readonly NodeLabel[] = Object.freeze([
'Class',
'Protocol',
'Category',
'Interface',
'Enum',
'Struct',

View file

@ -82,6 +82,8 @@ const STRICT_ORIGINS: ReadonlySet<BindingRef['origin']> = new Set<BindingRef['or
*/
const TYPE_KINDS: ReadonlySet<NodeLabel> = new Set<NodeLabel>([
'Class',
'Protocol',
'Category',
'Interface',
'Enum',
'Struct',

View file

@ -8,6 +8,8 @@ export const NODE_COLORS: Record<NodeLabel, string> = {
Folder: '#6366f1', // Indigo
File: '#3b82f6', // Blue
Class: '#f59e0b', // Amber - stands out
Protocol: '#ec4899', // Pink - like Interface
Category: '#14b8a6', // Teal - like Method
Function: '#10b981', // Emerald
Method: '#14b8a6', // Teal
Variable: '#64748b', // Slate - muted (less important)
@ -51,6 +53,8 @@ export const NODE_SIZES: Record<NodeLabel, number> = {
Folder: 10, // Structural - clearly bigger than files
File: 6, // Common element - smaller than folders
Class: 8, // Important code structure
Protocol: 7, // Like Interface
Category: 3, // Like Method
Function: 4, // Common code element - small
Method: 3, // Smaller than function
Variable: 2, // Tiny - leaf node
@ -115,6 +119,8 @@ export const DEFAULT_VISIBLE_LABELS: NodeLabel[] = [
'Folder',
'File',
'Class',
'Protocol',
'Category',
'Function',
'Method',
'Property', // Kotlin/Java fields (HAS_PROPERTY + DEFINES File→Property)

View file

@ -25,7 +25,7 @@
* or exit non-zero — a failure for any single grammar must not break the install.
*
* Opt-out: GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (strict '1') skips the OPTIONAL
* grammars only. tree-sitter-c is REQUIRED (it backstops upstream's 4/6 ARM
* grammars only. tree-sitter-c and tree-sitter-objc are REQUIRED (C backstops upstream's 4/6 ARM
* prebuild gap, #2116) and is always built.
*
* Usage:
@ -40,6 +40,7 @@ const { execSync } = require('child_process');
// grammars ignore the opt-out gate. Insertion order == build order (c first).
const GRAMMARS = {
c: { required: true, display: 'C', ext: '.c' },
objc: { required: true, display: 'Objective-C', ext: '.m/.mm/.h' },
dart: { required: false, display: 'Dart', ext: '.dart' },
proto: { required: false, display: 'Proto', ext: '.proto' },
swift: { required: false, display: 'Swift', ext: '.swift' },

View file

@ -11,6 +11,7 @@ import {
JAVA_RECORD_COMPONENT_ACCESSORS_FEATURE,
SPRING_CONFIG_BINDINGS_FEATURE,
} from './ingestion/languages/java/analysis-features.js';
import { OBJECTIVE_C_PROVIDER_FEATURE } from './ingestion/languages/objective-c/analysis-features.js';
/** Production registry of independently versioned analysis capabilities. */
export const ANALYSIS_FEATURES = [
@ -23,4 +24,5 @@ export const ANALYSIS_FEATURES = [
SPRING_CONFIG_BINDINGS_FEATURE,
JAVA_ENUM_INTERFACE_HERITAGE_FEATURE,
JAVA_RECORD_COMPONENT_ACCESSORS_FEATURE,
OBJECTIVE_C_PROVIDER_FEATURE,
] as const;

View file

@ -8,6 +8,8 @@ export const LABEL_FUNCTION = 'Function' as const;
export const LABEL_METHOD = 'Method' as const;
export const LABEL_CONSTRUCTOR = 'Constructor' as const;
export const LABEL_CLASS = 'Class' as const;
export const LABEL_PROTOCOL = 'Protocol' as const;
export const LABEL_CATEGORY = 'Category' as const;
export const LABEL_INTERFACE = 'Interface' as const;
export const LABEL_STRUCT = 'Struct' as const;
export const LABEL_ENUM = 'Enum' as const;
@ -53,6 +55,8 @@ export const CHUNKABLE_LABELS = [
LABEL_METHOD,
LABEL_CONSTRUCTOR,
LABEL_CLASS,
LABEL_PROTOCOL,
LABEL_CATEGORY,
LABEL_INTERFACE,
LABEL_STRUCT,
LABEL_ENUM,
@ -108,6 +112,8 @@ export const isShortLabel = (label: string): boolean =>
*/
export const STRUCTURAL_LABELS: ReadonlySet<string> = new Set([
LABEL_CLASS,
LABEL_PROTOCOL,
LABEL_CATEGORY,
LABEL_STRUCT,
LABEL_INTERFACE,
]);
@ -118,6 +124,8 @@ export const STRUCTURAL_LABELS: ReadonlySet<string> = new Set([
export const LABELS_WITH_EXPORTED = new Set([
LABEL_FUNCTION,
LABEL_CLASS,
LABEL_PROTOCOL,
LABEL_CATEGORY,
LABEL_INTERFACE,
LABEL_METHOD,
LABEL_CODE_ELEMENT,
@ -165,6 +173,20 @@ export const CHUNKING_RULES: Readonly<Partial<Record<ChunkableLabel, ChunkingRul
groupFields: false,
structuralTextMode: STRUCTURAL_TEXT_MODE_DECLARATION,
},
[LABEL_PROTOCOL]: {
mode: CHUNK_MODE_AST_DECLARATION,
includePrefix: true,
includeSuffix: false,
groupFields: false,
structuralTextMode: STRUCTURAL_TEXT_MODE_DECLARATION,
},
[LABEL_CATEGORY]: {
mode: CHUNK_MODE_AST_DECLARATION,
includePrefix: true,
includeSuffix: false,
groupFields: true,
structuralTextMode: STRUCTURAL_TEXT_MODE_DECLARATION,
},
[LABEL_STRUCT]: {
mode: CHUNK_MODE_AST_DECLARATION,
includePrefix: true,

View file

@ -25,7 +25,7 @@ export interface ManifestExtractResult {
// two would widen which nodes resolve as contract symbols and must update the
// #2325 test, so they are intentionally kept separate for now.
export const CUSTOM_CONTRACT_RESOLVE_QUERY = `MATCH (n)
WHERE labels(n) IN ['Function','Method','Class','Interface','Struct','Enum','Trait','Constructor','TypeAlias','Impl','Macro','Union','Typedef','Property','Record','Delegate','Annotation','Template','Const','Static','CodeElement']
WHERE labels(n) IN ['Function','Method','Class','Protocol','Category','Interface','Struct','Enum','Trait','Constructor','TypeAlias','Impl','Macro','Union','Typedef','Property','Record','Delegate','Annotation','Template','Const','Static','CodeElement']
AND n.name = $symbolName
RETURN n.id AS uid, n.name AS name, n.filePath AS filePath
ORDER BY n.filePath ASC, n.id ASC

View file

@ -36,7 +36,12 @@ import type { VariableExtractor } from './variable-types.js';
import type { ImportResolverFn } from './import-resolvers/types.js';
import type { SyntaxNode } from './utils/ast-helpers.js';
import type { CfgVisitor } from './cfg/types.js';
import type { GraphNode, NodeLabel } from 'gitnexus-shared';
import type {
GraphNode,
NodeLabel,
ParameterTypeClass,
RelationshipType,
} from 'gitnexus-shared';
import type { ExtractedRoute } from './route-extractors/laravel.js';
import type { SharedSpringType } from './route-extractors/spring-shared.js';
import type {
@ -120,6 +125,63 @@ export function mergeCanonicalDefinitionProperties<
return { ...providerProperties, ...canonicalProperties } as Record<string, unknown> & TCanonical;
}
export interface ProviderSemanticNode {
readonly id: string;
readonly label: NodeLabel;
readonly properties: {
readonly name: string;
readonly filePath: string;
readonly startLine: number;
readonly endLine: number;
readonly language: SupportedLanguages;
readonly isExported: boolean;
readonly qualifiedName?: string;
readonly parameterCount?: number;
readonly requiredParameterCount?: number;
readonly parameterTypes?: readonly string[];
readonly parameterTypeClasses?: readonly ParameterTypeClass[];
readonly returnType?: string;
readonly declaredType?: string;
readonly visibility?: string;
readonly isStatic?: boolean;
readonly isReadonly?: boolean;
readonly [key: string]: unknown;
};
}
export interface ProviderSemanticRelationship {
readonly id: string;
readonly sourceId: string;
readonly targetId: string;
readonly type: RelationshipType;
readonly confidence: number;
readonly reason: string;
}
export interface ProviderSemanticSymbol {
readonly filePath: string;
readonly name: string;
readonly nodeId: string;
readonly type: NodeLabel;
readonly qualifiedName?: string;
readonly parameterCount?: number;
readonly requiredParameterCount?: number;
readonly parameterTypes?: readonly string[];
readonly parameterTypeClasses?: readonly ParameterTypeClass[];
readonly returnType?: string;
readonly declaredType?: string;
readonly ownerId?: string;
readonly visibility?: string;
readonly isStatic?: boolean;
readonly isReadonly?: boolean;
}
export interface ProviderSemanticGraph {
readonly nodes: readonly ProviderSemanticNode[];
readonly relationships: readonly ProviderSemanticRelationship[];
readonly symbols: readonly ProviderSemanticSymbol[];
}
// ── Strategy tag types ─────────────────────────────────────────────────────
// NOTE: `MroStrategy` is defined in `gitnexus-shared` and re-exported above
// so `core/ingestion/model/resolve.ts` can consume it without importing from
@ -230,6 +292,29 @@ interface LanguageProviderConfig {
*/
readonly runtimeSymbolStrategy?: RuntimeSymbolStrategy;
/**
* Optional content-based language classifier. The filename detector remains
* the default source of truth; this hook lets a provider claim ambiguous
* files only when the source text carries language-specific evidence.
*
* Used for extensions shared by several languages, where mapping the suffix
* globally would steal files from an existing provider. Implementations must
* be deterministic and conservative: false negatives are acceptable, false
* positives change which parser and resolver consumes the file.
*
* Default: undefined (provider never overrides filename detection).
*/
readonly classifyFileContent?: (filePath: string, sourceText: string) => boolean;
/**
* Cheap path-only prefilter for `classifyFileContent`. When supplied, callers
* can avoid loading source text for files this provider would never claim.
*
* Default: undefined (only callers that already have content invoke
* `classifyFileContent`).
*/
readonly shouldClassifyFileContent?: (filePath: string) => boolean;
// ── Core (required) ───────────────────────────────────────────────
/** Type extraction: declarations, initializers, for-loop bindings */
readonly typeConfig: LanguageTypeConfig;
@ -643,6 +728,23 @@ interface LanguageProviderConfig {
repo: RepoConstants,
) => string | null;
/**
* Optional provider-owned semantic graph extraction for languages whose
* stable symbol identities cannot be represented by the generic query
* pipeline's `(label, filePath, qualifiedName)` rule.
*
* Runs in the parse worker after tree-sitter has parsed the file and after
* `extractParsedFile` has produced the scope-resolution artifact. The hook is
* deterministic and AST-based: it receives the already-parsed tree and must
* return plain graph nodes/relationships/symbol-table rows. Existing
* providers leave it undefined, preserving the generic capture path exactly.
*/
readonly extractSemanticGraph?: (
tree: Parser.Tree,
filePath: string,
sourceText: string,
) => ProviderSemanticGraph;
// ── Noise filtering ────────────────────────────────────────────────
/** Built-in/stdlib names that should be filtered from the call graph for this language.
* Default: undefined (no language-specific filtering). */

View file

@ -19,6 +19,7 @@ import { goProvider } from './go.js';
import { rustProvider } from './rust.js';
import { csharpProvider } from './csharp.js';
import { cProvider, cppProvider } from './c-cpp.js';
import { objectiveCProvider } from './objective-c.js';
import { phpProvider } from './php.js';
import { rubyProvider } from './ruby.js';
import { swiftProvider } from './swift.js';
@ -38,6 +39,7 @@ export const providers = {
[SupportedLanguages.CSharp]: csharpProvider,
[SupportedLanguages.C]: cProvider,
[SupportedLanguages.CPlusPlus]: cppProvider,
[SupportedLanguages.ObjectiveC]: objectiveCProvider,
[SupportedLanguages.PHP]: phpProvider,
[SupportedLanguages.Ruby]: rubyProvider,
[SupportedLanguages.Swift]: swiftProvider,
@ -70,3 +72,34 @@ export function getProviderForFile(filePath: string): LanguageProvider | null {
const basename = filePath.slice(filePath.lastIndexOf('/') + 1);
return extensionMap.get(ext) ?? extensionMap.get(basename) ?? null;
}
/** Return the provider whose content classifier confidently claims this file. */
export function getProviderForFileContent(
filePath: string,
content: string,
): LanguageProvider | null {
if (isBladeTemplateFilename(filePath)) return null;
for (const provider of Object.values(providers)) {
if (provider.classifyFileContent?.(filePath, content) === true) return provider;
}
return getProviderForFile(filePath);
}
/** True when at least one provider wants source text before language bucketing. */
export function needsContentLanguageClassification(filePath: string): boolean {
if (isBladeTemplateFilename(filePath)) return false;
return Object.values(providers).some(
(provider) =>
provider.classifyFileContent !== undefined &&
provider.shouldClassifyFileContent?.(filePath) === true,
);
}
/** Return the effective language for a file, optionally using source content. */
export function getLanguageForFileContent(
filePath: string,
content: string,
): SupportedLanguages | null {
return getProviderForFileContent(filePath, content)?.id ?? null;
}

View file

@ -0,0 +1,195 @@
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";
import {
buildObjectiveCSemanticGraph,
buildObjectiveCScopeCaptures,
collectObjectiveCCaptureSideChannel,
collectObjectiveCFacts,
parseObjCType,
setObjectiveCFileFacts,
} from "./objective-c/facts.js";
const OBJECTIVE_C_SCOPE_QUERY = `((translation_unit) @objc.root)`;
const EMPTY_TYPE_CONFIG = {
declarationNodeTypes: new Set<string>(),
extractDeclaration: () => null,
extractParameter: () => null,
};
const noImportResolution: ImportResolverFn = () => null;
function normalizedExt(filePath: string): string {
return path.extname(filePath).toLowerCase();
}
function isObjectiveCSourcePath(filePath: string): boolean {
const ext = normalizedExt(filePath);
return ext === ".m" || ext === ".mm";
}
function isHeaderPath(filePath: string): boolean {
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",
]);
const OBJECTIVE_C_FRAMEWORK_NAMES = [
"AppKit",
"Foundation",
"UIKit",
"CoreData",
"CoreFoundation",
"QuartzCore",
"Swift",
];
function hasObjectiveCHeaderSyntax(sourceText: string): boolean {
try {
const tree = parseObjectiveCSource(sourceText);
const stack: Parser.SyntaxNode[] = [tree.rootNode];
while (stack.length > 0) {
const node = stack.pop()!;
if (OBJECTIVE_C_HEADER_NODE_TYPES.has(node.type)) return true;
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),
)
) {
return true;
}
}
for (let i = node.namedChildCount - 1; i >= 0; i--) {
const child = node.namedChild(i);
if (child !== null) stack.push(child);
}
}
} catch {
// The regular parser availability path reports the actionable grammar error.
}
return false;
}
export function classifyObjectiveCFileContent(
filePath: string,
sourceText: string,
): boolean {
if (isObjectiveCSourcePath(filePath)) return true;
if (!isHeaderPath(filePath)) return false;
return hasObjectiveCHeaderSyntax(sourceText);
}
function parseObjectiveCSource(sourceText: string): Parser.Tree {
const parser = new Parser();
parser.setLanguage(getLanguageGrammar(SupportedLanguages.ObjectiveC));
return parser.parse(sourceText);
}
function treeFromCachedOrSource(
cachedTree: unknown,
sourceText: string,
): Parser.Tree {
if (cachedTree !== undefined && looksLikeTree(cachedTree)) return cachedTree;
return parseObjectiveCSource(sourceText);
}
function looksLikeTree(value: unknown): value is Parser.Tree {
return (
value !== null &&
typeof value === "object" &&
"rootNode" in value &&
(value as { rootNode?: unknown }).rootNode !== undefined
);
}
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();
return {
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}`,
};
}
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",
};
}
export const objectiveCProvider = defineLanguage({
id: SupportedLanguages.ObjectiveC,
extensions: [".m", ".mm"],
treeSitterQueries: OBJECTIVE_C_SCOPE_QUERY,
typeConfig: EMPTY_TYPE_CONFIG,
exportChecker: () => true,
importResolver: noImportResolution,
classifyFileContent: classifyObjectiveCFileContent,
shouldClassifyFileContent: isHeaderPath,
importsExecuteWhereWritten: false,
emitScopeCaptures: (
sourceText,
filePath,
cachedTree,
): readonly CaptureMatch[] => {
const tree = treeFromCachedOrSource(cachedTree, sourceText);
const facts = collectObjectiveCFacts(tree, filePath);
setObjectiveCFileFacts(facts);
return buildObjectiveCScopeCaptures(facts, tree.rootNode);
},
collectCaptureSideChannel: (filePath) =>
assertCloneable(collectObjectiveCCaptureSideChannel(filePath)),
interpretImport: interpretObjectiveCImport,
interpretTypeBinding: interpretObjectiveCTypeBinding,
extractSemanticGraph: (tree, filePath) => {
const facts = collectObjectiveCFacts(tree, filePath);
setObjectiveCFileFacts(facts);
return buildObjectiveCSemanticGraph(facts);
},
});

View file

@ -0,0 +1,27 @@
import type { AnalysisFeatureDescriptor } from '../../../analysis-features.js';
import {
OBJECTIVE_C_GRAMMAR_PACKAGE,
OBJECTIVE_C_GRAMMAR_VERSION,
OBJECTIVE_C_PROVIDER_VERSION,
} from './facts.js';
function isObjectiveCProviderCandidatePath(filePath: string): boolean {
const normalized = filePath.replaceAll('\\', '/').toLowerCase();
return normalized.endsWith('.m') || normalized.endsWith('.mm') || normalized.endsWith('.h');
}
/**
* Durable metadata stamp for Objective-C semantic indexing. The feature id
* carries provider and grammar versions verbatim so a semantic identity/edge
* change records the exact producer in index metadata and forces a full rebuild.
*
* `.h` is included only as a path-level rebuild predicate; content classification
* still decides whether a header is actually parsed as Objective-C.
*/
export const OBJECTIVE_C_PROVIDER_FEATURE: AnalysisFeatureDescriptor = {
id:
`objective-c.provider-${OBJECTIVE_C_PROVIDER_VERSION}.` +
`${OBJECTIVE_C_GRAMMAR_PACKAGE}-${OBJECTIVE_C_GRAMMAR_VERSION}`,
version: 1,
appliesTo: (filePaths) => filePaths.some(isObjectiveCProviderCandidatePath),
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,680 @@
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 {
applyObjectiveCCaptureSideChannel,
objcCategoryQualifiedName,
objcClassQualifiedName,
objcMethodQualifiedName,
objcProtocolQualifiedName,
objectiveCFactsFromParsedFiles,
type ObjCContainerFact,
type ObjCFileFacts,
type ObjCMessageFact,
type ObjCMethodFact,
} 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 methodsByExactOwner: ReadonlyMap<string, readonly ObjCMethodFact[]>;
readonly classProtocols: ReadonlyMap<string, ReadonlySet<string>>;
readonly superclassByClass: ReadonlyMap<string, string>;
}
export const objectiveCScopeResolver: ScopeResolver = {
language: SupportedLanguages.ObjectiveC,
languageProvider: objectiveCProvider,
importEdgeReason: "objective-c-scope: import",
resolveImportTarget: (targetRaw, fromFile, allFilePaths) =>
resolveObjectiveCImportTarget(targetRaw, fromFile, allFilePaths),
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";
},
buildMro: () => new Map(),
applyCaptureSideChannel: applyObjectiveCCaptureSideChannel,
populateOwners: () => {},
isSuperReceiver: (receiverText) => receiverText.trim() === "super",
fieldFallbackOnMethodLookup: false,
propagatesReturnTypesAcrossImports: false,
collapseMemberCallsByCallerTarget: true,
emitPostResolutionEdges(graph, parsedFiles) {
const facts = objectiveCFactsFromParsedFiles(parsedFiles);
if (facts.length === 0) return;
const workspace = buildObjectiveCWorkspaceFacts(facts);
for (const fact of facts) {
emitObjectiveCHeritageEdges(graph, fact, workspace);
emitObjectiveCCategoryEdges(graph, fact);
emitObjectiveCImplementationEvidence(graph, fact);
emitObjectiveCMessageEdges(graph, fact, workspace);
}
},
};
function graphNodeId(label: string, qualifiedName: string): string {
return generateId(label, qualifiedName);
}
function relationshipId(
type: RelationshipType,
sourceId: string,
targetId: string,
reason: string,
): string {
return generateId(type, `${sourceId}->${targetId}:${reason}`);
}
function addRelationship(
graph: KnowledgeGraph,
type: RelationshipType,
sourceId: string,
targetId: string,
reason: string,
confidence = 0.9,
): void {
graph.addRelationship({
id: relationshipId(type, sourceId, targetId, reason),
sourceId,
targetId,
type,
confidence,
reason,
});
}
function labelForContainer(
container: ObjCContainerFact,
): "Class" | "Protocol" | "Category" {
return container.label;
}
function buildObjectiveCWorkspaceFacts(
facts: readonly ObjCFileFacts[],
): ObjCWorkspaceFacts {
const containersByQualifiedName = new Map<string, ObjCContainerFact>();
const classByName = new Map<string, ObjCContainerFact>();
const protocolsByName = new Map<string, ObjCContainerFact>();
const categoriesByHost = new Map<string, ObjCContainerFact[]>();
const methodsByExactOwner = new Map<string, ObjCMethodFact[]>();
const methodsByDispatchOwner = new Map<string, ObjCMethodFact[]>();
const classProtocols = new Map<string, Set<string>>();
const superclassByClass = new Map<string, string>();
for (const fileFact of facts) {
for (const container of fileFact.containers) {
const existing = containersByQualifiedName.get(container.qualifiedName);
containersByQualifiedName.set(
container.qualifiedName,
mergeContainerFacts(existing, container),
);
if (container.kind === "class") {
classByName.set(container.name, container);
if (container.superclass !== undefined)
superclassByClass.set(container.name, container.superclass);
if (container.protocols.length > 0) {
let protocols = classProtocols.get(container.name);
if (protocols === undefined) {
protocols = new Set();
classProtocols.set(container.name, protocols);
}
for (const protocol of container.protocols) protocols.add(protocol);
}
} else if (container.kind === "protocol") {
protocolsByName.set(container.name, container);
} else if (container.hostClass !== undefined) {
let categories = categoriesByHost.get(container.hostClass);
if (categories === undefined) {
categories = [];
categoriesByHost.set(container.hostClass, categories);
}
categories.push(container);
if (container.protocols.length > 0) {
let protocols = classProtocols.get(container.hostClass);
if (protocols === undefined) {
protocols = new Set();
classProtocols.set(container.hostClass, protocols);
}
for (const protocol of container.protocols) protocols.add(protocol);
}
}
}
for (const method of fileFact.methods) {
appendMap(methodsByExactOwner, method.ownerQualifiedName, method);
appendMap(methodsByDispatchOwner, method.ownerQualifiedName, method);
if (method.hostClass !== undefined) {
appendMap(
methodsByDispatchOwner,
objcClassQualifiedName(method.hostClass),
method,
);
}
}
}
return {
containersByQualifiedName,
classByName,
protocolsByName,
categoriesByHost,
methodsByDispatchOwner,
methodsByExactOwner,
classProtocols,
superclassByClass,
};
}
function mergeContainerFacts(
existing: ObjCContainerFact | undefined,
incoming: ObjCContainerFact,
): ObjCContainerFact {
if (existing === undefined) return incoming;
const protocols = Array.from(
new Set([...existing.protocols, ...incoming.protocols]),
).sort();
return {
...existing,
declarationRole:
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
? { superclass: existing.superclass ?? incoming.superclass }
: {}),
protocols,
};
}
function appendMap<K, V>(map: Map<K, V[]>, key: K, value: V): void {
const existing = map.get(key);
if (existing === undefined) map.set(key, [value]);
else existing.push(value);
}
function emitObjectiveCHeritageEdges(
graph: KnowledgeGraph,
facts: ObjCFileFacts,
workspace: ObjCWorkspaceFacts,
): void {
for (const container of facts.containers) {
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",
sourceId,
graphNodeId("Class", superclass.qualifiedName),
"objc: superclass",
);
}
}
const protocolSourceId =
container.hostClass !== undefined
? 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",
protocolSourceId,
graphNodeId("Protocol", protocol.qualifiedName),
"objc: protocol conformance",
);
}
}
}
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",
);
}
}
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,
);
emitImplementationEvidence(
graph,
facts.filePath,
targetId,
`@implementation ${container.name}`,
`objc:implementation:${container.qualifiedName}:${facts.filePath}:${container.startLine}`,
container.startLine,
container.endLine,
{
objectiveCKind: "implementation-evidence",
implementationKind: container.kind,
targetQualifiedName: container.qualifiedName,
},
);
}
for (const method of facts.methods) {
if (method.declarationRole !== "implementation") continue;
emitImplementationEvidence(
graph,
facts.filePath,
method.nodeId,
`${method.methodKind}[${method.ownerName} ${method.selector}] implementation`,
`objc:method-implementation:${method.qualifiedName}:${facts.filePath}:${method.startLine}`,
method.startLine,
method.endLine,
{
objectiveCKind: "implementation-evidence",
implementationKind: "method",
targetQualifiedName: method.qualifiedName,
selector: method.selector,
methodKind: method.methodKind,
objectiveCOwner: method.ownerQualifiedName,
},
);
}
}
function emitImplementationEvidence(
graph: KnowledgeGraph,
filePath: string,
targetId: string,
name: string,
qualifiedName: string,
startLine: number,
endLine: number,
extras: Record<string, unknown>,
): void {
const nodeId = graphNodeId("CodeElement", qualifiedName);
graph.addNode({
id: nodeId,
label: "CodeElement",
properties: {
name,
qualifiedName,
filePath,
startLine,
endLine,
language: SupportedLanguages.ObjectiveC,
isExported: false,
...extras,
},
});
addRelationship(
graph,
"DEFINES",
graphNodeId("File", filePath),
nodeId,
"objc: implementation evidence",
1,
);
addRelationship(
graph,
"DECLARES",
nodeId,
targetId,
"objc: implementation of merged symbol",
1,
);
}
function emitObjectiveCMessageEdges(
graph: KnowledgeGraph,
facts: ObjCFileFacts,
workspace: ObjCWorkspaceFacts,
): 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,
);
}
for (const target of targets.methods) {
if (graph.getNode(target.nodeId) === undefined) continue;
addRelationship(
graph,
"CALLS",
message.sourceMethodId,
target.nodeId,
targets.kind === "protocol"
? "objc-message: protocol receiver"
: `objc-message: ${message.receiverKind} receiver`,
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: "protocol";
readonly protocolName: string;
readonly methods: readonly ObjCMethodFact[];
readonly candidates: readonly ObjCMethodFact[];
};
function resolveMessageTargets(
message: ObjCMessageFact,
workspace: ObjCWorkspaceFacts,
): MessageTargets {
if (
message.receiverKind === "dynamic" ||
message.receiverKind === "unknown"
) {
return { kind: "none", methods: [] };
}
if (message.receiverKind === "class") {
const className = message.receiverType?.name ?? message.receiverText;
return {
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;
const methods =
owner?.kind === "protocol"
? findExactOwnerMethods(
workspace,
owner.qualifiedName,
message.sourceMethodKind,
message.selector,
)
: 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;
const superclass = workspace.superclassByClass.get(className);
return superclass === undefined
? { kind: "none", methods: [] }
: {
kind: "direct",
methods: findDispatchMethods(
workspace,
superclass,
message.sourceMethodKind,
message.selector,
),
};
}
const receiverType = message.receiverType;
if (receiverType?.kind === "class" && receiverType.name !== undefined) {
return {
kind: "direct",
methods: findDispatchMethods(
workspace,
receiverType.name,
"-",
message.selector,
),
};
}
if (receiverType?.kind === "protocol" && receiverType.name !== undefined) {
const protocolQn = objcProtocolQualifiedName(receiverType.name);
const methods = findExactOwnerMethods(
workspace,
protocolQn,
"-",
message.selector,
);
const candidates = findProtocolImplementationCandidates(
workspace,
receiverType.name,
message.selector,
);
return {
kind: "protocol",
protocolName: receiverType.name,
methods,
candidates,
};
}
return { kind: "none", methods: [] };
}
function findDispatchMethods(
workspace: ObjCWorkspaceFacts,
className: string,
methodKind: "-" | "+",
selector: string,
): readonly ObjCMethodFact[] {
const seen = new Set<string>();
let currentClass: string | undefined = className;
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,
);
if (methods.length > 0) return methods;
currentClass = workspace.superclassByClass.get(currentClass);
}
return [];
}
function findExactOwnerMethods(
workspace: ObjCWorkspaceFacts,
ownerQualifiedName: string,
methodKind: "-" | "+",
selector: string,
): readonly ObjCMethodFact[] {
return (workspace.methodsByExactOwner.get(ownerQualifiedName) ?? []).filter(
(method) =>
method.methodKind === methodKind && method.selector === selector,
);
}
function findProtocolImplementationCandidates(
workspace: ObjCWorkspaceFacts,
protocolName: string,
selector: string,
): readonly ObjCMethodFact[] {
const out: ObjCMethodFact[] = [];
for (const [className, protocols] of workspace.classProtocols) {
if (!protocols.has(protocolName)) continue;
out.push(...findDispatchMethods(workspace, className, "-", selector));
}
return out;
}
function emitProtocolMessageEvidence(
graph: KnowledgeGraph,
facts: ObjCFileFacts,
message: ObjCMessageFact,
protocolName: string,
candidates: readonly ObjCMethodFact[],
): 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 node: GraphNode = {
id: nodeId,
label: "CodeElement",
properties: {
name: `[${message.receiverText} ${message.selector}] candidates`,
qualifiedName,
filePath: facts.filePath,
startLine: message.startLine,
endLine: message.startLine,
language: SupportedLanguages.ObjectiveC,
isExported: false,
objectiveCKind: "protocol-candidate-implementations",
protocolName,
selector: message.selector,
sourceMethod: message.sourceMethodQualifiedName,
candidateImplementations: candidates
.map((candidate) => candidate.qualifiedName)
.sort(),
},
};
graph.addNode(node);
addRelationship(
graph,
"DEFINES",
graphNodeId("File", facts.filePath),
nodeId,
"objc: protocol receiver candidate evidence",
1,
);
addRelationship(
graph,
"USES",
message.sourceMethodId,
nodeId,
"objc-message: protocol receiver candidates",
0.7,
);
}
function resolveObjectiveCImportTarget(
targetRaw: string,
fromFile: string,
allFilePaths: ReadonlySet<string>,
): string | null {
const importIndex = getObjectiveCImportIndex(allFilePaths);
const target = targetRaw.trim();
if (target.length === 0) return null;
const looksLikeFileImport =
target.startsWith(".") ||
target.includes("/") ||
path.posix.extname(target).length > 0;
if (!looksLikeFileImport) return null;
return findImportCandidate(target, fromFile, importIndex);
}
interface ObjectiveCImportIndex {
readonly filePaths: readonly string[];
readonly filePathSet: ReadonlySet<string>;
}
const getObjectiveCImportIndex = perFileSet(
(allFilePaths: ReadonlySet<string>): ObjectiveCImportIndex => {
const filePaths = [...allFilePaths];
return { filePaths, filePathSet: new Set(filePaths) };
},
);
function findImportCandidate(
targetRaw: string,
fromFile: string,
importIndex: ObjectiveCImportIndex,
): string | null {
const normalizedTarget = normalizeRepoPath(targetRaw);
const fromDir = normalizeRepoPath(
path.posix.dirname(normalizeRepoPath(fromFile)),
);
const spelledCandidates = new Set<string>([
normalizeRepoPath(path.posix.join(fromDir, normalizedTarget)),
normalizedTarget,
]);
const ext = path.posix.extname(normalizedTarget);
if (ext.length === 0) {
for (const base of [...spelledCandidates]) {
spelledCandidates.add(`${base}.h`);
spelledCandidates.add(`${base}.m`);
spelledCandidates.add(`${base}.mm`);
}
}
for (const candidate of spelledCandidates) {
if (importIndex.filePathSet.has(candidate)) return candidate;
}
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;
}
return null;
}
function normalizeRepoPath(value: string): string {
return value.replaceAll("\\", "/").replace(/^\.\//, "");
}

View file

@ -19,7 +19,7 @@
*
* | Group | NodeLabel values | Hook | Skip callable? |
* |---------------|---------------------------------------------------|--------------|----------------|
* | class-like | Class, Struct, Interface, Enum, Record, Trait | classLikeHook | no |
* | class-like | Class, Protocol, Category, Struct, Interface, Enum, Record, Trait | classLikeHook | no |
* | method-like | Method, Constructor | methodHook | no |
* | property | Property | propertyHook | YES |
* | impl-block | Impl | implHook | no |
@ -143,6 +143,8 @@ export type LabelBehavior = 'dispatch' | 'callable-only' | 'inert';
const LABEL_BEHAVIOR = {
// dispatch — owner-scoped registry writes
Class: 'dispatch',
Protocol: 'dispatch',
Category: 'dispatch',
Struct: 'dispatch',
Interface: 'dispatch',
Enum: 'dispatch',
@ -316,6 +318,8 @@ export const createRegistrationTable = (
// `Record<ClassLikeLabel, 'dispatch'>` cross-invariant on
// `LABEL_BEHAVIOR`.
Class: classLikeHook,
Protocol: classLikeHook,
Category: classLikeHook,
Struct: classLikeHook,
Interface: classLikeHook,
Enum: classLikeHook,

View file

@ -56,6 +56,8 @@ import type { NodeLabel, ParameterTypeClass, SymbolDefinition } from 'gitnexus-s
*/
export const CLASS_TYPES_TUPLE = [
'Class',
'Protocol',
'Category',
'Struct',
'Interface',
'Enum',

View file

@ -1,7 +1,7 @@
import type { NodeLabel } from 'gitnexus-shared';
import { KnowledgeGraph } from '../graph/types.js';
import type { SymbolTableWriter } from './model/index.js';
import { getLanguageFromFilename } from 'gitnexus-shared';
import { getLanguageForFileContent } from './languages/index.js';
import { accumulateExportedTypesFromParsedNode, type ExportedTypeMap } from './call-processor.js';
@ -229,7 +229,7 @@ export const dispatchChunkParse = async (
): Promise<ParseWorkerResult[]> => {
const parseableFiles: ParseWorkerInput[] = [];
for (const file of files) {
const lang = getLanguageFromFilename(file.path);
const lang = getLanguageForFileContent(file.path, file.content);
if (lang) parseableFiles.push({ path: file.path, content: file.content });
}
if (parseableFiles.length === 0) return [];

View file

@ -847,6 +847,10 @@ function normalizeNodeLabel(kindStr: string): SymbolDefinition['type'] | undefin
switch (kindStr.toLowerCase()) {
case 'class':
return 'Class';
case 'protocol':
return 'Protocol';
case 'category':
return 'Category';
case 'interface':
return 'Interface';
case 'enum':

View file

@ -87,6 +87,8 @@ export const CALLER_ANCHOR_LABELS: ReadonlySet<NodeLabel> = new Set<NodeLabel>([
'Constructor',
'Module',
'Class',
'Protocol',
'Category',
'Interface',
'Struct',
'Enum',

View file

@ -280,6 +280,8 @@ export const LINKABLE_LABELS: ReadonlySet<NodeLabel> = new Set<NodeLabel>([
// targets and need the same def→graph bridge.
'Module',
'Class',
'Protocol',
'Category',
'Interface',
'Struct',
'Enum',

View file

@ -34,6 +34,10 @@ import { runScopeResolution, type ScopeResolutionSubPhase } from './run.js';
import { isLanguageAvailable } from '../../../tree-sitter/parser-loader.js';
import { buildGraphNodeLookup } from '../graph-bridge/node-lookup.js';
import { SCOPE_RESOLVERS } from './registry.js';
import {
getLanguageForFileContent,
needsContentLanguageClassification,
} from '../../languages/index.js';
import { isDev, isSemanticModelValidatorEnabled } from '../../utils/env.js';
import { logHeapProbe } from '../../utils/heap-probe.js';
import {
@ -247,6 +251,13 @@ export const scopeResolutionPhase: PipelinePhase<ScopeResolutionOutput> = {
let totalScopeFiles = 0;
let totalScopeLangs = 0;
const allScannedPaths = new Set(scannedFiles.map((f) => f.path));
const contentClassifiedPaths = scannedFiles
.map((f) => f.path)
.filter(needsContentLanguageClassification);
const contentClassificationMap =
contentClassifiedPaths.length > 0
? await readFileContents(ctx.repoPath, contentClassifiedPaths)
: new Map<string, string>();
// Partition scanned files by language ONCE (O(F)). The previous code
// re-filtered all scannedFiles per language for the precount AND again in the
// per-language loop below — O(languages × files), ~2.3M getLanguageFromFilename
@ -257,7 +268,11 @@ export const scopeResolutionPhase: PipelinePhase<ScopeResolutionOutput> = {
(typeof scannedFiles)[number][]
>();
for (const f of scannedFiles) {
const fileLang = getLanguageFromFilename(f.path);
const classifiedContent = contentClassificationMap.get(f.path);
const fileLang =
classifiedContent !== undefined
? getLanguageForFileContent(f.path, classifiedContent)
: getLanguageFromFilename(f.path);
if (fileLang === null) continue;
// Tree-sitter providers require an available grammar. Standalone regex
// providers deliberately have none and re-extract on the main thread.

View file

@ -28,6 +28,7 @@ import { swiftScopeResolver } from '../../languages/swift/scope-resolver.js';
import { dartScopeResolver } from '../../languages/dart/scope-resolver.js';
import { vueScopeResolver } from '../../languages/vue/scope-resolver.js';
import { zigScopeResolver } from '../../languages/zig/scope-resolver.js';
import { objectiveCScopeResolver } from '../../languages/objective-c/scope-resolver.js';
/** Map of `SupportedLanguages` → `ScopeResolver`. The scope-resolution phase
* iterates this map directly — every registered resolver runs. This is the
@ -53,4 +54,6 @@ export const SCOPE_RESOLVERS: ReadonlyMap<SupportedLanguages, ScopeResolver> = n
[SupportedLanguages.Dart, dartScopeResolver],
[SupportedLanguages.Vue, vueScopeResolver],
[SupportedLanguages.Zig, zigScopeResolver],
[SupportedLanguages.ObjectiveC, objectiveCScopeResolver],
[SupportedLanguages.Zig, zigScopeResolver],
]);

View file

@ -181,6 +181,8 @@ export function namesAtScope(scopeId: ScopeId, scopes: ScopeResolutionIndexes):
export function isClassLike(t: string): boolean {
return (
t === 'Class' ||
t === 'Protocol' ||
t === 'Category' ||
t === 'Interface' ||
t === 'Struct' ||
t === 'Record' ||

View file

@ -2610,12 +2610,15 @@ export const ZIG_QUERIES = `
import { SupportedLanguages } from 'gitnexus-shared';
const OBJECTIVE_C_QUERIES = `((translation_unit) @objc.root)`;
export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
[SupportedLanguages.TypeScript]: TYPESCRIPT_QUERIES,
[SupportedLanguages.JavaScript]: JAVASCRIPT_QUERIES,
[SupportedLanguages.Python]: PYTHON_QUERIES,
[SupportedLanguages.Java]: JAVA_QUERIES,
[SupportedLanguages.C]: C_QUERIES,
[SupportedLanguages.ObjectiveC]: OBJECTIVE_C_QUERIES,
[SupportedLanguages.Go]: GO_QUERIES,
[SupportedLanguages.CPlusPlus]: CPP_QUERIES,
[SupportedLanguages.CSharp]: CSHARP_QUERIES,

View file

@ -23,6 +23,8 @@ export const SYMBOL_NODE_LABELS: ReadonlySet<NodeLabel> = new Set<NodeLabel>([
'Function',
'Method',
'Class',
'Protocol',
'Category',
'Interface',
'CodeElement',
'Struct',

View file

@ -20,7 +20,7 @@ import PHP from 'tree-sitter-php';
import Ruby from 'tree-sitter-ruby';
import { requireVendoredGrammar } from '../../tree-sitter/vendored-grammars.js';
import { SupportedLanguages } from 'gitnexus-shared';
import { getProvider } from '../languages/index.js';
import { getLanguageForFileContent, getProvider } from '../languages/index.js';
import {
getTreeSitterBufferSize,
getTreeSitterContentByteLength,
@ -90,6 +90,11 @@ let Zig: TreeSitterLanguage | null = null;
try {
Zig = _require('@tree-sitter-grammars/tree-sitter-zig');
} catch {}
let ObjectiveC: TreeSitterLanguage | null = null;
try {
ObjectiveC = requireVendoredGrammar('tree-sitter-objc') as TreeSitterLanguage;
} catch {}
import { getLanguageFromFilename } from 'gitnexus-shared';
import {
buildDefinitionPreScan,
@ -254,7 +259,7 @@ interface ParsedRelationship {
id: string;
sourceId: string;
targetId: string;
type: 'DEFINES' | 'HAS_METHOD' | 'HAS_PROPERTY';
type: 'DEFINES' | 'DECLARES' | 'HAS_METHOD' | 'HAS_PROPERTY';
confidence: number;
reason: string;
}
@ -560,6 +565,7 @@ const languageMap: Record<string, TreeSitterLanguage> = {
[SupportedLanguages.Java]: Java,
...(C ? { [SupportedLanguages.C]: C } : {}),
[SupportedLanguages.CPlusPlus]: CPP,
...(ObjectiveC ? { [SupportedLanguages.ObjectiveC]: ObjectiveC } : {}),
[SupportedLanguages.CSharp]: CSharp,
[SupportedLanguages.Go]: Go,
[SupportedLanguages.Rust]: Rust,
@ -1272,7 +1278,7 @@ const processBatch = (
// Group by language to minimize setLanguage calls
const byLanguage = new Map<SupportedLanguages, ParseWorkerInput[]>();
for (const file of files) {
const lang = getLanguageFromFilename(file.path);
const lang = getLanguageForFileContent(file.path, file.content);
if (!lang) continue;
let list = byLanguage.get(lang);
if (!list) {
@ -1731,6 +1737,52 @@ const processFileGroup = (
result.parsedFiles.push(withChannels);
}
const semanticGraph = provider.extractSemanticGraph?.(tree, file.path, parseContent);
if (semanticGraph !== undefined) {
for (const node of semanticGraph.nodes) {
result.nodes.push({
id: node.id,
label: node.label,
properties: { ...node.properties },
});
}
for (const relationship of semanticGraph.relationships) {
if (
relationship.type === 'DECLARES' ||
relationship.type === 'DEFINES' ||
relationship.type === 'HAS_METHOD' ||
relationship.type === 'HAS_PROPERTY'
) {
result.relationships.push({ ...relationship, type: relationship.type });
}
}
for (const symbol of semanticGraph.symbols) {
result.symbols.push({
filePath: symbol.filePath,
name: symbol.name,
nodeId: symbol.nodeId,
type: symbol.type,
...(symbol.qualifiedName !== undefined ? { qualifiedName: symbol.qualifiedName } : {}),
...(symbol.parameterCount !== undefined ? { parameterCount: symbol.parameterCount } : {}),
...(symbol.requiredParameterCount !== undefined
? { requiredParameterCount: symbol.requiredParameterCount }
: {}),
...(symbol.parameterTypes !== undefined
? { parameterTypes: [...symbol.parameterTypes] }
: {}),
...(symbol.parameterTypeClasses !== undefined
? { parameterTypeClasses: [...symbol.parameterTypeClasses] }
: {}),
...(symbol.returnType !== undefined ? { returnType: symbol.returnType } : {}),
...(symbol.declaredType !== undefined ? { declaredType: symbol.declaredType } : {}),
...(symbol.ownerId !== undefined ? { ownerId: symbol.ownerId } : {}),
...(symbol.visibility !== undefined ? { visibility: symbol.visibility } : {}),
...(symbol.isStatic !== undefined ? { isStatic: symbol.isStatic } : {}),
...(symbol.isReadonly !== undefined ? { isReadonly: symbol.isReadonly } : {}),
});
}
}
// Build per-file type environment + constructor bindings in a single AST walk.
// The legacy heritage pre-pass that seeded a file-local parentMap for
// buildTypeEnv was removed in RING4-1 (#942) along with the rest of the

View file

@ -568,6 +568,8 @@ export const streamAllCSVsToDisk = async (
const multiLangHeader = 'id,name,filePath,startLine,endLine,content,description';
const constHeader = `${multiLangHeader},convexEndpointFactory`;
const MULTI_LANG_TYPES = [
'Protocol',
'Category',
'Struct',
'Enum',
'Macro',

View file

@ -69,6 +69,30 @@ CREATE NODE TABLE Class (
PRIMARY KEY (id)
)`;
export const PROTOCOL_SCHEMA = `
CREATE NODE TABLE Protocol (
id STRING,
name STRING,
filePath STRING,
startLine INT64,
endLine INT64,
content STRING,
description STRING,
PRIMARY KEY (id)
)`;
export const CATEGORY_SCHEMA = `
CREATE NODE TABLE Category (
id STRING,
name STRING,
filePath STRING,
startLine INT64,
endLine INT64,
content STRING,
description STRING,
PRIMARY KEY (id)
)`;
export const INTERFACE_SCHEMA = `
CREATE NODE TABLE Interface (
id STRING,
@ -581,6 +605,9 @@ export const STRUCTURAL_PAIR_DDL = ` FROM File TO Folder,
FROM \`Module\` TO \`Namespace\`,
FROM \`Namespace\` TO Function,
FROM CodeElement TO CodeElement,
FROM CodeElement TO Class,
FROM CodeElement TO Category,
FROM CodeElement TO Method,
FROM CodeElement TO \`Module\`,
FROM CodeElement TO \`Property\`,
FROM Section TO Section,
@ -714,6 +741,8 @@ export const NODE_SCHEMA_QUERIES = [
FOLDER_SCHEMA,
FUNCTION_SCHEMA,
CLASS_SCHEMA,
PROTOCOL_SCHEMA,
CATEGORY_SCHEMA,
INTERFACE_SCHEMA,
METHOD_SCHEMA,
CODE_ELEMENT_SCHEMA,

View file

@ -25,6 +25,8 @@ export const FTS_INDEXES: readonly FTSIndexDefinition[] = [
// Original 5 (minus File) gain `description`.
{ table: 'Function', indexName: 'function_fts', properties: FTS_PROPERTIES },
{ table: 'Class', indexName: 'class_fts', properties: FTS_PROPERTIES },
{ table: 'Protocol', indexName: 'protocol_fts', properties: FTS_PROPERTIES },
{ table: 'Category', indexName: 'category_fts', properties: FTS_PROPERTIES },
{ table: 'Method', indexName: 'method_fts', properties: FTS_PROPERTIES },
{ table: 'Interface', indexName: 'interface_fts', properties: FTS_PROPERTIES },
// Remaining EMBEDDABLE_LABELS symbol tables — all CODE_ELEMENT_BASE-shaped

View file

@ -96,6 +96,17 @@ const SOURCES: Record<string, GrammarSource> = {
unavailableNote:
'C++ parsing requires `tree-sitter-cpp`. Check the install and native binding.',
},
[SupportedLanguages.ObjectiveC]: {
load: () => requireVendoredGrammar('tree-sitter-objc'),
optional: true,
severity: 'error',
unavailableNote:
'Objective-C parsing disabled: vendored `tree-sitter-objc` (under ' +
'`gitnexus/vendor/tree-sitter-objc`) could not be loaded. GitNexus ships ' +
'prebuilt binaries for supported macOS/Linux runner architectures; this usually ' +
'indicates a corrupted install or native ABI mismatch with the bundled ' +
'tree-sitter@0.21.1 runtime.',
},
[SupportedLanguages.Go]: {
load: () => _require('tree-sitter-go'),
unavailableNote: 'Go parsing requires `tree-sitter-go`. Check the install and native binding.',

View file

@ -33,6 +33,7 @@ export const VENDORED_GRAMMAR_PACKAGES: ReadonlySet<string> = new Set([
'tree-sitter-proto',
'tree-sitter-swift',
'tree-sitter-kotlin',
'tree-sitter-objc',
]);
/** Absolute directory of a vendored grammar package under `vendor/`. */
@ -42,7 +43,7 @@ export const vendoredGrammarDir = (packageName: string): string =>
/**
* Load a vendored tree-sitter grammar by its absolute path under `vendor/`.
*
* GitNexus vendors five grammars (c/dart/proto/swift/kotlin) inside its own
* GitNexus vendors six grammars (c/dart/proto/swift/kotlin/objc) inside its own
* package under `vendor/`, each shipping committed per-platform prebuilds. They
* are deliberately NOT npm dependencies and must NEVER be copied into
* `node_modules`: an undeclared package under `node_modules` is "extraneous" to

View file

@ -323,6 +323,8 @@ export const VALID_NODE_LABELS = new Set([
'Folder',
'Function',
'Class',
'Protocol',
'Category',
'Interface',
'Method',
'CodeElement',
@ -4244,7 +4246,7 @@ export class LocalBackend {
repo.lbugPath,
`
MATCH (caller)-[r:CodeRelation]->(n {id: $symId})
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'USES', 'HAS_METHOD', 'HAS_PROPERTY', 'METHOD_OVERRIDES', 'OVERRIDES', 'METHOD_IMPLEMENTS', 'ACCESSES']
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'USES', 'DECLARES', 'HAS_METHOD', 'HAS_PROPERTY', 'METHOD_OVERRIDES', 'OVERRIDES', 'METHOD_IMPLEMENTS', 'ACCESSES']
RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
ORDER BY uid, relType
LIMIT 30
@ -4399,7 +4401,7 @@ export class LocalBackend {
repo.lbugPath,
`
MATCH (n {id: $symId})-[r:CodeRelation]->(target)
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'USES', 'HAS_METHOD', 'HAS_PROPERTY', 'METHOD_OVERRIDES', 'OVERRIDES', 'METHOD_IMPLEMENTS', 'ACCESSES']
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'USES', 'DECLARES', 'HAS_METHOD', 'HAS_PROPERTY', 'METHOD_OVERRIDES', 'OVERRIDES', 'METHOD_IMPLEMENTS', 'ACCESSES']
RETURN r.type AS relType, target.id AS uid, target.name AS name, target.filePath AS filePath, labels(target)[0] AS kind
ORDER BY uid, relType
LIMIT 30

View file

@ -0,0 +1,23 @@
#import <Foundation/Foundation.h>
int SYModuleSupportAdd(int a, int b);
@protocol SYModuleRunnable <NSObject>
- (void)runTask:(NSString *)name completion:(void (^)(BOOL ok))completion;
@end
@interface SYBaseCaller : NSObject
- (void)loadData:(NSString *)name completion:(void (^)(BOOL ok))completion;
@end
@interface SYModuleCaller : SYBaseCaller <SYModuleRunnable> {
SYBaseCaller *_base;
}
@property (nonatomic, strong) SYBaseCaller *helper;
+ (instancetype)sharedCaller;
- (void)runTask:(NSString *)name completion:(void (^)(BOOL ok))completion;
@end
@interface SYModuleCaller (Tracing)
- (void)traceEvent:(NSString *)name;
@end

View file

@ -0,0 +1,32 @@
#import "SYModuleCaller.h"
#include "SYModuleSupport.h"
@import Foundation;
#define SY_OBJC_RECEIVER(x) x
@interface SYModuleCaller ()
@property (nonatomic, strong) SYBaseCaller *privateHelper;
@end
@implementation SYModuleCaller
+ (instancetype)sharedCaller { return [SYModuleCaller new]; }
- (void)runTask:(NSString *)name completion:(void (^)(BOOL ok))completion {
SYBaseCaller *typed = self.helper;
id dynamic = typed;
[self traceEvent:name];
[SY_OBJC_RECEIVER(self) traceEvent:name];
[super loadData:name completion:completion];
[self loadData:name completion:completion];
[typed loadData:name completion:completion];
[dynamic loadData:name completion:completion];
}
- (void)runProtocol:(id<SYModuleRunnable>)runner {
[runner runTask:@"x" completion:^(BOOL ok) {}];
}
@end
@implementation SYModuleCaller (Tracing)
- (void)traceEvent:(NSString *)name {}
@end
static int SYModuleCompute(int value) { return value + 1; }

View file

@ -0,0 +1,4 @@
#ifndef SY_MODULE_SUPPORT_H
#define SY_MODULE_SUPPORT_H
int SYModuleSupportAdd(int a, int b);
#endif

View file

@ -151,6 +151,50 @@ describe('streamAllCSVsToDisk', () => {
expect(await readAllRelRows(result.relsByPair)).toHaveLength(3);
});
it('persists Protocol and Category nodes and their structural relationships', async () => {
const graph = buildTestGraph(
[
{
id: 'Protocol:objc:protocol:Runnable',
label: 'Protocol',
name: 'Runnable',
filePath: 'src/Runnable.h',
},
{
id: 'Class:objc:class:Worker',
label: 'Class',
name: 'Worker',
filePath: 'src/Worker.h',
},
{
id: 'Category:objc:category:Worker:Tracing',
label: 'Category',
name: 'Tracing',
filePath: 'src/Worker+Tracing.m',
},
],
[
{
sourceId: 'Class:objc:class:Worker',
targetId: 'Protocol:objc:protocol:Runnable',
type: 'IMPLEMENTS',
},
{
sourceId: 'Category:objc:category:Worker:Tracing',
targetId: 'Class:objc:class:Worker',
type: 'MEMBER_OF',
},
],
);
const result = await streamAllCSVsToDisk(graph, repoDir, csvDir);
expect(result.nodeFiles.get('Protocol')?.rows).toBe(1);
expect(result.nodeFiles.get('Category')?.rows).toBe(1);
expect(result.relsByPair.get('Class|Protocol')?.rows).toBe(1);
expect(result.relsByPair.get('Category|Class')?.rows).toBe(1);
});
it('CSV content is properly escaped', async () => {
const graph = buildTestGraph([
{

View file

@ -0,0 +1,523 @@
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",
);
function readFixture(name: string): string {
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_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",
).replace(
"- (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];",
).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",
);
function git(repoRoot: string, command: string): void {
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 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 normalizeRows(rows: unknown): unknown[] {
if (!Array.isArray(rows)) return [];
return rows
.map((row) => {
const record = row as Record<string, unknown>;
return {
uid: record.uid ?? record.id,
name: record.name,
filePath: record.filePath,
kind: record.kind,
};
})
.sort((left, right) =>
`${left.uid ?? ""}:${left.name ?? ""}:${left.filePath ?? ""}`.localeCompare(
`${right.uid ?? ""}:${right.name ?? ""}:${right.filePath ?? ""}`,
),
);
}
function normalizeBuckets(value: unknown): Record<string, unknown[]> {
const record = (value ?? {}) as Record<string, unknown>;
return Object.fromEntries(
Object.keys(record)
.sort()
.map((key) => [key, normalizeRows(record[key])]),
);
}
function normalizeContext(value: unknown): Record<string, unknown> {
const record = value as Record<string, unknown>;
const symbol = (record.symbol ?? {}) as Record<string, unknown>;
return {
status: record.status,
symbol: {
uid: symbol.uid,
name: symbol.name,
kind: symbol.kind,
filePath: symbol.filePath,
},
incoming: normalizeBuckets(record.incoming),
outgoing: normalizeBuckets(record.outgoing),
};
}
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",
repo: repoRoot,
});
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",
repo: repoRoot,
limit: 5,
include_content: false,
})) as Record<string, unknown>;
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",
repo: repoRoot,
});
const categoryHostResult = await backend.callTool("cypher", {
query:
"MATCH (category:Category)-[r:CodeRelation]->(host:Class) " +
"WHERE r.type = 'MEMBER_OF' " +
"RETURN category.id AS category, host.id AS host",
repo: repoRoot,
});
return {
classContext: normalizeContext(classContext),
runTaskContext: normalizeContext(runTaskContext),
queryDefinitions: normalizeRows(queryResult.definitions),
protocolAndCategoryResult,
categoryHostResult,
};
} finally {
await backend.disconnect();
}
}
async function analyzeObjectiveCRepo(
repoRoot: string,
options: { force?: boolean } = {},
): Promise<string[]> {
const logs: string[] = [];
await runFullAnalysis(
repoRoot,
{
force: options.force,
skipAgentsMd: true,
skipSkills: true,
workerPoolSize: 1,
},
{
onProgress: () => undefined,
onLog: (message) => logs.push(message),
},
);
return logs;
}
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);
result = await runPipelineFromRepo(repoRoot, () => undefined, {
workerPoolSize: 1,
});
}, 60000);
afterAll(() => {
fs.rmSync(repoRoot, { recursive: true, force: true });
});
function nodeByQualifiedName(qualifiedName: string): GraphNode | undefined {
return result.graph.nodes.find(
(node) => node.properties.qualifiedName === qualifiedName,
);
}
function expectNode(
qualifiedName: string,
label: GraphNode["label"],
): GraphNode {
const node = nodeByQualifiedName(qualifiedName);
expect(node, qualifiedName).toBeDefined();
expect(node?.label).toBe(label);
return node!;
}
function hasRelationship(
type: RelationshipType,
sourceId: string,
targetId: string,
reason?: string | RegExp,
): boolean {
return result.graph.relationships.some((rel) => {
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);
});
}
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("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);
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",
]),
);
const mFile = result.graph.nodes.find(
(node) =>
node.label === "File" &&
node.properties.filePath === "SYModuleCaller.m",
);
const hFile = result.graph.nodes.find(
(node) =>
node.label === "File" &&
node.properties.filePath === "SYModuleCaller.h",
);
expect(mFile).toBeDefined();
expect(hFile).toBeDefined();
expect(hasRelationship("IMPORTS", mFile!.id, hFile!.id)).toBe(true);
});
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",
);
const implementationEvidence = result.graph.nodes.filter(
(node) =>
node.label === "CodeElement" &&
node.properties.objectiveCKind === "implementation-evidence" &&
node.properties.filePath === "SYModuleCaller.m",
);
expect(
implementationEvidence.map((node) => node.properties.targetQualifiedName),
).toEqual(
expect.arrayContaining([
"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",
),
),
).toBe(true);
expect(
implementationEvidence.some((node) =>
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", () => {
const runTask = expectNode(
"objc:method:objc:class:SYModuleCaller:-:runTask:completion:",
"Method",
);
const loadData = expectNode(
"objc:method:objc:class:SYBaseCaller:-:loadData:completion:",
"Method",
);
const traceEvent = expectNode(
"objc:method:objc:category:SYModuleCaller:Tracing:-:traceEvent:",
"Method",
);
const runProtocol = expectNode(
"objc:method:objc:class:SYModuleCaller:-:runProtocol:",
"Method",
);
const protocolRun = expectNode(
"objc:method:objc:protocol:SYModuleRunnable:-:runTask:completion:",
"Method",
);
expect(
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",
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",
);
expect(unresolved).toBeDefined();
expect(
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",
);
const candidates = result.graph.nodes.find(
(node) =>
node.label === "CodeElement" &&
node.properties.objectiveCKind === "protocol-candidate-implementations",
);
expect(candidates?.properties.candidateImplementations).toEqual(
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-"),
);
try {
writeObjectiveCRepo(repoRoot);
git(repoRoot, "git init");
gitCommitAll(repoRoot, "initial Objective-C fixture");
await analyzeObjectiveCRepo(repoRoot);
const initialSurface = await readPersistedObjectiveCSurface(repoRoot);
expect(initialSurface).toMatchObject({
classContext: {
status: "found",
symbol: {
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:",
),
filePath: "SYModuleCaller.m",
}),
]),
imports: expect.arrayContaining([
expect.objectContaining({
uid: "File:SYModuleCaller.m",
filePath: "SYModuleCaller.m",
}),
]),
},
},
queryDefinitions: expect.arrayContaining([
expect.objectContaining({ uid: "Class:objc:class:SYModuleCaller" }),
expect.objectContaining({
uid: expect.stringMatching(
/^Method:objc:method:objc:class:SYModuleCaller:/,
),
}),
]),
protocolAndCategoryResult: expect.objectContaining({
markdown: expect.stringContaining(
"Protocol:objc:protocol:SYModuleRunnable",
),
}),
categoryHostResult: expect.objectContaining({
markdown: expect.stringContaining(
"Category:objc:category:SYModuleCaller:Tracing",
),
}),
});
writeObjectiveCRepo(repoRoot, HEADER_V2, IMPL_V2);
gitCommitAll(
repoRoot,
"change Objective-C declarations and implementations",
);
const incrementalLogs = await analyzeObjectiveCRepo(repoRoot);
expect(incrementalLogs).toContainEqual(
expect.stringContaining("Incremental: changed="),
);
const incrementalSurface = await readPersistedObjectiveCSurface(repoRoot);
await analyzeObjectiveCRepo(repoRoot, { force: true });
const forceSurface = await readPersistedObjectiveCSurface(repoRoot);
expect(incrementalSurface).toEqual(forceSurface);
expect(forceSurface).toMatchObject({
classContext: {
outgoing: {
has_method: expect.arrayContaining([
expect.objectContaining({
uid: "Method:objc:method:objc:category:SYModuleCaller:Tracing:-:traceDetail:level:",
}),
]),
},
},
runTaskContext: {
outgoing: {
calls: expect.arrayContaining([
expect.objectContaining({
uid: "Method:objc:method:objc:category:SYModuleCaller:Tracing:-:traceDetail:level:",
}),
]),
},
},
});
} finally {
fs.rmSync(repoRoot, { recursive: true, force: true });
}
}, 180000);
});

View file

@ -34,6 +34,7 @@ const CALLABLE_FLOW_PROVIDER_COVERAGE = {
[SupportedLanguages.Vue]: 'matrix',
[SupportedLanguages.Cobol]: 'matrix',
[SupportedLanguages.Zig]: 'matrix',
[SupportedLanguages.ObjectiveC]: 'dedicated',
} as const satisfies Record<SupportedLanguages, 'matrix' | 'dedicated'>;
const PROVIDER_FLOW_CASES = [

View file

@ -6,6 +6,13 @@ import {
type AnalysisFeatureDescriptor,
} from '../../src/core/analysis-features.js';
import { ANALYSIS_FEATURES } from '../../src/core/analysis-feature-registry.js';
import { OBJECTIVE_C_PROVIDER_FEATURE } from '../../src/core/ingestion/languages/objective-c/analysis-features.js';
import {
OBJECTIVE_C_GRAMMAR_PACKAGE,
OBJECTIVE_C_GRAMMAR_VERSION,
OBJECTIVE_C_PROVIDER_VERSION,
} from '../../src/core/ingestion/languages/objective-c/facts.js';
describe('analysis feature versions', () => {
it('separates the global Class schema capability from JVM-only Bean evidence', () => {
@ -88,6 +95,32 @@ describe('analysis feature versions', () => {
]);
});
it('stamps Objective-C provider and grammar versions for semantic rebuilds', () => {
const expectedId =
`objective-c.provider-${OBJECTIVE_C_PROVIDER_VERSION}.` +
`${OBJECTIVE_C_GRAMMAR_PACKAGE}-${OBJECTIVE_C_GRAMMAR_VERSION}`;
expect(OBJECTIVE_C_PROVIDER_FEATURE.id).toBe(expectedId);
const objcFeatures = resolveAnalysisFeatureVersions(ANALYSIS_FEATURES, [
'Sources/SYModuleCaller.m',
'Sources/SYModuleCaller.mm',
'Headers/SYModuleCaller.h',
]);
expect(objcFeatures).toMatchObject({
[OBJECTIVE_C_PROVIDER_FEATURE.id]: OBJECTIVE_C_PROVIDER_FEATURE.version,
});
expect(resolveAnalysisFeatureVersions(ANALYSIS_FEATURES, ['include/plain.hpp'])).not.toHaveProperty(
OBJECTIVE_C_PROVIDER_FEATURE.id,
);
expect(
findAnalysisFeatureMismatches(
{ [OBJECTIVE_C_PROVIDER_FEATURE.id]: OBJECTIVE_C_PROVIDER_FEATURE.version - 1 },
{ [OBJECTIVE_C_PROVIDER_FEATURE.id]: OBJECTIVE_C_PROVIDER_FEATURE.version },
),
).toEqual([`version:${OBJECTIVE_C_PROVIDER_FEATURE.id}`]);
});
it('rejects invalid or duplicate descriptors', () => {
const invalid: AnalysisFeatureDescriptor = {
id: 'invalid',

View file

@ -73,8 +73,8 @@ describe('COMPATIBLE_ABI gate', () => {
});
describe('GRAMMARS registry', () => {
it('covers all five grammars (swift/kotlin npm, dart/proto github, c npm)', () => {
expect(Object.keys(mod.GRAMMARS).sort()).toEqual(['c', 'dart', 'kotlin', 'proto', 'swift']);
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(mod.GRAMMARS.swift.npm).toBe('tree-sitter-swift');
expect(mod.GRAMMARS.dart.github).toContain('tree-sitter-dart');
});
@ -82,6 +82,8 @@ describe('GRAMMARS registry', () => {
it('marks c and kotlin report-only (holds); swift/dart/proto are auto-updatable', () => {
expect(mod.GRAMMARS.c.npm).toBe('tree-sitter-c');
expect(mod.GRAMMARS.c.hold).toBeTruthy(); // ABI-pinned: detected/reported, never auto-applied
expect(mod.GRAMMARS.objc.npm).toBe('tree-sitter-objc');
expect(mod.GRAMMARS.objc.hold).toBeTruthy();
// kotlin is pinned to an unreleased fwcd main commit for `fun interface`
// support (#169); npm latest (0.3.8) lacks it, so the strict-inequality
// isNewer would auto-revert the pin without this hold.

View file

@ -78,9 +78,11 @@ describe('NodeLabel taxonomy coverage', () => {
expect(CALLABLE_ONLY_LABELS.has('Delegate')).toBe(true);
});
it('DISPATCH_LABELS includes all 10 routed kinds', () => {
it('DISPATCH_LABELS includes all 12 routed kinds', () => {
const expected = [
'Class',
'Protocol',
'Category',
'Struct',
'Interface',
'Enum',

View file

@ -0,0 +1,41 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { SupportedLanguages } from '../../src/config/supported-languages.js';
describe('Objective-C parser-loader failure path', () => {
afterEach(() => {
vi.resetModules();
vi.doUnmock('../../src/core/logger.js');
vi.doUnmock('../../src/core/tree-sitter/vendored-grammars.js');
});
it('reports a clean unavailable Objective-C grammar with an actionable diagnostic', async () => {
const errorLog = vi.fn();
const warnLog = vi.fn();
vi.doMock('../../src/core/logger.js', () => ({
logger: {
error: errorLog,
warn: warnLog,
},
}));
vi.doMock('../../src/core/tree-sitter/vendored-grammars.js', () => ({
requireVendoredGrammar: (name: string) => {
if (name === 'tree-sitter-objc') throw new Error('synthetic missing objc grammar');
return {};
},
}));
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);
expect(() => getLanguageGrammar(SupportedLanguages.ObjectiveC)).toThrow(
/Unsupported language: objective-c/,
);
expect(warnLog).not.toHaveBeenCalled();
expect(String(errorLog.mock.calls[0]?.[0] ?? '')).toMatch(
/Objective-C parsing disabled[\s\S]*tree-sitter-objc[\s\S]*synthetic missing objc grammar/,
);
});
});

View file

@ -0,0 +1,355 @@
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";
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";
const FIXTURE = `#import "SYModuleCaller.h"
#include "SYModuleSupport.h"
@import Foundation;
@protocol SYModuleRunnable <NSObject>
- (void)runTask:(NSString *)name completion:(void (^)(BOOL ok))completion;
@end
@interface SYBaseCaller : NSObject
- (void)loadData:(NSString *)name completion:(void (^)(BOOL ok))completion;
@end
@interface SYModuleCaller : SYBaseCaller <SYModuleRunnable> {
SYBaseCaller *_base;
}
@property (nonatomic, strong) SYBaseCaller *helper;
+ (instancetype)sharedCaller;
- (void)runTask:(NSString *)name completion:(void (^)(BOOL ok))completion;
@end
@interface SYModuleCaller ()
@property (nonatomic, strong) SYBaseCaller *privateHelper;
@end
@interface SYModuleCaller (Tracing)
- (void)traceEvent:(NSString *)name;
@end
@implementation SYModuleCaller
+ (instancetype)sharedCaller { return [SYModuleCaller new]; }
- (void)runTask:(NSString *)name completion:(void (^)(BOOL ok))completion {
SYBaseCaller *typed = self.helper;
id dynamic = typed;
[self traceEvent:name];
[super loadData:name completion:completion];
[typed loadData:name completion:completion];
[dynamic loadData:name completion:completion];
}
- (void)runProtocol:(id<SYModuleRunnable>)runner {
[runner runTask:@"x" completion:^(BOOL ok) {}];
}
@end
@implementation SYModuleCaller (Tracing)
- (void)traceEvent:(NSString *)name {}
@end
static int SYModuleCompute(int value) { return value + 1; }
`;
function parseFixture() {
const parser = new Parser();
parser.setLanguage(requireVendoredGrammar("tree-sitter-objc"));
return parser.parse(FIXTURE);
}
function parseSource(source: string) {
const parser = new Parser();
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", () => {
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",
);
});
it("classifies Objective-C headers by content without stealing plain C headers", () => {
expect(
classifyObjectiveCFileContent(
"SYModuleCaller.h",
"@interface SYModuleCaller : NSObject\n@end",
),
).toBe(true);
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",
),
).toBe(SupportedLanguages.CPlusPlus);
expect(
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,
);
});
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",
);
expect(facts.functions.map((fn) => fn.name)).toEqual(
expect.arrayContaining(["add", "returnsPointer"]),
);
expect(facts.functions.map((fn) => fn.name)).not.toContain("callback");
});
it("does not treat protocol-qualified parameter types as conformance", () => {
const facts = collectObjectiveCFacts(
parseSource(`
@protocol P <NSObject>
- (void)run:(id<Q>)value;
@end
@interface Child : Base <P>
- (void)run:(id<Q>)value;
@end
`),
"protocols.h",
);
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", () => {
const facts = collectObjectiveCFacts(
parseSource(`
#define RECEIVER_MACRO(x) x
@interface A
+ (void)run;
@end
@interface Base
- (void)ping;
@end
@interface Child : Base
- (void)call;
@end
@implementation Child
- (void)call {
[A run];
[self ping];
[RECEIVER_MACRO(self) ping];
}
@end
`),
"receivers.m",
);
expect(
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",
}),
]),
);
});
it("resolves extensionless local imports to Objective-C source/header files", () => {
expect(
objectiveCScopeResolver.resolveImportTarget(
"./NestedHeader",
"src/Caller.m",
new Set(["src/NestedHeader.h"]),
),
).toBe("src/NestedHeader.h");
expect(
objectiveCScopeResolver.resolveImportTarget(
"./NestedImpl",
"src/Caller.m",
new Set(["src/NestedImpl.mm"]),
),
).toBe("src/NestedImpl.mm");
expect(
objectiveCScopeResolver.resolveImportTarget(
"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");
expect(facts.containers.map((c) => `${c.kind}:${c.name}`)).toEqual(
expect.arrayContaining([
"protocol:SYModuleRunnable",
"class:SYBaseCaller",
"class:SYModuleCaller",
"extension:SYModuleCaller ()",
"category:SYModuleCaller (Tracing)",
]),
);
expect(
facts.containers.find(
(c) => c.name === "SYModuleCaller" && c.kind === "class",
),
).toMatchObject({
superclass: "SYBaseCaller",
protocols: ["SYModuleRunnable"],
});
expect(
facts.methods.map((m) => ({
kind: m.methodKind,
selector: m.selector,
owner: m.ownerQualifiedName,
})),
).toEqual(
expect.arrayContaining([
{
kind: "-",
selector: "runTask:completion:",
owner: objcClassQualifiedName("SYModuleCaller"),
},
{
kind: "+",
selector: "sharedCaller",
owner: objcClassQualifiedName("SYModuleCaller"),
},
{
kind: "-",
selector: "traceEvent:",
owner: objcCategoryQualifiedName("SYModuleCaller", "Tracing"),
},
{
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.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",
]),
);
expect(
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:",
]),
);
expect(facts.unresolvedMessages).toEqual(
expect.arrayContaining([
expect.objectContaining({
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");
const graph = buildObjectiveCSemanticGraph(facts);
const methodIds = new Set(
graph.nodes
.filter((node) => node.label === "Method")
.map((node) => node.id),
);
expect(methodIds).toContain(
`Method:${objcMethodQualifiedName(objcClassQualifiedName("SYModuleCaller"), "-", "runTask:completion:")}`,
);
expect(methodIds).toContain(
`Method:${objcMethodQualifiedName(objcClassQualifiedName("SYModuleCaller"), "+", "sharedCaller")}`,
);
expect(methodIds).toContain(
`Method:${objcMethodQualifiedName(
objcCategoryQualifiedName("SYModuleCaller", "Tracing"),
"-",
"traceEvent:",
)}`,
);
expect(methodIds.size).toBeGreaterThan(4);
});
});

View file

@ -73,6 +73,11 @@ const SMOKE_CASES: Record<string, SmokeCase> = {
snippet: 'int main() { return 0; }\n',
rootType: 'translation_unit',
},
[SupportedLanguages.ObjectiveC]: {
language: SupportedLanguages.ObjectiveC,
snippet: '@interface ObjcSmoke\n- (void)run;\n@end\n@implementation ObjcSmoke\n- (void)run {}\n@end\n',
rootType: 'translation_unit',
},
[SupportedLanguages.Go]: {
language: SupportedLanguages.Go,
snippet: 'package main\nfunc main() {}\n',
@ -140,6 +145,10 @@ describe('parser-loader ABI load-smoke (#1922)', () => {
expect(sources.some((s) => s.key === SupportedLanguages.Swift)).toBe(true);
});
it('includes Objective-C in the smoke matrix', () => {
expect(sources.some((s) => s.key === SupportedLanguages.ObjectiveC)).toBe(true);
});
for (const { key, optional } of sources) {
const testCase = SMOKE_CASES[key];
if (!testCase) continue; // covered by the "every entry" assertion above

View file

@ -75,8 +75,8 @@ describe('LadybugDB Schema', () => {
});
it('has expected total count', () => {
// 9 core + 19 multi-language + Route + Tool + Destination + BasicBlock = 33
expect(NODE_TABLES).toHaveLength(33);
// 9 core + 21 multi-language + Route + Tool + Destination + BasicBlock = 35
expect(NODE_TABLES).toHaveLength(35);
});
});
@ -308,8 +308,8 @@ describe('LadybugDB Schema', () => {
describe('schema query ordering', () => {
it('NODE_SCHEMA_QUERIES has correct count', () => {
// 31 + Destination + BasicBlock = 33
expect(NODE_SCHEMA_QUERIES).toHaveLength(33);
// 33 + Protocol + Category = 35
expect(NODE_SCHEMA_QUERIES).toHaveLength(35);
});
it('REL_SCHEMA_QUERIES has one relation table', () => {
@ -317,8 +317,8 @@ describe('LadybugDB Schema', () => {
});
it('SCHEMA_QUERIES includes all node + rel + embedding schemas', () => {
// 33 node + 1 rel + 1 embedding = 35
expect(SCHEMA_QUERIES).toHaveLength(35);
// 35 node + 1 rel + 1 embedding = 37
expect(SCHEMA_QUERIES).toHaveLength(37);
});
it('node schemas come before relation schemas in SCHEMA_QUERIES', () => {

View file

@ -378,6 +378,17 @@ const CASES: ReadonlyMap<SupportedLanguages, ConformanceCase> = new Map([
reachesDecoy: 'std.zig',
},
],
[
SupportedLanguages.ObjectiveC,
{
files: ['Headers/Foundation.h', 'Headers/Widget.h', 'Sources/main.m'],
fromFile: 'Sources/main.m',
resolutionConfig: undefined,
external: 'Foundation',
decoy: 'Headers/Foundation.h',
reachesDecoy: 'Foundation.h',
},
],
]);
/**

View file

@ -547,6 +547,19 @@ const FIXTURES: ReadonlyMap<SupportedLanguages, ImportTargetFixture> = new Map<
minimumParsedFileReads: 0,
},
],
[
SupportedLanguages.ObjectiveC,
{
files: ['Headers/Widget.h', 'Sources/main.m'],
fromFile: 'Sources/main.m',
resolutionConfig: undefined,
missTarget: (i) => `ghost${i}.h`,
hitTarget: 'Widget.h',
parsedImport: IGNORES_CONTEXT,
minimumScans: 1,
minimumParsedFileReads: 0,
},
],
]);
/**

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2023 Amaan Qureshi <amaanq12@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,20 @@
# tree-sitter-objc
[![CI][ci]](https://github.com/tree-sitter-grammars/tree-sitter-objc/actions/workflows/ci.yml)
[![discord][discord]](https://discord.gg/w7nTvsVJhm)
[![matrix][matrix]](https://matrix.to/#/#tree-sitter-chat:matrix.org)
[![crates][crates]](https://crates.io/crates/tree-sitter-objc)
[![npm][npm]](https://www.npmjs.com/package/tree-sitter-objc)
[![pypi][pypi]](https://pypi.org/project/tree-sitter-objc)
[Objective C](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjectiveC/Introduction/introObjectiveC.html)
grammar for [tree-sitter](https://tree-sitter.github.io)
[ci]: https://img.shields.io/github/actions/workflow/status/tree-sitter-grammars/tree-sitter-objc/ci.yml?logo=github&label=CI
[discord]: https://img.shields.io/discord/1063097320771698699?logo=discord&label=discord
[matrix]: https://img.shields.io/matrix/tree-sitter-chat%3Amatrix.org?logo=matrix&label=matrix
[npm]: https://img.shields.io/npm/v/tree-sitter-objc?logo=npm
[crates]: https://img.shields.io/crates/v/tree-sitter-objc?logo=rust
[pypi]: https://img.shields.io/pypi/v/tree-sitter-objc?logo=pypi&logoColor=ffd242

View file

@ -0,0 +1,35 @@
{
"targets": [
{
"target_name": "tree_sitter_objc_binding",
"dependencies": [
"<!(node -p \"require('node-addon-api').targets\"):node_addon_api_except",
],
"include_dirs": [
"src",
],
"sources": [
"bindings/node/binding.cc",
"src/parser.c",
],
"variables": {
"has_scanner": "<!(node -p \"fs.existsSync('src/scanner.c')\")"
},
"conditions": [
["has_scanner=='true'", {
"sources+": ["src/scanner.c"],
}],
["OS!='win'", {
"cflags_c": [
"-std=c11",
],
}, { # OS == "win"
"cflags_c": [
"/std:c11",
"/utf-8",
],
}],
],
}
]
}

View file

@ -0,0 +1,19 @@
#include <napi.h>
typedef struct TSLanguage TSLanguage;
extern "C" TSLanguage *tree_sitter_objc();
// "tree-sitter", "language" hashed with BLAKE2
const napi_type_tag LANGUAGE_TYPE_TAG = {
0x8AF2E5212AD58ABF, 0xD5006CAD83ABBA16
};
Napi::Object Init(Napi::Env env, Napi::Object exports) {
auto language = Napi::External<TSLanguage>::New(env, tree_sitter_objc());
language.TypeTag(&LANGUAGE_TYPE_TAG);
exports["language"] = language;
return exports;
}
NODE_API_MODULE(tree_sitter_objc_binding, Init)

View file

@ -0,0 +1,9 @@
const assert = require("node:assert");
const { test } = require("node:test");
const Parser = require("tree-sitter");
test("can load grammar", () => {
const parser = new Parser();
assert.doesNotThrow(() => parser.setLanguage(require(".")));
});

View file

@ -0,0 +1,27 @@
type BaseNode = {
type: string;
named: boolean;
};
type ChildNode = {
multiple: boolean;
required: boolean;
types: BaseNode[];
};
type NodeInfo =
| (BaseNode & {
subtypes: BaseNode[];
})
| (BaseNode & {
fields: { [name: string]: ChildNode };
children: ChildNode[];
});
type Language = {
language: unknown;
nodeTypeInfo: NodeInfo[];
};
declare const language: Language;
export = language;

View file

@ -0,0 +1,11 @@
const root = require("path").join(__dirname, "..", "..");
module.exports =
typeof process.versions.bun === "string"
// Support `bun build --compile` by being statically analyzable enough to find the .node file at build-time
? require(`../../prebuilds/${process.platform}-${process.arch}/tree-sitter-objc.node`)
: require("node-gyp-build")(root);
try {
module.exports.nodeTypeInfo = require("../../src/node-types.json");
} catch (_) {}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,56 @@
{
"name": "tree-sitter-objc",
"version": "3.0.2",
"description": "Objective-C grammar for tree-sitter",
"repository": "https://github.com/tree-sitter-grammars/tree-sitter-objc",
"license": "MIT",
"author": {
"name": "Amaan Qureshi",
"email": "amaanq12@gmail.com"
},
"main": "bindings/node",
"types": "bindings/node",
"keywords": [
"incremental",
"parsing",
"tree-sitter",
"objective-c",
"objc"
],
"files": [
"grammar.js",
"tree-sitter.json",
"binding.gyp",
"prebuilds/**",
"bindings/node/*",
"queries/*",
"src/**",
"*.wasm"
],
"dependencies": {
"node-addon-api": "^8.3.0",
"node-gyp-build": "^4.8.4",
"tree-sitter-c": "^0.23.4"
},
"devDependencies": {
"eslint": "^9.17.0",
"eslint-config-treesitter": "^1.0.2",
"prebuildify": "^6.0.1",
"tree-sitter-cli": "^0.24.5"
},
"peerDependencies": {
"tree-sitter": "^0.22.1"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
},
"scripts": {
"install": "node-gyp-build",
"lint": "eslint grammar.js",
"prestart": "tree-sitter build --wasm",
"start": "tree-sitter playground",
"test": "node --test bindings/node/*_test.js"
}
}

View file

@ -0,0 +1,20 @@
; inherits: c
[
(class_declaration)
(class_interface)
(class_implementation)
(protocol_declaration)
(property_declaration)
(method_declaration)
(struct_declaration)
(struct_declarator)
(try_statement)
(catch_clause)
(finally_clause)
(throw_statement)
(block_literal)
(ms_asm_block)
(dictionary_literal)
(array_literal)
] @fold

View file

@ -0,0 +1,216 @@
; inherits: c
; Preprocs
(preproc_undef
name: (_) @constant) @preproc
; Includes
(module_import "@import" @include path: (identifier) @namespace)
((preproc_include
_ @include path: (_))
(#any-of? @include "#include" "#import"))
; Type Qualifiers
[
"@optional"
"@required"
"__covariant"
"__contravariant"
(visibility_specification)
] @type.qualifier
; Storageclasses
[
"@autoreleasepool"
"@synthesize"
"@dynamic"
"volatile"
(protocol_qualifier)
] @storageclass
; Keywords
[
"@protocol"
"@interface"
"@implementation"
"@compatibility_alias"
"@property"
"@selector"
"@defs"
"availability"
"@end"
] @keyword
(class_declaration "@" @keyword "class" @keyword) ; I hate Obj-C for allowing "@ class" :)
(method_definition ["+" "-"] @keyword.function)
(method_declaration ["+" "-"] @keyword.function)
[
"__typeof__"
"__typeof"
"typeof"
"in"
] @keyword.operator
[
"@synchronized"
"oneway"
] @keyword.coroutine
; Exceptions
[
"@try"
"__try"
"@catch"
"__catch"
"@finally"
"__finally"
"@throw"
] @exception
; Variables
((identifier) @variable.builtin
(#any-of? @variable.builtin "self" "super"))
; Functions & Methods
[
"objc_bridge_related"
"@available"
"__builtin_available"
"va_arg"
"asm"
] @function.builtin
(method_definition (identifier) @method)
(method_declaration (identifier) @method)
(method_identifier (identifier)? @method ":" @method (identifier)? @method)
(message_expression method: (identifier) @method.call)
; Constructors
((message_expression method: (identifier) @constructor)
(#eq? @constructor "init"))
; Attributes
(availability_attribute_specifier
[
"CF_FORMAT_FUNCTION" "NS_AVAILABLE" "__IOS_AVAILABLE" "NS_AVAILABLE_IOS"
"API_AVAILABLE" "API_UNAVAILABLE" "API_DEPRECATED" "NS_ENUM_AVAILABLE_IOS"
"NS_DEPRECATED_IOS" "NS_ENUM_DEPRECATED_IOS" "NS_FORMAT_FUNCTION" "DEPRECATED_MSG_ATTRIBUTE"
"__deprecated_msg" "__deprecated_enum_msg" "NS_SWIFT_NAME" "NS_SWIFT_UNAVAILABLE"
"NS_EXTENSION_UNAVAILABLE_IOS" "NS_CLASS_AVAILABLE_IOS" "NS_CLASS_DEPRECATED_IOS" "__OSX_AVAILABLE_STARTING"
"NS_ROOT_CLASS" "NS_UNAVAILABLE" "NS_REQUIRES_NIL_TERMINATION" "CF_RETURNS_RETAINED"
"CF_RETURNS_NOT_RETAINED" "DEPRECATED_ATTRIBUTE" "UI_APPEARANCE_SELECTOR" "UNAVAILABLE_ATTRIBUTE"
]) @attribute
; Macros
(type_qualifier
[
"_Complex"
"_Nonnull"
"_Nullable"
"_Nullable_result"
"_Null_unspecified"
"__autoreleasing"
"__block"
"__bridge"
"__bridge_retained"
"__bridge_transfer"
"__complex"
"__kindof"
"__nonnull"
"__nullable"
"__ptrauth_objc_class_ro"
"__ptrauth_objc_isa_pointer"
"__ptrauth_objc_super_pointer"
"__strong"
"__thread"
"__unsafe_unretained"
"__unused"
"__weak"
]) @function.macro.builtin
[ "__real" "__imag" ] @function.macro.builtin
((call_expression function: (identifier) @function.macro)
(#eq? @function.macro "testassert"))
; Types
(class_declaration (identifier) @type)
(class_interface "@interface" . (identifier) @type superclass: _? @type category: _? @namespace)
(class_implementation "@implementation" . (identifier) @type superclass: _? @type category: _? @namespace)
(protocol_forward_declaration (identifier) @type) ; @interface :(
(protocol_reference_list (identifier) @type) ; ^
[
"BOOL"
"IMP"
"SEL"
"Class"
"id"
] @type.builtin
; Constants
(property_attribute (identifier) @constant "="?)
[ "__asm" "__asm__" ] @constant.macro
; Properties
(property_implementation "@synthesize" (identifier) @property)
((identifier) @property
(#has-ancestor? @property struct_declaration))
; Parameters
(method_parameter ":" @method (identifier) @parameter)
(method_parameter declarator: (identifier) @parameter)
(parameter_declaration
declarator: (function_declarator
declarator: (parenthesized_declarator
(block_pointer_declarator
declarator: (identifier) @parameter))))
"..." @parameter.builtin
; Operators
[
"^"
] @operator
; Literals
(platform) @string.special
(version_number) @text.uri @number
; Punctuation
"@" @punctuation.special
[ "<" ">" ] @punctuation.bracket

View file

@ -0,0 +1 @@
; inherits: c

View file

@ -0,0 +1,10 @@
; inherits: c
; TODO(amaanq): uncomment/add when I add asm support
; (ms_asm_block "{" _ @asm "}")
;
; ((asm_specifier (string_literal) @asm)
; (#offset! @asm 0 1 0 -1))
;
; ((asm_statement (string_literal) @asm)
; (#offset! @asm 0 1 0 -1))

View file

@ -0,0 +1 @@
; inherits: c

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

682692
gitnexus/vendor/tree-sitter-objc/src/parser.c vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,54 @@
#ifndef TREE_SITTER_ALLOC_H_
#define TREE_SITTER_ALLOC_H_
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
// Allow clients to override allocation functions
#ifdef TREE_SITTER_REUSE_ALLOCATOR
extern void *(*ts_current_malloc)(size_t size);
extern void *(*ts_current_calloc)(size_t count, size_t size);
extern void *(*ts_current_realloc)(void *ptr, size_t size);
extern void (*ts_current_free)(void *ptr);
#ifndef ts_malloc
#define ts_malloc ts_current_malloc
#endif
#ifndef ts_calloc
#define ts_calloc ts_current_calloc
#endif
#ifndef ts_realloc
#define ts_realloc ts_current_realloc
#endif
#ifndef ts_free
#define ts_free ts_current_free
#endif
#else
#ifndef ts_malloc
#define ts_malloc malloc
#endif
#ifndef ts_calloc
#define ts_calloc calloc
#endif
#ifndef ts_realloc
#define ts_realloc realloc
#endif
#ifndef ts_free
#define ts_free free
#endif
#endif
#ifdef __cplusplus
}
#endif
#endif // TREE_SITTER_ALLOC_H_

View file

@ -0,0 +1,291 @@
#ifndef TREE_SITTER_ARRAY_H_
#define TREE_SITTER_ARRAY_H_
#ifdef __cplusplus
extern "C" {
#endif
#include "./alloc.h"
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4101)
#elif defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-variable"
#endif
#define Array(T) \
struct { \
T *contents; \
uint32_t size; \
uint32_t capacity; \
}
/// Initialize an array.
#define array_init(self) \
((self)->size = 0, (self)->capacity = 0, (self)->contents = NULL)
/// Create an empty array.
#define array_new() \
{ NULL, 0, 0 }
/// Get a pointer to the element at a given `index` in the array.
#define array_get(self, _index) \
(assert((uint32_t)(_index) < (self)->size), &(self)->contents[_index])
/// Get a pointer to the first element in the array.
#define array_front(self) array_get(self, 0)
/// Get a pointer to the last element in the array.
#define array_back(self) array_get(self, (self)->size - 1)
/// Clear the array, setting its size to zero. Note that this does not free any
/// memory allocated for the array's contents.
#define array_clear(self) ((self)->size = 0)
/// Reserve `new_capacity` elements of space in the array. If `new_capacity` is
/// less than the array's current capacity, this function has no effect.
#define array_reserve(self, new_capacity) \
_array__reserve((Array *)(self), array_elem_size(self), new_capacity)
/// Free any memory allocated for this array. Note that this does not free any
/// memory allocated for the array's contents.
#define array_delete(self) _array__delete((Array *)(self))
/// Push a new `element` onto the end of the array.
#define array_push(self, element) \
(_array__grow((Array *)(self), 1, array_elem_size(self)), \
(self)->contents[(self)->size++] = (element))
/// Increase the array's size by `count` elements.
/// New elements are zero-initialized.
#define array_grow_by(self, count) \
do { \
if ((count) == 0) break; \
_array__grow((Array *)(self), count, array_elem_size(self)); \
memset((self)->contents + (self)->size, 0, (count) * array_elem_size(self)); \
(self)->size += (count); \
} while (0)
/// Append all elements from one array to the end of another.
#define array_push_all(self, other) \
array_extend((self), (other)->size, (other)->contents)
/// Append `count` elements to the end of the array, reading their values from the
/// `contents` pointer.
#define array_extend(self, count, contents) \
_array__splice( \
(Array *)(self), array_elem_size(self), (self)->size, \
0, count, contents \
)
/// Remove `old_count` elements from the array starting at the given `index`. At
/// the same index, insert `new_count` new elements, reading their values from the
/// `new_contents` pointer.
#define array_splice(self, _index, old_count, new_count, new_contents) \
_array__splice( \
(Array *)(self), array_elem_size(self), _index, \
old_count, new_count, new_contents \
)
/// Insert one `element` into the array at the given `index`.
#define array_insert(self, _index, element) \
_array__splice((Array *)(self), array_elem_size(self), _index, 0, 1, &(element))
/// Remove one element from the array at the given `index`.
#define array_erase(self, _index) \
_array__erase((Array *)(self), array_elem_size(self), _index)
/// Pop the last element off the array, returning the element by value.
#define array_pop(self) ((self)->contents[--(self)->size])
/// Assign the contents of one array to another, reallocating if necessary.
#define array_assign(self, other) \
_array__assign((Array *)(self), (const Array *)(other), array_elem_size(self))
/// Swap one array with another
#define array_swap(self, other) \
_array__swap((Array *)(self), (Array *)(other))
/// Get the size of the array contents
#define array_elem_size(self) (sizeof *(self)->contents)
/// Search a sorted array for a given `needle` value, using the given `compare`
/// callback to determine the order.
///
/// If an existing element is found to be equal to `needle`, then the `index`
/// out-parameter is set to the existing value's index, and the `exists`
/// out-parameter is set to true. Otherwise, `index` is set to an index where
/// `needle` should be inserted in order to preserve the sorting, and `exists`
/// is set to false.
#define array_search_sorted_with(self, compare, needle, _index, _exists) \
_array__search_sorted(self, 0, compare, , needle, _index, _exists)
/// Search a sorted array for a given `needle` value, using integer comparisons
/// of a given struct field (specified with a leading dot) to determine the order.
///
/// See also `array_search_sorted_with`.
#define array_search_sorted_by(self, field, needle, _index, _exists) \
_array__search_sorted(self, 0, _compare_int, field, needle, _index, _exists)
/// Insert a given `value` into a sorted array, using the given `compare`
/// callback to determine the order.
#define array_insert_sorted_with(self, compare, value) \
do { \
unsigned _index, _exists; \
array_search_sorted_with(self, compare, &(value), &_index, &_exists); \
if (!_exists) array_insert(self, _index, value); \
} while (0)
/// Insert a given `value` into a sorted array, using integer comparisons of
/// a given struct field (specified with a leading dot) to determine the order.
///
/// See also `array_search_sorted_by`.
#define array_insert_sorted_by(self, field, value) \
do { \
unsigned _index, _exists; \
array_search_sorted_by(self, field, (value) field, &_index, &_exists); \
if (!_exists) array_insert(self, _index, value); \
} while (0)
// Private
typedef Array(void) Array;
/// This is not what you're looking for, see `array_delete`.
static inline void _array__delete(Array *self) {
if (self->contents) {
ts_free(self->contents);
self->contents = NULL;
self->size = 0;
self->capacity = 0;
}
}
/// This is not what you're looking for, see `array_erase`.
static inline void _array__erase(Array *self, size_t element_size,
uint32_t index) {
assert(index < self->size);
char *contents = (char *)self->contents;
memmove(contents + index * element_size, contents + (index + 1) * element_size,
(self->size - index - 1) * element_size);
self->size--;
}
/// This is not what you're looking for, see `array_reserve`.
static inline void _array__reserve(Array *self, size_t element_size, uint32_t new_capacity) {
if (new_capacity > self->capacity) {
if (self->contents) {
self->contents = ts_realloc(self->contents, new_capacity * element_size);
} else {
self->contents = ts_malloc(new_capacity * element_size);
}
self->capacity = new_capacity;
}
}
/// This is not what you're looking for, see `array_assign`.
static inline void _array__assign(Array *self, const Array *other, size_t element_size) {
_array__reserve(self, element_size, other->size);
self->size = other->size;
memcpy(self->contents, other->contents, self->size * element_size);
}
/// This is not what you're looking for, see `array_swap`.
static inline void _array__swap(Array *self, Array *other) {
Array swap = *other;
*other = *self;
*self = swap;
}
/// This is not what you're looking for, see `array_push` or `array_grow_by`.
static inline void _array__grow(Array *self, uint32_t count, size_t element_size) {
uint32_t new_size = self->size + count;
if (new_size > self->capacity) {
uint32_t new_capacity = self->capacity * 2;
if (new_capacity < 8) new_capacity = 8;
if (new_capacity < new_size) new_capacity = new_size;
_array__reserve(self, element_size, new_capacity);
}
}
/// This is not what you're looking for, see `array_splice`.
static inline void _array__splice(Array *self, size_t element_size,
uint32_t index, uint32_t old_count,
uint32_t new_count, const void *elements) {
uint32_t new_size = self->size + new_count - old_count;
uint32_t old_end = index + old_count;
uint32_t new_end = index + new_count;
assert(old_end <= self->size);
_array__reserve(self, element_size, new_size);
char *contents = (char *)self->contents;
if (self->size > old_end) {
memmove(
contents + new_end * element_size,
contents + old_end * element_size,
(self->size - old_end) * element_size
);
}
if (new_count > 0) {
if (elements) {
memcpy(
(contents + index * element_size),
elements,
new_count * element_size
);
} else {
memset(
(contents + index * element_size),
0,
new_count * element_size
);
}
}
self->size += new_count - old_count;
}
/// A binary search routine, based on Rust's `std::slice::binary_search_by`.
/// This is not what you're looking for, see `array_search_sorted_with` or `array_search_sorted_by`.
#define _array__search_sorted(self, start, compare, suffix, needle, _index, _exists) \
do { \
*(_index) = start; \
*(_exists) = false; \
uint32_t size = (self)->size - *(_index); \
if (size == 0) break; \
int comparison; \
while (size > 1) { \
uint32_t half_size = size / 2; \
uint32_t mid_index = *(_index) + half_size; \
comparison = compare(&((self)->contents[mid_index] suffix), (needle)); \
if (comparison <= 0) *(_index) = mid_index; \
size -= half_size; \
} \
comparison = compare(&((self)->contents[*(_index)] suffix), (needle)); \
if (comparison == 0) *(_exists) = true; \
else if (comparison < 0) *(_index) += 1; \
} while (0)
/// Helper macro for the `_sorted_by` routines below. This takes the left (existing)
/// parameter by reference in order to work with the generic sorting function above.
#define _compare_int(a, b) ((int)*(a) - (int)(b))
#ifdef _MSC_VER
#pragma warning(pop)
#elif defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic pop
#endif
#ifdef __cplusplus
}
#endif
#endif // TREE_SITTER_ARRAY_H_

View file

@ -0,0 +1,266 @@
#ifndef TREE_SITTER_PARSER_H_
#define TREE_SITTER_PARSER_H_
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#define ts_builtin_sym_error ((TSSymbol)-1)
#define ts_builtin_sym_end 0
#define TREE_SITTER_SERIALIZATION_BUFFER_SIZE 1024
#ifndef TREE_SITTER_API_H_
typedef uint16_t TSStateId;
typedef uint16_t TSSymbol;
typedef uint16_t TSFieldId;
typedef struct TSLanguage TSLanguage;
#endif
typedef struct {
TSFieldId field_id;
uint8_t child_index;
bool inherited;
} TSFieldMapEntry;
typedef struct {
uint16_t index;
uint16_t length;
} TSFieldMapSlice;
typedef struct {
bool visible;
bool named;
bool supertype;
} TSSymbolMetadata;
typedef struct TSLexer TSLexer;
struct TSLexer {
int32_t lookahead;
TSSymbol result_symbol;
void (*advance)(TSLexer *, bool);
void (*mark_end)(TSLexer *);
uint32_t (*get_column)(TSLexer *);
bool (*is_at_included_range_start)(const TSLexer *);
bool (*eof)(const TSLexer *);
void (*log)(const TSLexer *, const char *, ...);
};
typedef enum {
TSParseActionTypeShift,
TSParseActionTypeReduce,
TSParseActionTypeAccept,
TSParseActionTypeRecover,
} TSParseActionType;
typedef union {
struct {
uint8_t type;
TSStateId state;
bool extra;
bool repetition;
} shift;
struct {
uint8_t type;
uint8_t child_count;
TSSymbol symbol;
int16_t dynamic_precedence;
uint16_t production_id;
} reduce;
uint8_t type;
} TSParseAction;
typedef struct {
uint16_t lex_state;
uint16_t external_lex_state;
} TSLexMode;
typedef union {
TSParseAction action;
struct {
uint8_t count;
bool reusable;
} entry;
} TSParseActionEntry;
typedef struct {
int32_t start;
int32_t end;
} TSCharacterRange;
struct TSLanguage {
uint32_t version;
uint32_t symbol_count;
uint32_t alias_count;
uint32_t token_count;
uint32_t external_token_count;
uint32_t state_count;
uint32_t large_state_count;
uint32_t production_id_count;
uint32_t field_count;
uint16_t max_alias_sequence_length;
const uint16_t *parse_table;
const uint16_t *small_parse_table;
const uint32_t *small_parse_table_map;
const TSParseActionEntry *parse_actions;
const char * const *symbol_names;
const char * const *field_names;
const TSFieldMapSlice *field_map_slices;
const TSFieldMapEntry *field_map_entries;
const TSSymbolMetadata *symbol_metadata;
const TSSymbol *public_symbol_map;
const uint16_t *alias_map;
const TSSymbol *alias_sequences;
const TSLexMode *lex_modes;
bool (*lex_fn)(TSLexer *, TSStateId);
bool (*keyword_lex_fn)(TSLexer *, TSStateId);
TSSymbol keyword_capture_token;
struct {
const bool *states;
const TSSymbol *symbol_map;
void *(*create)(void);
void (*destroy)(void *);
bool (*scan)(void *, TSLexer *, const bool *symbol_whitelist);
unsigned (*serialize)(void *, char *);
void (*deserialize)(void *, const char *, unsigned);
} external_scanner;
const TSStateId *primary_state_ids;
};
static inline bool set_contains(TSCharacterRange *ranges, uint32_t len, int32_t lookahead) {
uint32_t index = 0;
uint32_t size = len - index;
while (size > 1) {
uint32_t half_size = size / 2;
uint32_t mid_index = index + half_size;
TSCharacterRange *range = &ranges[mid_index];
if (lookahead >= range->start && lookahead <= range->end) {
return true;
} else if (lookahead > range->end) {
index = mid_index;
}
size -= half_size;
}
TSCharacterRange *range = &ranges[index];
return (lookahead >= range->start && lookahead <= range->end);
}
/*
* Lexer Macros
*/
#ifdef _MSC_VER
#define UNUSED __pragma(warning(suppress : 4101))
#else
#define UNUSED __attribute__((unused))
#endif
#define START_LEXER() \
bool result = false; \
bool skip = false; \
UNUSED \
bool eof = false; \
int32_t lookahead; \
goto start; \
next_state: \
lexer->advance(lexer, skip); \
start: \
skip = false; \
lookahead = lexer->lookahead;
#define ADVANCE(state_value) \
{ \
state = state_value; \
goto next_state; \
}
#define ADVANCE_MAP(...) \
{ \
static const uint16_t map[] = { __VA_ARGS__ }; \
for (uint32_t i = 0; i < sizeof(map) / sizeof(map[0]); i += 2) { \
if (map[i] == lookahead) { \
state = map[i + 1]; \
goto next_state; \
} \
} \
}
#define SKIP(state_value) \
{ \
skip = true; \
state = state_value; \
goto next_state; \
}
#define ACCEPT_TOKEN(symbol_value) \
result = true; \
lexer->result_symbol = symbol_value; \
lexer->mark_end(lexer);
#define END_STATE() return result;
/*
* Parse Table Macros
*/
#define SMALL_STATE(id) ((id) - LARGE_STATE_COUNT)
#define STATE(id) id
#define ACTIONS(id) id
#define SHIFT(state_value) \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.state = (state_value) \
} \
}}
#define SHIFT_REPEAT(state_value) \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.state = (state_value), \
.repetition = true \
} \
}}
#define SHIFT_EXTRA() \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.extra = true \
} \
}}
#define REDUCE(symbol_name, children, precedence, prod_id) \
{{ \
.reduce = { \
.type = TSParseActionTypeReduce, \
.symbol = symbol_name, \
.child_count = children, \
.dynamic_precedence = precedence, \
.production_id = prod_id \
}, \
}}
#define RECOVER() \
{{ \
.type = TSParseActionTypeRecover \
}}
#define ACCEPT_INPUT() \
{{ \
.type = TSParseActionTypeAccept \
}}
#ifdef __cplusplus
}
#endif
#endif // TREE_SITTER_PARSER_H_

Binary file not shown.

View file

@ -0,0 +1,42 @@
{
"$schema": "https://tree-sitter.github.io/tree-sitter/assets/schemas/config.schema.json",
"grammars": [
{
"name": "objc",
"camelcase": "Objc",
"scope": "source.objc",
"path": ".",
"file-types": [
"h",
"m",
"objc"
],
"highlights": "queries/highlights.scm",
"injections": "queries/injections.scm",
"locals": "queries/locals.scm",
"injection-regex": "^(objc|objectivec)$"
}
],
"metadata": {
"version": "3.0.2",
"license": "MIT",
"description": "Objective-C grammar for tree-sitter",
"authors": [
{
"name": "Amaan Qureshi",
"email": "amaanq12@gmail.com"
}
],
"links": {
"repository": "https://github.com/tree-sitter-grammars/tree-sitter-objc"
}
},
"bindings": {
"c": true,
"go": true,
"node": true,
"python": true,
"rust": true,
"swift": true
}
}