mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-10 03:27:59 +00:00
merge: integrate Objective-C provider
This commit is contained in:
commit
acc54938f6
90 changed files with 713146 additions and 28 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
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -70,6 +70,8 @@ eval/.hypothesis/
|
|||
|
||||
# Local docs — planning output (gitnexus-plan / gitnexus-work) stays local, not tracked
|
||||
docs/*
|
||||
!docs/fork/
|
||||
!docs/fork/**
|
||||
|
||||
gitnexus/test/fixtures/mini-repo/*.md
|
||||
gitnexus/test/fixtures/mini-repo/.claude
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ Commands and gotchas live under **Repo reference** below and in **[CONTRIBUTING.
|
|||
## Reference docs
|
||||
|
||||
- **[ARCHITECTURE.md](ARCHITECTURE.md)**, **[CONTRIBUTING.md](CONTRIBUTING.md)**, **[GUARDRAILS.md](GUARDRAILS.md)**
|
||||
- **Fork-specific work:** read **[docs/fork/README.md](docs/fork/README.md)** before changing the Objective-C provider, external index storage, or content-retention behavior. These documents define fork contracts; ForgeMate runner and version orchestration remain outside this repository.
|
||||
- **Call & inheritance resolution (RFC #909 Ring 3):** See ARCHITECTURE.md § Scope-Resolution Pipeline. All languages resolve calls and inheritance through the scope-resolution pipeline (`Registry.lookup`, `preEmitInheritanceEdges`, `emitHeritageEdges`, `buildMro` → `MethodDispatchIndex`). **Shared code in `gitnexus/src/core/ingestion/` must not name languages** — plug language behavior in via `LanguageProvider` / `ScopeResolver` hooks. A language plugs in by implementing `ScopeResolver` (`scope-resolution/contract/scope-resolver.ts`) and registering it in `SCOPE_RESOLVERS`. (The legacy call-resolution DAG + `@heritage` capture path were removed in RING4-1 #942.)
|
||||
- **Cursor:** `.cursor/index.mdc` (always-on); `.cursor/rules/*.mdc` (glob-scoped). Legacy `.cursorrules` deprecated.
|
||||
- **GitNexus:** standard skills in `.claude/skills/gitnexus-*/`; MCP rules in `gitnexus:start` block below.
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -59,6 +59,16 @@ npx gitnexus setup
|
|||
|
||||
That's it. `analyze` indexes the codebase, installs agent skills, registers Claude Code hooks, and creates `AGENTS.md` / `CLAUDE.md` context files — all in one command. `setup` writes the MCP config so your AI agent can use the graph.
|
||||
|
||||
## ForgeMate Fork Extensions
|
||||
|
||||
This fork keeps upstream GitNexus behavior as the default. The following fork-specific capabilities are **planned**, not yet implemented or released:
|
||||
|
||||
- Objective-C semantic indexing.
|
||||
- An external per-repository index directory via `GITNEXUS_STORAGE_PATH`.
|
||||
- Configurable source-content retention via `GITNEXUS_CONTENT_RETENTION`.
|
||||
|
||||
The implementation contracts, compatibility requirements, and acceptance criteria live in [docs/fork/README.md](docs/fork/README.md). Do not rely on these planned variables until the corresponding implementation and README environment-variable entries are released.
|
||||
|
||||
<details>
|
||||
<summary><strong>Install problems?</strong> npm 11 crash · slow cold install · no C++ toolchain</summary>
|
||||
|
||||
|
|
|
|||
109
docs/fork/EXTERNAL_STORAGE_AND_CONTENT_RETENTION.md
Normal file
109
docs/fork/EXTERNAL_STORAGE_AND_CONTENT_RETENTION.md
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
# External Storage and Content Retention
|
||||
|
||||
Status: planned
|
||||
|
||||
## Goal
|
||||
|
||||
Add two opt-in environment variables while preserving the current repository-local behavior by default:
|
||||
|
||||
```sh
|
||||
GITNEXUS_STORAGE_PATH=/absolute/path/to/index-slot
|
||||
GITNEXUS_CONTENT_RETENTION=full|symbol|none
|
||||
```
|
||||
|
||||
`GITNEXUS_STORAGE_PATH` lets an orchestrator choose the exact directory for one repository index. `GITNEXUS_CONTENT_RETENTION` controls source-derived text written into LadybugDB. Neither option creates a shared graph database or makes GitNexus responsible for an external system's version lifecycle.
|
||||
|
||||
## Storage path contract
|
||||
|
||||
### Inputs and default
|
||||
|
||||
- A missing `GITNEXUS_STORAGE_PATH` keeps the upstream storage location: `<repo>/.gitnexus/`.
|
||||
- A non-empty value must be an absolute, NUL-free path whose parent can be created and written. Relative, invalid, or unusable values fail before analysis begins.
|
||||
- The supplied value is the complete directory for one index, not a common root. The caller owns repository identity and chooses a collision-free slot name.
|
||||
- When external storage is selected, GitNexus must not create a `.gitnexus/` placeholder, symlink, or metadata duplicate in the source worktree.
|
||||
|
||||
### What moves
|
||||
|
||||
Every repository-index artifact must resolve through one storage resolver: LadybugDB (`lbug`), index metadata, GitNexus metadata, parse caches, parsed-file caches, lock files, branch placement, runner files, cleanup targets, and any future sibling artifacts. Redirecting only the database is incorrect because later CLI, MCP, server, or cleanup operations would still look in the temporary checkout.
|
||||
|
||||
Resolution order is:
|
||||
|
||||
```text
|
||||
1. Explicit GITNEXUS_STORAGE_PATH
|
||||
2. storagePath recorded in the registered repository entry
|
||||
3. <repository>/.gitnexus/ compatibility default
|
||||
```
|
||||
|
||||
Registry records may add an optional `storagePath`. On a successful analysis with an explicit path, GitNexus records the resolved absolute path so `status`, `serve`, and MCP can open the index after the source worktree has been removed. Existing registry entries without this field remain valid and use the local default.
|
||||
|
||||
### Concurrency and lifecycle
|
||||
|
||||
One storage slot permits one writer under the existing lock discipline. Distinct slots may analyze concurrently. GitNexus may know the checkout path used for analysis as diagnostic metadata, but it must not require that path to still exist for graph-only MCP/CLI operations.
|
||||
|
||||
An external caller may write to a staging slot and atomically promote it to its own `current` location, retaining a previous slot for rollback. That naming, retention, deletion, source checkout, source revision selection, and authorization are external orchestration concerns, not GitNexus behavior.
|
||||
|
||||
## Content retention contract
|
||||
|
||||
### Profiles
|
||||
|
||||
| Value | File `content` | Symbol/section snippets | Graph and identities | Intended effect |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `full` | Retained | Retained | Complete | Current upstream-compatible behavior. |
|
||||
| `symbol` | Omitted | Retained | Complete | Preserve symbol-level evidence without full eligible file text. |
|
||||
| `none` | Omitted | Omitted | Complete | Preserve structural graph only. |
|
||||
|
||||
Missing or empty `GITNEXUS_CONTENT_RETENTION` means `full`. An explicitly invalid value fails clearly; it must not silently select another profile.
|
||||
|
||||
`File.content` is normalized text used for retrieval, not a byte-faithful source archive. It may have normalized whitespace or CJK segmentation and cannot become a source viewer substitute.
|
||||
|
||||
### Query behavior
|
||||
|
||||
| Capability | `full` | `symbol` | `none` |
|
||||
| --- | --- | --- | --- |
|
||||
| Symbol name/selector search | Available | Available | Available |
|
||||
| `context`, `impact`, `trace`, structural Cypher | Available | Available | Available |
|
||||
| Arbitrary file-body keyword retrieval | Available | Only if retained in a symbol snippet | Unavailable |
|
||||
| Content-bearing query/context response | File and symbol content where supported | Symbol snippets only | Clear capability absence |
|
||||
| Filesystem preview, grep, rename, detect-changes | Requires source worktree | Requires source worktree | Requires source worktree |
|
||||
|
||||
The `symbol` and `none` profiles do not change provider parsing, node IDs, line ranges, relationships, or call-graph correctness. They reduce text-retrieval evidence. `symbol` is not equivalent to `full` for natural-language searches that rely on arbitrary comments or method bodies; `none` intentionally removes source text as evidence.
|
||||
|
||||
No profile may fabricate an empty source result as though it were a valid source snippet. Commands and MCP tools must expose capability absence or omit content fields deliberately. Tools that require a live worktree remain unavailable after that worktree is deleted regardless of retained profile.
|
||||
|
||||
### FTS and stored text
|
||||
|
||||
Under `symbol` and `none`, GitNexus must not build file-content FTS indexes. Other FTS indexes may cover only fields allowed by the selected profile. Future FTS allocation improvements, such as skipping unused label indexes, are compatible optimizations but are not required for the first implementation.
|
||||
|
||||
`none` removes all source-derived body text, including source comments/descriptions and PDG basic-block text. It retains stable IDs, names, paths, line ranges, node/edge types, relation structure, resolution confidence, and non-text diagnostics.
|
||||
|
||||
## Metadata, compatibility, and rebuilds
|
||||
|
||||
Persist at least:
|
||||
|
||||
```json
|
||||
{
|
||||
"contentRetention": "symbol",
|
||||
"contentRetentionSchemaVersion": 1,
|
||||
"ftsProfile": "symbol-no-file-content",
|
||||
"storagePath": "/absolute/path/to/index-slot"
|
||||
}
|
||||
```
|
||||
|
||||
Incremental reuse must compare source revision/file hashes, GitNexus build and schema, language-provider and grammar versions, retention profile/schema, FTS profile, and include/exclude/max-file-size configuration. A mismatch requires a clean full rebuild into a new database. Clearing `content` values in place is not a valid conversion because prior FTS and database page allocation may remain.
|
||||
|
||||
Indexes written before these fields existed are interpreted as `full` for read compatibility. The next successful full rebuild writes the new metadata.
|
||||
|
||||
## Web UI and missing source files
|
||||
|
||||
The existing Web UI's file preview and grep operations read the repository filesystem. They must not assume that database `File.content` is valid raw code. With an external index and deleted checkout, the UI must report full-file preview and filesystem grep as unavailable, while continuing to show graph data, symbol snippets permitted by the retention profile, file paths, line ranges, source revision, and resolution confidence. A future raw-code browser needs a separate, explicit byte-faithful retention design.
|
||||
|
||||
## Required regression coverage
|
||||
|
||||
- No option: storage, metadata, CLI, MCP, and query results remain compatible with `<repo>/.gitnexus/` and `full` content.
|
||||
- External storage: all artifacts reside in the selected directory; none appears in the source worktree; a deleted worktree does not prevent graph-only `status` and MCP queries through the registry.
|
||||
- Storage failures: relative, invalid, unwritable, and conflicting paths fail cleanly without a usable partial index.
|
||||
- Concurrency: two repositories with distinct external slots do not share locks, caches, metadata, or data.
|
||||
- Retention: full/symbol/none keep identical structural node and edge counts for a fixture; only allowed text fields and FTS indexes differ.
|
||||
- Rebuild: changing profile, provider/grammar version, schema, or indexing configuration forces full rebuild and produces a physically new index.
|
||||
- MCP/CLI: content requests honor each profile and do not return misleading empty source text.
|
||||
- Web UI: missing checkout, `symbol`, and `none` present explicit unavailable states rather than broken previews.
|
||||
107
docs/fork/OBJECTIVE_C_PROVIDER.md
Normal file
107
docs/fork/OBJECTIVE_C_PROVIDER.md
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
# Objective-C Provider
|
||||
|
||||
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.
|
||||
|
||||
The provider belongs in the existing language-provider and scope-resolution extension points. Shared ingestion code must remain language-agnostic.
|
||||
|
||||
## Compatibility contract
|
||||
|
||||
- Existing language detection and parsing must remain unchanged.
|
||||
- A `.h` file must be classified from its content or surrounding context; it cannot be unconditionally claimed by Objective-C because C and C++ also use that extension.
|
||||
- If Objective-C grammar loading fails, the error must clearly name the missing provider/grammar and cannot corrupt a previously valid index.
|
||||
- Provider and grammar versions must be stored in index metadata. A version change that can alter node identity or edges requires a full rebuild.
|
||||
- No LLM participates in parsing, name resolution, or edge creation. Analysis is Tree-sitter plus deterministic static resolution.
|
||||
|
||||
## MVP model
|
||||
|
||||
The provider must extract and connect:
|
||||
|
||||
- Classes, superclasses, protocols, categories, class extensions, properties, ivars, C functions, imports, declarations, and implementations.
|
||||
- Instance and class methods, preserving their complete multi-part selector.
|
||||
- Inheritance, protocol conformance, import, declaration/implementation, host-class/category, and statically resolved call relationships.
|
||||
|
||||
Stable identity must include enough ownership to distinguish same-named methods. Recommended forms are:
|
||||
|
||||
```text
|
||||
objc:class:<ClassName>
|
||||
objc:protocol:<ProtocolName>
|
||||
objc:category:<HostClass>:<CategoryName>
|
||||
objc:method:<Owner>:-:<selector>
|
||||
objc:method:<Owner>:+:<selector>
|
||||
objc:function:<qualified-or-file-scoped-name>
|
||||
```
|
||||
|
||||
For example, `-loadData:completion:` and `+loadData:completion:` are different symbols. A category method remains linked to both its category and host class; querying the host class must expose distributed implementations.
|
||||
|
||||
## Resolution policy
|
||||
|
||||
Resolution must be conservative. A missing or dynamic target is evidence of uncertainty, not proof that no target exists.
|
||||
|
||||
| Receiver case | Required result |
|
||||
| --- | --- |
|
||||
| Explicit class name, `self`, or `super` | Resolve when the owner is statically known. |
|
||||
| Local, parameter, property, or ivar with known static type | Resolve to matching owner and selector. |
|
||||
| Protocol-typed receiver | Link the protocol method and identify possible implementations as candidates. |
|
||||
| `id`, `Class`, macros, reflection, `performSelector:`, `NSInvocation`, runtime injection, or unknown type | Store selector/location with `resolution=unresolved`; do not emit a certain call edge. |
|
||||
|
||||
The provider should first collect file-local declarations, imports, and types, then resolve across the repository. It must use structured Tree-sitter captures or AST traversal, not regular expressions over source text. Multi-part selectors, block arguments, nullability annotations, generics, macros, and multiline declarations make a regex-only extractor unsafe.
|
||||
|
||||
## Imports and incremental correctness
|
||||
|
||||
- Resolve quoted project imports against the current directory, configured include roots, and indexed headers. Model framework imports as external-module evidence without downloading SDK source.
|
||||
- Merge `@interface`, `@implementation`, categories, and extensions across files.
|
||||
- A changed header, protocol, class declaration, or category invalidates importing and affected implementation/call-resolution state. Incremental output after such a change must match a full rebuild.
|
||||
- Index metadata must record provider version, grammar version, include/exclude configuration, and parsing options used for resolution.
|
||||
|
||||
## Implementation sequence
|
||||
|
||||
1. Add and package a pinned Objective-C Tree-sitter grammar; verify macOS arm64 and the production Linux runner can load it.
|
||||
2. Add language detection for `.m`, `.mm`, and content-classified `.h` files.
|
||||
3. Implement AST extraction and stable IDs for declarations and definitions.
|
||||
4. Implement repository-level merge, imports, inheritance, protocol, and category relationships.
|
||||
5. Add conservative message-send resolution and explicit unresolved evidence.
|
||||
6. Integrate invalidation, metadata comparison, MCP/CLI output, and fixtures.
|
||||
|
||||
## Fixtures and acceptance
|
||||
|
||||
Create a minimal Objective-C fixture containing a class, protocol, category, extension, superclass, properties, ivars, C function, imports, multi-part selector, block parameter, `self`, `super`, protocol receiver, and `id` receiver. Use `symodulebridge` as a real integration fixture after the minimal suite is stable.
|
||||
|
||||
The acceptance bar is:
|
||||
|
||||
- `query "SYModuleCaller"` yields class/method semantic nodes, not only file nodes.
|
||||
- `context "SYModuleCaller" --file <path>` yields declaration, implementation, imports, and known references.
|
||||
- Known statically typed message sends create call edges; dynamic sends are marked unresolved.
|
||||
- Same selector on multiple classes, a category override, and `+` versus `-` methods remain distinct.
|
||||
- A `.m`, `.h`, protocol, or category edit produces results equivalent to a clean rebuild.
|
||||
- Generated documentation, dependency directories, and build output are excluded through explicit indexing configuration.
|
||||
|
||||
## 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.
|
||||
55
docs/fork/README.md
Normal file
55
docs/fork/README.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# Forge-Specific Extensions
|
||||
|
||||
Status: planning baseline
|
||||
|
||||
This directory records behavior that belongs to the `mengkaka/GitNexus` fork. It is intentionally separate from upstream-oriented architecture and user documentation so that a future rebase can distinguish fork contracts from upstream behavior.
|
||||
|
||||
## Baseline and status
|
||||
|
||||
- Upstream project: <https://github.com/abhigyanpatwari/GitNexus>
|
||||
- Fork remote: <https://github.com/mengkaka/GitNexus>
|
||||
- Current fork revision when this directory was introduced: `28187bb3a708`
|
||||
- Public compatibility rule: without a documented fork option, GitNexus must preserve upstream behavior.
|
||||
|
||||
| Capability | Status | Contract |
|
||||
| --- | --- | --- |
|
||||
| Objective-C Provider | Planned | [OBJECTIVE_C_PROVIDER.md](OBJECTIVE_C_PROVIDER.md) |
|
||||
| External index storage | Planned | [EXTERNAL_STORAGE_AND_CONTENT_RETENTION.md](EXTERNAL_STORAGE_AND_CONTENT_RETENTION.md) |
|
||||
| Content retention profiles | Planned | [EXTERNAL_STORAGE_AND_CONTENT_RETENTION.md](EXTERNAL_STORAGE_AND_CONTENT_RETENTION.md) |
|
||||
|
||||
`Planned` means no CLI, MCP, Web UI, or environment-variable behavior may claim support yet. Each implementation PR must update this table, its related design document, tests, and the public README environment-variable table where applicable.
|
||||
|
||||
## Reading order
|
||||
|
||||
1. Read this file for scope and compatibility boundaries.
|
||||
2. Read [OBJECTIVE_C_PROVIDER.md](OBJECTIVE_C_PROVIDER.md) before adding Objective-C parsing or resolution.
|
||||
3. Read [EXTERNAL_STORAGE_AND_CONTENT_RETENTION.md](EXTERNAL_STORAGE_AND_CONTENT_RETENTION.md) before changing repository storage, metadata, content fields, FTS, CLI, MCP, or Web UI behavior.
|
||||
4. Read `ARCHITECTURE.md`, `AGENTS.md`, and the affected implementation code before editing.
|
||||
|
||||
## Ownership boundary
|
||||
|
||||
GitNexus fork responsibilities:
|
||||
|
||||
- Parse and model Objective-C with explicit confidence and unresolved cases.
|
||||
- Persist one repository index at a caller-selected directory while preserving the existing default location.
|
||||
- Control which source-derived text is stored in that index and expose the resulting capabilities honestly.
|
||||
|
||||
ForgeMate responsibilities, intentionally not implemented here:
|
||||
|
||||
- Temporary checkout lifecycle, repository identity, index slot naming, and `current` / `previous` retention.
|
||||
- Source SHA selection, Wiki publication, job status, access control, and cross-repository authorization.
|
||||
- Combining Wiki and code evidence in its own MCP facade.
|
||||
|
||||
The fork must not turn GitNexus into ForgeMate's authoritative version database or a multi-repository shared graph database. A GitNexus index remains a rebuildable per-repository artifact.
|
||||
|
||||
## Change rules
|
||||
|
||||
- Keep default, no-option behavior byte-for-byte compatible where practical.
|
||||
- Persist every option that changes index semantics in repository metadata and reject unsafe incremental reuse.
|
||||
- Treat an index as valid only for its recorded source revision, parser/provider versions, schema, retention profile, and indexing configuration.
|
||||
- Do not silently substitute filesystem source content for stored content, or vice versa.
|
||||
- Do not add ForgeMate-specific paths, project IDs, credentials, or runner behavior to GitNexus core.
|
||||
|
||||
## Documentation lifecycle
|
||||
|
||||
The local `docs/plans/` files are working notes and may remain ignored. Once a design is adopted, this directory is the tracked source of truth for fork contracts. User-facing documentation must not advertise a planned option as available before its code and regression tests land.
|
||||
|
|
@ -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',
|
||||
|
|
@ -110,6 +111,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)
|
||||
*/
|
||||
|
|
@ -31,6 +32,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)
|
||||
|
|
@ -50,6 +52,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
|
||||
|
|
@ -113,6 +117,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' },
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,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,7 @@ 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 { NodeLabel } from 'gitnexus-shared';
|
||||
import type { 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 Parser from 'tree-sitter';
|
||||
|
|
@ -46,6 +46,63 @@ import type { ExtractedDecoratorRoute } from './workers/parse-worker.js';
|
|||
/** Tree-sitter query captures: capture name → AST node (or undefined if not captured). */
|
||||
export type CaptureMap = Record<string, SyntaxNode | undefined>;
|
||||
|
||||
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
|
||||
|
|
@ -130,6 +187,29 @@ interface LanguageProviderConfig {
|
|||
*/
|
||||
readonly preprocessSource?: (sourceText: string, filePath: string) => string;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
|
@ -336,6 +416,23 @@ interface LanguageProviderConfig {
|
|||
filePath: string,
|
||||
) => SharedSpringType[];
|
||||
|
||||
/**
|
||||
* 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';
|
||||
|
|
@ -37,6 +38,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,
|
||||
|
|
@ -68,3 +70,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',
|
||||
|
|
@ -313,6 +315,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';
|
||||
|
||||
|
|
@ -222,7 +222,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':
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ export const CALLER_ANCHOR_LABELS: ReadonlySet<NodeLabel> = new Set<NodeLabel>([
|
|||
'Constructor',
|
||||
'Module',
|
||||
'Class',
|
||||
'Protocol',
|
||||
'Category',
|
||||
'Interface',
|
||||
'Struct',
|
||||
'Enum',
|
||||
|
|
|
|||
|
|
@ -265,6 +265,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 {
|
||||
|
|
@ -242,6 +246,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
|
||||
|
|
@ -252,7 +263,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.
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { cobolScopeResolver } from '../../languages/cobol/scope-resolver.js';
|
|||
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 { 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
|
||||
|
|
@ -51,4 +52,5 @@ export const SCOPE_RESOLVERS: ReadonlyMap<SupportedLanguages, ScopeResolver> = n
|
|||
[SupportedLanguages.Swift, swiftScopeResolver],
|
||||
[SupportedLanguages.Dart, dartScopeResolver],
|
||||
[SupportedLanguages.Vue, vueScopeResolver],
|
||||
[SupportedLanguages.ObjectiveC, objectiveCScopeResolver],
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -180,6 +180,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' ||
|
||||
|
|
|
|||
|
|
@ -2449,12 +2449,15 @@ export const DART_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',
|
||||
|
|
|
|||
|
|
@ -18,7 +18,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,
|
||||
|
|
@ -81,6 +81,11 @@ let C: TreeSitterLanguage | null = null;
|
|||
try {
|
||||
C = requireVendoredGrammar('tree-sitter-c') as TreeSitterLanguage;
|
||||
} catch {}
|
||||
|
||||
let ObjectiveC: TreeSitterLanguage | null = null;
|
||||
try {
|
||||
ObjectiveC = requireVendoredGrammar('tree-sitter-objc') as TreeSitterLanguage;
|
||||
} catch {}
|
||||
import { getLanguageFromFilename } from 'gitnexus-shared';
|
||||
import {
|
||||
buildDefinitionPreScan,
|
||||
|
|
@ -239,7 +244,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;
|
||||
}
|
||||
|
|
@ -539,6 +544,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,
|
||||
|
|
@ -1170,7 +1176,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) {
|
||||
|
|
@ -1619,6 +1625,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
|
||||
|
|
|
|||
|
|
@ -537,6 +537,8 @@ export const streamAllCSVsToDisk = async (
|
|||
// Multi-language node types share the same CSV shape (no isExported column)
|
||||
const multiLangHeader = 'id,name,filePath,startLine,endLine,content,description';
|
||||
const MULTI_LANG_TYPES = [
|
||||
'Protocol',
|
||||
'Category',
|
||||
'Struct',
|
||||
'Enum',
|
||||
'Macro',
|
||||
|
|
|
|||
|
|
@ -68,6 +68,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,
|
||||
|
|
@ -510,6 +534,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,
|
||||
|
|
@ -641,6 +668,8 @@ export const NODE_SCHEMA_QUERIES = [
|
|||
FOLDER_SCHEMA,
|
||||
FUNCTION_SCHEMA,
|
||||
CLASS_SCHEMA,
|
||||
PROTOCOL_SCHEMA,
|
||||
CATEGORY_SCHEMA,
|
||||
INTERFACE_SCHEMA,
|
||||
METHOD_SCHEMA,
|
||||
CODE_ELEMENT_SCHEMA,
|
||||
|
|
|
|||
|
|
@ -177,6 +177,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';
|
||||
import {
|
||||
CLASS_FRAMEWORK_ANNOTATIONS_FEATURE,
|
||||
findAnalysisFeatureMismatches,
|
||||
|
|
@ -231,6 +232,7 @@ const ANALYSIS_FEATURES = [
|
|||
SPRING_CONFIG_BINDINGS_FEATURE,
|
||||
JAVA_ENUM_INTERFACE_HERITAGE_FEATURE,
|
||||
JAVA_RECORD_COMPONENT_ACCESSORS_FEATURE,
|
||||
OBJECTIVE_C_PROVIDER_FEATURE,
|
||||
] as const;
|
||||
|
||||
interface PersistedFrameworkAnnotationRow {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -340,6 +340,8 @@ export const VALID_NODE_LABELS = new Set([
|
|||
'Folder',
|
||||
'Function',
|
||||
'Class',
|
||||
'Protocol',
|
||||
'Category',
|
||||
'Interface',
|
||||
'Method',
|
||||
'CodeElement',
|
||||
|
|
@ -4091,7 +4093,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
|
||||
|
|
@ -4246,7 +4248,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
|
||||
|
|
|
|||
|
|
@ -533,7 +533,12 @@ import type { ParseWorkerResult } from '../core/ingestion/workers/parse-worker.j
|
|||
// value above every in-flight claim at this merge (main 67, #2891's 59, #1616's
|
||||
// stale 2), which is the rule above: above every claim, not above origin/main.
|
||||
// RE-CHECK AGAINST origin/main IMMEDIATELY BEFORE MERGING.
|
||||
const SCHEMA_BUMP = 68;
|
||||
//
|
||||
// 68 -> 69 for Objective-C semantic graph extraction plus the objective-c
|
||||
// ParsedFile side-channel carrying provider facts. A warm v68 cache would replay
|
||||
// `.m`/`.mm`/classified `.h` chunks without Class/Protocol/Category/Method
|
||||
// nodes, unresolved-message evidence, or post-resolution ObjC call facts.
|
||||
const SCHEMA_BUMP = 69;
|
||||
const GITNEXUS_PKG_VERSION = (() => {
|
||||
try {
|
||||
// package.json sits at gitnexus/package.json — two levels up from
|
||||
|
|
|
|||
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);
|
||||
});
|
||||
|
|
@ -33,6 +33,7 @@ const CALLABLE_FLOW_PROVIDER_COVERAGE = {
|
|||
[SupportedLanguages.Dart]: 'matrix',
|
||||
[SupportedLanguages.Vue]: 'matrix',
|
||||
[SupportedLanguages.Cobol]: 'matrix',
|
||||
[SupportedLanguages.ObjectiveC]: 'dedicated',
|
||||
} as const satisfies Record<SupportedLanguages, 'matrix' | 'dedicated'>;
|
||||
|
||||
const PROVIDER_FLOW_CASES = [
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ import {
|
|||
JAVA_RECORD_COMPONENT_ACCESSORS_FEATURE,
|
||||
SPRING_CONFIG_BINDINGS_FEATURE,
|
||||
} from '../../src/core/ingestion/languages/java/analysis-features.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';
|
||||
|
||||
const FEATURES = [
|
||||
CLASS_FRAMEWORK_ANNOTATIONS_FEATURE,
|
||||
|
|
@ -24,6 +30,7 @@ const FEATURES = [
|
|||
SPRING_CONFIG_BINDINGS_FEATURE,
|
||||
JAVA_ENUM_INTERFACE_HERITAGE_FEATURE,
|
||||
JAVA_RECORD_COMPONENT_ACCESSORS_FEATURE,
|
||||
OBJECTIVE_C_PROVIDER_FEATURE,
|
||||
] as const;
|
||||
|
||||
describe('analysis feature versions', () => {
|
||||
|
|
@ -94,6 +101,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(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(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.
|
||||
|
|
|
|||
|
|
@ -218,8 +218,8 @@ describe('PARSE_CACHE_VERSION', () => {
|
|||
// the pre-fix fan-out. This branch staged 64 above the claims live at the
|
||||
// time (61, 62, 63); all three landed and cascaded main to 67, so 68 is the
|
||||
// next free value above every claim at merge — the rule, re-applied.
|
||||
it('pins SCHEMA_BUMP to 68 so concurrent bumps cannot silently collide (#2766)', () => {
|
||||
expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).toBe(68);
|
||||
it('pins SCHEMA_BUMP to 69 so concurrent bumps cannot silently collide (#2766)', () => {
|
||||
expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).toBe(69);
|
||||
// The PREVIOUS version must fail the reuse gate, not merely differ from the
|
||||
// current one — a hardcoded number outside the conflict hunk rebases cleanly
|
||||
// while being wrong, which is exactly how the 37/38 exact clashes landed.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -72,6 +72,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',
|
||||
|
|
@ -134,6 +139,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 + BasicBlock = 32
|
||||
expect(NODE_TABLES).toHaveLength(32);
|
||||
// 9 core + 21 multi-language + Route + Tool + BasicBlock = 34
|
||||
expect(NODE_TABLES).toHaveLength(34);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -308,8 +308,8 @@ describe('LadybugDB Schema', () => {
|
|||
|
||||
describe('schema query ordering', () => {
|
||||
it('NODE_SCHEMA_QUERIES has correct count', () => {
|
||||
// 31 + BasicBlock = 32
|
||||
expect(NODE_SCHEMA_QUERIES).toHaveLength(32);
|
||||
// 33 + BasicBlock = 34
|
||||
expect(NODE_SCHEMA_QUERIES).toHaveLength(34);
|
||||
});
|
||||
|
||||
it('REL_SCHEMA_QUERIES has one relation table', () => {
|
||||
|
|
@ -317,8 +317,8 @@ describe('LadybugDB Schema', () => {
|
|||
});
|
||||
|
||||
it('SCHEMA_QUERIES includes all node + rel + embedding schemas', () => {
|
||||
// 32 node + 1 rel + 1 embedding = 34
|
||||
expect(SCHEMA_QUERIES).toHaveLength(34);
|
||||
// 34 node + 1 rel + 1 embedding = 36
|
||||
expect(SCHEMA_QUERIES).toHaveLength(36);
|
||||
});
|
||||
|
||||
it('node schemas come before relation schemas in SCHEMA_QUERIES', () => {
|
||||
|
|
|
|||
|
|
@ -283,6 +283,17 @@ const CASES: ReadonlyMap<SupportedLanguages, ConformanceCase> = new Map([
|
|||
reachesDecoy: 'CUSTREC',
|
||||
},
|
||||
],
|
||||
[
|
||||
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',
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -451,6 +451,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