mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
feat(objective-c): add deterministic provider and grammar
This commit is contained in:
parent
9f16eae2d4
commit
236568b8f9
83 changed files with 712883 additions and 25 deletions
2
.github/scripts/update-vendored-grammars.mjs
vendored
2
.github/scripts/update-vendored-grammars.mjs
vendored
|
|
@ -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.
|
||||
|
|
|
|||
5
.github/vendored-grammars.json
vendored
5
.github/vendored-grammars.json
vendored
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ export type NodeLabel =
|
|||
| 'Folder'
|
||||
| 'File'
|
||||
| 'Class'
|
||||
| 'Protocol'
|
||||
| 'Category'
|
||||
| 'Function'
|
||||
| 'Method'
|
||||
| 'Variable'
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export enum SupportedLanguages {
|
|||
Java = 'java',
|
||||
C = 'c',
|
||||
CPlusPlus = 'cpp',
|
||||
ObjectiveC = 'objective-c',
|
||||
CSharp = 'csharp',
|
||||
Go = 'go',
|
||||
Ruby = 'ruby',
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ export const NODE_TABLES = [
|
|||
'Folder',
|
||||
'Function',
|
||||
'Class',
|
||||
'Protocol',
|
||||
'Category',
|
||||
'Interface',
|
||||
'Method',
|
||||
'CodeElement',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -136,6 +136,8 @@ export interface RegistryContext {
|
|||
|
||||
export const CLASS_KINDS: readonly NodeLabel[] = Object.freeze([
|
||||
'Class',
|
||||
'Protocol',
|
||||
'Category',
|
||||
'Interface',
|
||||
'Enum',
|
||||
'Struct',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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). */
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
195
gitnexus/src/core/ingestion/languages/objective-c.ts
Normal file
195
gitnexus/src/core/ingestion/languages/objective-c.ts
Normal 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);
|
||||
},
|
||||
});
|
||||
|
|
@ -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),
|
||||
};
|
||||
1557
gitnexus/src/core/ingestion/languages/objective-c/facts.ts
Normal file
1557
gitnexus/src/core/ingestion/languages/objective-c/facts.ts
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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(/^\.\//, "");
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ import type { NodeLabel, ParameterTypeClass, SymbolDefinition } from 'gitnexus-s
|
|||
*/
|
||||
export const CLASS_TYPES_TUPLE = [
|
||||
'Class',
|
||||
'Protocol',
|
||||
'Category',
|
||||
'Struct',
|
||||
'Interface',
|
||||
'Enum',
|
||||
|
|
|
|||
|
|
@ -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 [];
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -87,6 +87,8 @@ export const CALLER_ANCHOR_LABELS: ReadonlySet<NodeLabel> = new Set<NodeLabel>([
|
|||
'Constructor',
|
||||
'Module',
|
||||
'Class',
|
||||
'Protocol',
|
||||
'Category',
|
||||
'Interface',
|
||||
'Struct',
|
||||
'Enum',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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' ||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ export const SYMBOL_NODE_LABELS: ReadonlySet<NodeLabel> = new Set<NodeLabel>([
|
|||
'Function',
|
||||
'Method',
|
||||
'Class',
|
||||
'Protocol',
|
||||
'Category',
|
||||
'Interface',
|
||||
'CodeElement',
|
||||
'Struct',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
23
gitnexus/test/fixtures/objective-c/SYModuleCaller.h
vendored
Normal file
23
gitnexus/test/fixtures/objective-c/SYModuleCaller.h
vendored
Normal 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
|
||||
32
gitnexus/test/fixtures/objective-c/SYModuleCaller.m
vendored
Normal file
32
gitnexus/test/fixtures/objective-c/SYModuleCaller.m
vendored
Normal 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; }
|
||||
4
gitnexus/test/fixtures/objective-c/SYModuleSupport.h
vendored
Normal file
4
gitnexus/test/fixtures/objective-c/SYModuleSupport.h
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
#ifndef SY_MODULE_SUPPORT_H
|
||||
#define SY_MODULE_SUPPORT_H
|
||||
int SYModuleSupportAdd(int a, int b);
|
||||
#endif
|
||||
|
|
@ -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([
|
||||
{
|
||||
|
|
|
|||
523
gitnexus/test/integration/objective-c-provider.test.ts
Normal file
523
gitnexus/test/integration/objective-c-provider.test.ts
Normal 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);
|
||||
});
|
||||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
41
gitnexus/test/unit/objective-c-parser-loader.test.ts
Normal file
41
gitnexus/test/unit/objective-c-parser-loader.test.ts
Normal 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/,
|
||||
);
|
||||
});
|
||||
});
|
||||
355
gitnexus/test/unit/objective-c-provider.test.ts
Normal file
355
gitnexus/test/unit/objective-c-provider.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
/**
|
||||
|
|
|
|||
21
gitnexus/vendor/tree-sitter-objc/LICENSE
vendored
Normal file
21
gitnexus/vendor/tree-sitter-objc/LICENSE
vendored
Normal 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.
|
||||
20
gitnexus/vendor/tree-sitter-objc/README.md
vendored
Normal file
20
gitnexus/vendor/tree-sitter-objc/README.md
vendored
Normal 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
|
||||
35
gitnexus/vendor/tree-sitter-objc/binding.gyp
vendored
Normal file
35
gitnexus/vendor/tree-sitter-objc/binding.gyp
vendored
Normal 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",
|
||||
],
|
||||
}],
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
19
gitnexus/vendor/tree-sitter-objc/bindings/node/binding.cc
vendored
Normal file
19
gitnexus/vendor/tree-sitter-objc/bindings/node/binding.cc
vendored
Normal 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)
|
||||
9
gitnexus/vendor/tree-sitter-objc/bindings/node/binding_test.js
vendored
Normal file
9
gitnexus/vendor/tree-sitter-objc/bindings/node/binding_test.js
vendored
Normal 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(".")));
|
||||
});
|
||||
27
gitnexus/vendor/tree-sitter-objc/bindings/node/index.d.ts
vendored
Normal file
27
gitnexus/vendor/tree-sitter-objc/bindings/node/index.d.ts
vendored
Normal 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;
|
||||
11
gitnexus/vendor/tree-sitter-objc/bindings/node/index.js
vendored
Normal file
11
gitnexus/vendor/tree-sitter-objc/bindings/node/index.js
vendored
Normal 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 (_) {}
|
||||
1284
gitnexus/vendor/tree-sitter-objc/grammar.js
vendored
Normal file
1284
gitnexus/vendor/tree-sitter-objc/grammar.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
56
gitnexus/vendor/tree-sitter-objc/package.json
vendored
Normal file
56
gitnexus/vendor/tree-sitter-objc/package.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
gitnexus/vendor/tree-sitter-objc/prebuilds/darwin-arm64/tree-sitter-objc.node
vendored
Normal file
BIN
gitnexus/vendor/tree-sitter-objc/prebuilds/darwin-arm64/tree-sitter-objc.node
vendored
Normal file
Binary file not shown.
BIN
gitnexus/vendor/tree-sitter-objc/prebuilds/darwin-x64/tree-sitter-objc.node
vendored
Normal file
BIN
gitnexus/vendor/tree-sitter-objc/prebuilds/darwin-x64/tree-sitter-objc.node
vendored
Normal file
Binary file not shown.
BIN
gitnexus/vendor/tree-sitter-objc/prebuilds/linux-arm64/tree-sitter-objc.node
vendored
Normal file
BIN
gitnexus/vendor/tree-sitter-objc/prebuilds/linux-arm64/tree-sitter-objc.node
vendored
Normal file
Binary file not shown.
BIN
gitnexus/vendor/tree-sitter-objc/prebuilds/linux-x64/tree-sitter-objc.node
vendored
Normal file
BIN
gitnexus/vendor/tree-sitter-objc/prebuilds/linux-x64/tree-sitter-objc.node
vendored
Normal file
Binary file not shown.
BIN
gitnexus/vendor/tree-sitter-objc/prebuilds/win32-arm64/tree-sitter-objc.node
vendored
Normal file
BIN
gitnexus/vendor/tree-sitter-objc/prebuilds/win32-arm64/tree-sitter-objc.node
vendored
Normal file
Binary file not shown.
BIN
gitnexus/vendor/tree-sitter-objc/prebuilds/win32-x64/tree-sitter-objc.node
vendored
Normal file
BIN
gitnexus/vendor/tree-sitter-objc/prebuilds/win32-x64/tree-sitter-objc.node
vendored
Normal file
Binary file not shown.
20
gitnexus/vendor/tree-sitter-objc/queries/folds.scm
vendored
Normal file
20
gitnexus/vendor/tree-sitter-objc/queries/folds.scm
vendored
Normal 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
|
||||
216
gitnexus/vendor/tree-sitter-objc/queries/highlights.scm
vendored
Normal file
216
gitnexus/vendor/tree-sitter-objc/queries/highlights.scm
vendored
Normal 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
|
||||
1
gitnexus/vendor/tree-sitter-objc/queries/indents.scm
vendored
Normal file
1
gitnexus/vendor/tree-sitter-objc/queries/indents.scm
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
; inherits: c
|
||||
10
gitnexus/vendor/tree-sitter-objc/queries/injections.scm
vendored
Normal file
10
gitnexus/vendor/tree-sitter-objc/queries/injections.scm
vendored
Normal 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))
|
||||
1
gitnexus/vendor/tree-sitter-objc/queries/locals.scm
vendored
Normal file
1
gitnexus/vendor/tree-sitter-objc/queries/locals.scm
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
; inherits: c
|
||||
16362
gitnexus/vendor/tree-sitter-objc/src/grammar.json
vendored
Normal file
16362
gitnexus/vendor/tree-sitter-objc/src/grammar.json
vendored
Normal file
File diff suppressed because it is too large
Load diff
7524
gitnexus/vendor/tree-sitter-objc/src/node-types.json
vendored
Normal file
7524
gitnexus/vendor/tree-sitter-objc/src/node-types.json
vendored
Normal file
File diff suppressed because it is too large
Load diff
682692
gitnexus/vendor/tree-sitter-objc/src/parser.c
vendored
Normal file
682692
gitnexus/vendor/tree-sitter-objc/src/parser.c
vendored
Normal file
File diff suppressed because it is too large
Load diff
54
gitnexus/vendor/tree-sitter-objc/src/tree_sitter/alloc.h
vendored
Normal file
54
gitnexus/vendor/tree-sitter-objc/src/tree_sitter/alloc.h
vendored
Normal 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_
|
||||
291
gitnexus/vendor/tree-sitter-objc/src/tree_sitter/array.h
vendored
Normal file
291
gitnexus/vendor/tree-sitter-objc/src/tree_sitter/array.h
vendored
Normal 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_
|
||||
266
gitnexus/vendor/tree-sitter-objc/src/tree_sitter/parser.h
vendored
Normal file
266
gitnexus/vendor/tree-sitter-objc/src/tree_sitter/parser.h
vendored
Normal 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_
|
||||
BIN
gitnexus/vendor/tree-sitter-objc/tree-sitter-objc.wasm
vendored
Normal file
BIN
gitnexus/vendor/tree-sitter-objc/tree-sitter-objc.wasm
vendored
Normal file
Binary file not shown.
42
gitnexus/vendor/tree-sitter-objc/tree-sitter.json
vendored
Normal file
42
gitnexus/vendor/tree-sitter-objc/tree-sitter.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue