feat: TypeEnvironment API with constructor inference, self/this/super resolution (#274)

* feat(type-env): constructor-call type inference for TypeEnv (Phase 1)

Add extractInitializer as a Tier 1 fallback in buildTypeEnv: when a
declaration node has no explicit type annotation, infer the type from
constructor-call patterns (new X(), X::new(), X::default(), $x = new X()).

Languages covered: TypeScript/JS, Java (var), Rust, PHP, C++ (auto).
Python/Kotlin/Swift deferred — need symbol-table access to distinguish
class constructors from function calls.

Adds 20 new unit tests covering constructor inference, annotation
precedence, and known limitations across all supported languages.

* fix(type-env): class-aware constructor resolution, multi-declarator fix

- Add collectClassNames pre-scan: walks AST to build Set<string> of
  class/struct names defined in the file
- C++ extractInitializer uses classNames.has() to verify identifier is
  a known class before inferring (auto x = User() resolves, auto x =
  getUser() does not — no false positives)
- Add InitializerExtractor type that receives classNames parameter
- Fix env.size gating: always call extractInitializer when available,
  so mixed declarators like const a: A = x, b = new B() resolve both
- Add env.has() guard in Java extractInitializer to skip already-bound vars
- Document Rust new/default whitelist rationale
- Pin all test assertions, add mixed multi-declarator test case

* fix(type-env): resolve Self/self/static/parent to actual type names

- Rust: Self::new()/Self::default() resolves to enclosing impl type
- PHP: new self()/static() resolves to enclosing class, parent() to superclass
- Rust: Tier 0 annotation guard prevents overwrite by constructor inference
- Rust: mut_pattern handling in extractVarName for let mut bindings
- TS: fix misleading comment in extractInitializer
- 58 tests passing (3 new Self/self resolution tests)

* perf(type-env): single-pass AST walk with closure-scoped state

Refactors buildTypeEnv to use closures instead of passing mutable state
as parameters. classNames, env, and config are captured by the inner
walk and extractTypeBinding functions — no parameter mutation.

- Eliminates separate collectClassNames pre-scan (O(2n) → O(n))
- config looked up once per file instead of per-node
- 29 fewer lines

* feat(type-env): constructor-inferred type resolution for all languages

Add cross-file constructor type inference to the ingestion pipeline,
enabling receiver-type disambiguation for member calls like
`user.save()` when the variable is assigned from a constructor without
explicit type annotations.

Pipeline changes:
- Add extractInitializer to Python and Swift type extractors
- Add CONSTRUCTOR_BINDING_SCANNERS for Python, Swift, C/C++ in type-env
- Wire constructorBindings through parse-worker → parsing-processor →
  pipeline → processCallsFromExtracted
- Rewrite resolveCallTarget receiver-type filtering (step D) to use
  tiered import resolution (same-file → import-scoped → global) before
  falling back to fuzzy ownerId matching
- Use collectTieredCandidates for constructor binding verification
  instead of raw lookupFuzzy

Bug fixes:
- Fix C++ inline method query: @definition.method was captured on
  field_declaration_list instead of function_definition, causing wrong
  parameterCount for all inline class methods
- Fix parse-worker accumulated/flush results missing constructorBindings

CI changes:
- Add swift.test.ts to ci-integration pipeline group and coverage job
- Update ci-report to fetch base branch (main) coverage for delta
  reporting instead of showing config thresholds
- Add per-suite timing breakdown table (unit/integration/total)
- Add expandable skipped test details section

Tests: 288 passed, 4 skipped (swift — macOS only) across 10 languages
- 36 new constructor-inferred integration tests (4 per language)
- 10 fixture directories with cross-file constructor patterns
- TypeScript, JavaScript, Java, Kotlin, Python, PHP, Rust, Go, C++, Swift

* fix(type-extractors): add type assertion for LanguageTypeConfig

* feat(ruby): constructor-inferred type resolution and self-receiver mapping

Add Ruby User.new constructor binding scanner to type-env, enabling
receiver-type disambiguation for member calls like user.save vs repo.save.
Add self/this → enclosing class resolution in lookupTypeEnv so self.method()
calls resolve to the correct class even when the method name is ambiguous.

* docs: update README with constructor inference and self/this resolution details

* refactor(ingestion): unified ResolutionContext replaces fragmented map passing

Introduce createResolutionContext() as the single resolution API for all
processors. Eliminates duplicated tier-selection logic, fixes heritage
namedImportMap bug, and adds per-file resolution caching.

- NEW resolution-context.ts: closure-factory with resolve(), per-file cache,
  TIER_CONFIDENCE constant, and shared ResolutionTier type
- DELETE symbol-resolver.ts: zero production importers, logic now in
  resolution-context.ts
- call-processor: all functions take ctx instead of 6 separate maps,
  collectTieredCandidates removed (ctx.resolve replaces it),
  D4 redundant re-resolve eliminated
- heritage-processor: takes ctx, resolveHeritageId helper extracts
  repeated 14-line fallback pattern, namedImportMap now included
- import-processor: takes ctx, dead createImportMap/createPackageMap/
  createNamedImportMap factories removed
- pipeline: creates single ctx, wires onProgress to all processors,
  logs cache hit rate in dev mode
- Tier renamed: unique-global → global (honest about returning all candidates)
- Tests migrated: 1178 unit + 84 integration passing

* feat(type-env): self/this/super resolution, TypeEnvironment API, and review fixes

Add cross-language receiver keyword resolution:
- self/this/$this → enclosing class name via AST walk
- super/base/parent → parent class name via heritage AST extraction
  (8 grammar variants: TS/JS, Java, Python, Ruby, C#, PHP, Kotlin, C++, Swift)
- D-phase widening in resolveCallTarget for super→parent method dispatch

Introduce TypeEnvironment API replacing loose TypeEnvResult + lookupTypeEnv:
- buildTypeEnv() returns TypeEnvironment with .lookup() method
- Single-pass AST walk merges constructor binding scan (was separate traversal)
- ClassNameLookup type replaces over-broad ReadonlySet<string> facade
- Memoized class name lookups to avoid redundant SymbolTable scans

Code review fixes (6 agents, 11 findings):
- Replace ctx.resolve(name, '') hack with direct symbols.lookupFuzzy()
- Extract scope key helpers (extractFuncNameFromScope, receiverKey)
- Simplify D-phase from 5 steps to 4 with deduped typeNodeIds
- Remove C from CONSTRUCTOR_BINDING_SCANNERS (YAGNI — C has no constructors)
- Cache Map reuse in ResolutionContext to reduce GC pressure
- Remove unused TieredCandidates import

Integration tests for self/this, parent, and super resolution across all
12 supported languages with per-language fixture directories.

* fix(type-env): generic parent resolution, TS cast inference, C++ brace-init

Fix generic parent class breaking super resolution:
- extractParentClassFromNode now uses extractSimpleTypeName to strip
  generic params (Base<T> → Base) and qualified names (models.Model → Model)
- Affects TS, Java, Python, C# heritage extraction

Fix TypeScript new X() as T / new X()! missed inference:
- Unwrap as_expression and non_null_expression before checking for
  new_expression in extractInitializer

Fix C++ brace-init User{} missed inference:
- Handle compound_literal_expression with type_identifier child
  in extractInitializer

Clean up deprecated lookupTypeEnv:
- Remove standalone lookupTypeEnv export, migrate all callers to
  TypeEnvironment.lookup() method
- Update all 80+ test assertions to use the new API

Integration test fixtures added:
- typescript-cast-constructor-inference (new X() as T, new X()!)
- typescript/java/csharp/kotlin-generic-parent-resolution
- cpp-brace-init-inference (auto x = User{})

* fix(type-extractors): Go &User{}, TS double-cast, Swift .init inference

Fix Go pointer-to-struct literal not inferred:
- Unwrap unary_expression (address-of &) before composite_literal check
- user := &User{} now correctly infers type User

Fix TypeScript double-cast only unwrapping one level:
- Change if to while loop for nested as_expression/non_null_expression
- new User() as unknown as Admin now correctly infers type User

Fix Swift User.init(name:) explicit init call missed:
- Handle navigation_expression callee with .init suffix in extractInitializer

Integration test fixtures:
- go-pointer-constructor-inference (&User{}, &Repo{})
- typescript-double-cast-inference (as unknown as T)

* feat: Rust struct literal, Python qualified ctor, Go new(), Swift .init scanner

- Rust: handle struct_expression in extractInitializer (User { name: "alice" })
- Python: support attribute nodes in extractInitializer (models.User("alice"))
  and the cross-file scanner — extractSimpleTypeName handles qualified names
- Go: handle new(User) built-in in extractGoShortVarDeclaration
- Swift: extend CONSTRUCTOR_BINDING_SCANNERS to handle navigation_expression
  callee for User.init(name:) cross-file resolution

Unit tests: 87 → 96 (Rust struct literal, Go new(), Python qualified ctor,
Python scanner qualified, plus edge cases)
Integration tests: 4 new describe blocks with fixtures

* fix: Rust Self{} resolution, C++ scoped brace-init, PHP promotion params, Ruby constants

- Rust: resolve Self {} struct literal to enclosing impl type (was stored as "Self")
- C++: replace type_identifier guard with extractSimpleTypeName for compound_literal_expression,
  enabling ns::User{} scoped brace-init (closes previously deferred gap)
- PHP: add property_promotion_parameter to TYPED_PARAMETER_TYPES for PHP 8.0+
  constructor property promotion (__construct(private Foo $x))
- Ruby: extend extractRubyConstructorBinding to accept constant left-hand side
  (REPO = Repo.new)

Unit tests: 96 → 101 (+5: Rust Self{} ×2, C++ ns::User{} ×1, PHP promotion ×1,
Ruby constant ×1)
Integration tests: 4 new describe blocks with fixtures

* feat: Phase 1 type resolution gaps — walrus, PHP properties, nullable, Go make/assert

Phase 1 quick wins from the type resolution gap analysis:

1. Python walrus operator := (named_expression) — extractInitializer + scanner
2. PHP 7.4+ typed class properties — property_declaration in extractDeclaration
3. Nullable union unwrapping — User | null → User in extractSimpleTypeName
4. Go make() builtin — slice/map element type extraction
5. Go type assertions — iface.(User) type extraction

Also: PHP primitive_type handling in extractSimpleTypeName (string, int, etc.)

Unit tests: 101 → 114 (+13)
Integration tests: 8 new describe blocks with fixtures

* feat: Phase 2 type resolution gaps — C++ range-for, Rust if-let, C# pattern matching, Python class annotations

Phase 2 medium-effort improvements:

1. C++ range-for with explicit type — for (User& u : vec) binds u: User
2. Rust if-let/while-let captured_pattern — user @ User { .. } binds user: User
3. C# is-pattern matching — if (obj is User user) binds user: User
4. Python class-level annotations — confirmed already working, added tests

Unit tests: 114 → 127 (+13)
Integration tests: 11 new test cases with fixtures
This commit is contained in:
Gergő Magyar 2026-03-14 19:05:49 +00:00 • committed by GitHub
parent 09a3a42ae9
commit 216a8b5db8
232 changed files with 7302 additions and 924 deletions

View file

@ -52,6 +52,7 @@ jobs:
test/integration/resolvers/kotlin.test.ts
test/integration/resolvers/php.test.ts
test/integration/resolvers/ruby.test.ts
test/integration/resolvers/swift.test.ts
- test-group: e2e
test-glob: >-
test/integration/cli-e2e.test.ts
@ -161,6 +162,7 @@ jobs:
test/integration/resolvers/kotlin.test.ts
test/integration/resolvers/php.test.ts
test/integration/resolvers/ruby.test.ts
test/integration/resolvers/swift.test.ts
- name: Upload integration coverage
if: always()

View file

@ -119,6 +119,66 @@ jobs:
sparse-checkout: gitnexus/vitest.config.ts
sparse-checkout-cone-mode: false
# ── Fetch base branch coverage for delta reporting ───────────
- name: Fetch base branch coverage
if: steps.meta.outputs.skip != 'true'
id: base-coverage
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
with:
script: |
const fs = require('fs');
const path = require('path');
// Find the latest successful CI run on main
const runs = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'ci.yml',
branch: 'main',
status: 'success',
per_page: 1,
});
if (runs.data.workflow_runs.length === 0) {
core.setOutput('found', 'false');
core.info('No successful main branch CI runs found');
return;
}
const mainRunId = runs.data.workflow_runs[0].id;
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: mainRunId,
});
const testReports = artifacts.data.artifacts.find(a => a.name === 'test-reports');
if (!testReports) {
core.setOutput('found', 'false');
core.info('No test-reports artifact on main branch');
return;
}
const zip = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: testReports.id,
archive_format: 'zip',
});
const dest = path.join(process.env.RUNNER_TEMP, 'base-coverage');
fs.mkdirSync(dest, { recursive: true });
fs.writeFileSync(path.join(dest, 'base.zip'), Buffer.from(zip.data));
core.setOutput('found', 'true');
core.setOutput('dir', dest);
- name: Extract base coverage
if: steps.meta.outputs.skip != 'true' && steps.base-coverage.outputs.found == 'true'
shell: bash
run: |
cd "${{ steps.base-coverage.outputs.dir }}"
unzip -o base.zip -d base
# ── Merge coverage from unit + integration ─────────────────────
- name: Setup Node.js
if: steps.meta.outputs.skip != 'true'
@ -183,13 +243,14 @@ jobs:
UNIT: ${{ steps.meta.outputs.unit }}
INTEG: ${{ steps.meta.outputs.integration }}
HAS_MERGED: ${{ steps.coverage.outputs.has_merged }}
BASE_FOUND: ${{ steps.base-coverage.outputs.found }}
BASE_DIR: ${{ steps.base-coverage.outputs.dir }}
RUN_URL: ${{ github.event.workflow_run.html_url }}
run: |
DIR="$RUNNER_TEMP/artifacts"
MERGED_DIR="$RUNNER_TEMP/merged-coverage"
# ── Helper: read coverage summary into prefixed vars ──
# Uses printf -v for safe variable assignment (no eval).
read_cov() {
local prefix=$1 file=$2
if [ -n "$file" ] && [ -f "$file" ]; then
@ -224,7 +285,7 @@ jobs:
fi
}
# ── Read all three coverage reports ──
# ── Read all coverage reports ──
UNIT_SUMMARY=$(find "$DIR/test-reports" -name "coverage-summary.json" -type f 2>/dev/null | head -1)
INTEG_SUMMARY=$(find "$DIR/integration-reports" -name "coverage-summary.json" -type f 2>/dev/null | head -1)
MERGED_SUMMARY="$MERGED_DIR/coverage-summary.json"
@ -235,7 +296,14 @@ jobs:
HAS_INTEG=$?
read_cov "M" "$MERGED_SUMMARY"
# ── Locate test results (unit) ──
# ── Read base branch coverage (main) ──
BASE_SUMMARY=""
if [ "$BASE_FOUND" = "true" ] && [ -n "$BASE_DIR" ]; then
BASE_SUMMARY=$(find "$BASE_DIR/base" -name "coverage-summary.json" -type f 2>/dev/null | head -1)
fi
read_cov "B" "$BASE_SUMMARY"
# ── Locate test results ──
RESULTS_FILE=$(find "$DIR/test-reports" -name "test-results.json" -type f 2>/dev/null | head -1)
INTEG_RESULTS=$(find "$DIR/integration-reports" -name "integration-results.json" -type f 2>/dev/null | head -1)
@ -267,17 +335,6 @@ jobs:
FAILED=$((U_FAILED + I_FAILED))
SKIPPED=$((U_SKIPPED + I_SKIPPED))
SUITES=$((U_SUITES + I_SUITES))
DURATION=$((U_DURATION + I_DURATION))
# ── Coverage thresholds (read from vitest.config.ts) ──
if [ -f gitnexus/vitest.config.ts ]; then
THRESH_STMTS=$(grep -oP 'statements:\s*\K[0-9]+' gitnexus/vitest.config.ts || echo 0)
THRESH_BRANCH=$(grep -oP 'branches:\s*\K[0-9]+' gitnexus/vitest.config.ts || echo 0)
THRESH_FUNCS=$(grep -oP 'functions:\s*\K[0-9]+' gitnexus/vitest.config.ts || echo 0)
THRESH_LINES=$(grep -oP 'lines:\s*\K[0-9]+' gitnexus/vitest.config.ts || echo 0)
else
THRESH_STMTS=0; THRESH_BRANCH=0; THRESH_FUNCS=0; THRESH_LINES=0
fi
# ── Status helpers ──
status_icon() {
@ -289,8 +346,22 @@ jobs:
esac
}
cov_delta() {
local pct=$1 base=$2
if [ "$pct" = "N/A" ] || [ "$base" = "N/A" ]; then echo "—"; return; fi
local diff
diff=$(awk "BEGIN { printf \"%.1f\", $pct - $base }")
if [ "$(awk "BEGIN { print ($pct > $base) ? 1 : 0 }")" = "1" ]; then
echo "📈 +${diff}"
elif [ "$(awk "BEGIN { print ($pct < $base) ? 1 : 0 }")" = "1" ]; then
echo "📉 ${diff}"
else
echo "= ${diff}"
fi
}
cov_bar() {
local pct=$1 thresh=$2
local pct=$1 base=$2
if [ "$pct" = "N/A" ]; then echo "—"; return; fi
local filled
filled=$(awk "BEGIN { printf \"%d\", $pct / 5 }")
@ -300,7 +371,8 @@ jobs:
local bar=""
for ((i=0; i<filled; i++)); do bar+="█"; done
for ((i=0; i<empty; i++)); do bar+="░"; done
if [ "$(awk "BEGIN { print ($pct >= $thresh) ? 1 : 0 }")" = "1" ]; then
# Green if >= base (or base unavailable), red if dropped
if [ "$base" = "N/A" ] || [ "$(awk "BEGIN { print ($pct >= $base) ? 1 : 0 }")" = "1" ]; then
echo "🟢 ${bar}"
else
echo "🔴 ${bar}"
@ -333,18 +405,50 @@ jobs:
if [ "$TOTAL" -gt 0 ] 2>/dev/null; then
echo "### Test Results"
echo ""
echo "| Suite | Tests | Passed | Failed | Skipped | Duration |"
echo "|-------|-------|--------|--------|---------|----------|"
if [ "$U_TOTAL" -gt 0 ] 2>/dev/null; then
echo "| Unit | ${U_TOTAL} | ${U_PASSED} | ${U_FAILED} | ${U_SKIPPED} | ${U_DURATION}s |"
fi
if [ "$I_TOTAL" -gt 0 ] 2>/dev/null; then
echo "| Integration | ${I_TOTAL} | ${I_PASSED} | ${I_FAILED} | ${I_SKIPPED} | ${I_DURATION}s |"
fi
echo "| **Total** | **${TOTAL}** | **${PASSED}** | **${FAILED}** | **${SKIPPED}** | **$((U_DURATION + I_DURATION))s** |"
echo ""
if [ "$FAILED" = "0" ]; then
echo "✅ **${PASSED}** passed"
echo "✅ All **${PASSED}** tests passed"
else
echo "❌ **${FAILED}** failed / **${PASSED}** passed"
fi
if [ "$SKIPPED" != "0" ]; then
echo " · ${SKIPPED} skipped"
fi
echo " · ${SUITES} suites · ${TOTAL} total"
echo " · ⏱️ ${DURATION}s"
if [ "$I_TOTAL" -gt 0 ] 2>/dev/null; then
echo " · 📊 ${U_TOTAL} unit + ${I_TOTAL} integration"
echo ""
echo "<details>"
echo "<summary>${SKIPPED} test(s) skipped — expand for details</summary>"
echo ""
# Extract skipped test names from integration results
if [ -n "$INTEG_RESULTS" ] && [ "$I_SKIPPED" -gt 0 ] 2>/dev/null; then
echo "**Integration:**"
jq -r '
.testResults[]
| .assertionResults[]?
| select(.status == "pending" or .status == "skipped")
| "- \(.ancestorTitles | join(" > ")) > \(.title)"
' "$INTEG_RESULTS" 2>/dev/null || echo "- _(unable to parse skipped test details)_"
fi
# Extract skipped test names from unit results
if [ -n "$RESULTS_FILE" ] && [ "$U_SKIPPED" -gt 0 ] 2>/dev/null; then
echo ""
echo "**Unit:**"
jq -r '
.testResults[]
| .assertionResults[]?
| select(.status == "pending" or .status == "skipped")
| "- \(.ancestorTitles | join(" > ")) > \(.title)"
' "$RESULTS_FILE" 2>/dev/null || echo "- _(unable to parse skipped test details)_"
fi
echo ""
echo "</details>"
fi
echo ""
fi
@ -353,15 +457,15 @@ jobs:
cov_table() {
local label=$1 s=$2 b=$3 f=$4 l=$5 sc=$6 bc=$7 fc=$8 lc=$9
shift 9
local ts=$1 tb=$2 tf=$3 tl=$4
local bs=$1 bb=$2 bf=$3 bl=$4
echo "#### ${label}"
echo ""
echo "| Metric | Coverage | Covered | Threshold | Status |"
echo "|--------|----------|---------|-----------|--------|"
echo "| Statements | **${s}%** | ${sc} | ${ts}% | $(cov_bar "$s" "$ts") |"
echo "| Branches | **${b}%** | ${bc} | ${tb}% | $(cov_bar "$b" "$tb") |"
echo "| Functions | **${f}%** | ${fc} | ${tf}% | $(cov_bar "$f" "$tf") |"
echo "| Lines | **${l}%** | ${lc} | ${tl}% | $(cov_bar "$l" "$tl") |"
echo "| Metric | Coverage | Covered | Base | Delta | Status |"
echo "|--------|----------|---------|------|-------|--------|"
echo "| Statements | **${s}%** | ${sc} | ${bs}% | $(cov_delta "$s" "$bs") | $(cov_bar "$s" "$bs") |"
echo "| Branches | **${b}%** | ${bc} | ${bb}% | $(cov_delta "$b" "$bb") | $(cov_bar "$b" "$bb") |"
echo "| Functions | **${f}%** | ${fc} | ${bf}% | $(cov_delta "$f" "$bf") | $(cov_bar "$f" "$bf") |"
echo "| Lines | **${l}%** | ${lc} | ${bl}% | $(cov_delta "$l" "$bl") | $(cov_bar "$l" "$bl") |"
echo ""
}
@ -371,7 +475,7 @@ jobs:
cov_table "Combined (Unit + Integration)" \
"$M_STMTS" "$M_BRANCH" "$M_FUNCS" "$M_LINES" \
"$M_STMTS_COV" "$M_BRANCH_COV" "$M_FUNCS_COV" "$M_LINES_COV" \
"$THRESH_STMTS" "$THRESH_BRANCH" "$THRESH_FUNCS" "$THRESH_LINES"
"$B_STMTS" "$B_BRANCH" "$B_FUNCS" "$B_LINES"
echo "<details>"
echo "<summary>Coverage breakdown by test suite</summary>"
@ -380,37 +484,23 @@ jobs:
cov_table "Unit Tests" \
"$U_STMTS" "$U_BRANCH" "$U_FUNCS" "$U_LINES" \
"$U_STMTS_COV" "$U_BRANCH_COV" "$U_FUNCS_COV" "$U_LINES_COV" \
"$THRESH_STMTS" "$THRESH_BRANCH" "$THRESH_FUNCS" "$THRESH_LINES"
"$B_STMTS" "$B_BRANCH" "$B_FUNCS" "$B_LINES"
fi
if [ "$I_STMTS" != "N/A" ]; then
cov_table "Integration Tests" \
"$I_STMTS" "$I_BRANCH" "$I_FUNCS" "$I_LINES" \
"$I_STMTS_COV" "$I_BRANCH_COV" "$I_FUNCS_COV" "$I_LINES_COV" \
"$THRESH_STMTS" "$THRESH_BRANCH" "$THRESH_FUNCS" "$THRESH_LINES"
"$B_STMTS" "$B_BRANCH" "$B_FUNCS" "$B_LINES"
fi
echo "</details>"
echo ""
echo "<details>"
echo "<summary>Coverage thresholds are auto-ratcheted — they only go up</summary>"
echo ""
echo "Vitest \`thresholds.autoUpdate\` bumps the floor whenever local coverage exceeds it."
echo "CI enforces the current thresholds; developers commit the ratcheted values."
echo "</details>"
echo ""
elif [ "$U_STMTS" != "N/A" ]; then
echo "### Code Coverage (Unit only)"
echo ""
cov_table "Unit Tests" \
"$U_STMTS" "$U_BRANCH" "$U_FUNCS" "$U_LINES" \
"$U_STMTS_COV" "$U_BRANCH_COV" "$U_FUNCS_COV" "$U_LINES_COV" \
"$THRESH_STMTS" "$THRESH_BRANCH" "$THRESH_FUNCS" "$THRESH_LINES"
echo "<details>"
echo "<summary>Coverage thresholds are auto-ratcheted — they only go up</summary>"
echo ""
echo "Vitest \`thresholds.autoUpdate\` bumps the floor whenever local coverage exceeds it."
echo "CI enforces the current thresholds; developers commit the ratcheted values."
echo "</details>"
echo ""
"$B_STMTS" "$B_BRANCH" "$B_FUNCS" "$B_LINES"
else
echo "### Code Coverage"
echo ""

View file

@ -1,7 +1,7 @@
<!-- gitnexus:start -->
# GitNexus — Code Intelligence
This project is indexed by GitNexus as **GitNexus** (1747 symbols, 4569 relationships, 130 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
This project is indexed by GitNexus as **GitNexus** (1927 symbols, 4372 relationships, 143 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
@ -69,6 +69,24 @@ Before completing any code modification task, verify:
3. `gitnexus_detect_changes()` confirms changes match expected scope
4. All d=1 (WILL BREAK) dependents were updated
## Keeping the Index Fresh
After committing code changes, the GitNexus index becomes stale. Re-run analyze to update it:
```bash
npx gitnexus analyze
```
If the index previously included embeddings, preserve them by adding `--embeddings`:
```bash
npx gitnexus analyze --embeddings
```
To check whether embeddings exist, inspect `.gitnexus/meta.json` — the `stats.embeddings` field shows the count (0 means no embeddings). **Running analyze without `--embeddings` will delete any previously generated embeddings.**
> Claude Code users: A PostToolUse hook handles this automatically after `git commit` and `git merge`.
## CLI
| Task | Read this skill file |
@ -79,25 +97,25 @@ Before completing any code modification task, verify:
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
| Work in the Ingestion area (135 symbols) | `.claude/skills/generated/ingestion/SKILL.md` |
| Work in the Workers area (70 symbols) | `.claude/skills/generated/workers/SKILL.md` |
| Work in the Cli area (63 symbols) | `.claude/skills/generated/cli/SKILL.md` |
| Work in the Kuzu area (52 symbols) | `.claude/skills/generated/kuzu/SKILL.md` |
| Work in the Wiki area (52 symbols) | `.claude/skills/generated/wiki/SKILL.md` |
| Work in the Embeddings area (48 symbols) | `.claude/skills/generated/embeddings/SKILL.md` |
| Work in the Components area (42 symbols) | `.claude/skills/generated/components/SKILL.md` |
| Work in the Local area (36 symbols) | `.claude/skills/generated/local/SKILL.md` |
| Work in the Storage area (36 symbols) | `.claude/skills/generated/storage/SKILL.md` |
| Work in the Services area (35 symbols) | `.claude/skills/generated/services/SKILL.md` |
| Work in the Mcp area (32 symbols) | `.claude/skills/generated/mcp/SKILL.md` |
| Work in the Llm area (30 symbols) | `.claude/skills/generated/llm/SKILL.md` |
| Work in the Eval area (18 symbols) | `.claude/skills/generated/eval/SKILL.md` |
| Work in the Bridge area (15 symbols) | `.claude/skills/generated/bridge/SKILL.md` |
| Work in the Ingestion area (184 symbols) | `.claude/skills/generated/ingestion/SKILL.md` |
| Work in the Cli area (71 symbols) | `.claude/skills/generated/cli/SKILL.md` |
| Work in the Workers area (58 symbols) | `.claude/skills/generated/workers/SKILL.md` |
| Work in the Wiki area (50 symbols) | `.claude/skills/generated/wiki/SKILL.md` |
| Work in the Kuzu area (45 symbols) | `.claude/skills/generated/kuzu/SKILL.md` |
| Work in the Components area (40 symbols) | `.claude/skills/generated/components/SKILL.md` |
| Work in the Embeddings area (32 symbols) | `.claude/skills/generated/embeddings/SKILL.md` |
| Work in the Local area (31 symbols) | `.claude/skills/generated/local/SKILL.md` |
| Work in the Mcp area (31 symbols) | `.claude/skills/generated/mcp/SKILL.md` |
| Work in the Services area (25 symbols) | `.claude/skills/generated/services/SKILL.md` |
| Work in the Resolvers area (15 symbols) | `.claude/skills/generated/resolvers/SKILL.md` |
| Work in the Eval area (15 symbols) | `.claude/skills/generated/eval/SKILL.md` |
| Work in the Llm area (14 symbols) | `.claude/skills/generated/llm/SKILL.md` |
| Work in the Hooks area (14 symbols) | `.claude/skills/generated/hooks/SKILL.md` |
| Work in the Search area (11 symbols) | `.claude/skills/generated/search/SKILL.md` |
| Work in the Bridge area (13 symbols) | `.claude/skills/generated/bridge/SKILL.md` |
| Work in the Environments area (11 symbols) | `.claude/skills/generated/environments/SKILL.md` |
| Work in the Analysis area (10 symbols) | `.claude/skills/generated/analysis/SKILL.md` |
| Work in the Agents area (9 symbols) | `.claude/skills/generated/agents/SKILL.md` |
| Work in the Graph area (6 symbols) | `.claude/skills/generated/graph/SKILL.md` |
| Work in the Server area (9 symbols) | `.claude/skills/generated/server/SKILL.md` |
| Work in the Type-extractors area (8 symbols) | `.claude/skills/generated/type-extractors/SKILL.md` |
| Work in the Unit area (7 symbols) | `.claude/skills/generated/unit/SKILL.md` |
<!-- gitnexus:end -->

View file

@ -1,7 +1,7 @@
<!-- gitnexus:start -->
# GitNexus — Code Intelligence
This project is indexed by GitNexus as **GitNexus** (1747 symbols, 4569 relationships, 130 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
This project is indexed by GitNexus as **GitNexus** (1927 symbols, 4372 relationships, 143 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
@ -69,6 +69,24 @@ Before completing any code modification task, verify:
3. `gitnexus_detect_changes()` confirms changes match expected scope
4. All d=1 (WILL BREAK) dependents were updated
## Keeping the Index Fresh
After committing code changes, the GitNexus index becomes stale. Re-run analyze to update it:
```bash
npx gitnexus analyze
```
If the index previously included embeddings, preserve them by adding `--embeddings`:
```bash
npx gitnexus analyze --embeddings
```
To check whether embeddings exist, inspect `.gitnexus/meta.json` — the `stats.embeddings` field shows the count (0 means no embeddings). **Running analyze without `--embeddings` will delete any previously generated embeddings.**
> Claude Code users: A PostToolUse hook handles this automatically after `git commit` and `git merge`.
## CLI
| Task | Read this skill file |
@ -79,25 +97,25 @@ Before completing any code modification task, verify:
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
| Work in the Ingestion area (135 symbols) | `.claude/skills/generated/ingestion/SKILL.md` |
| Work in the Workers area (70 symbols) | `.claude/skills/generated/workers/SKILL.md` |
| Work in the Cli area (63 symbols) | `.claude/skills/generated/cli/SKILL.md` |
| Work in the Kuzu area (52 symbols) | `.claude/skills/generated/kuzu/SKILL.md` |
| Work in the Wiki area (52 symbols) | `.claude/skills/generated/wiki/SKILL.md` |
| Work in the Embeddings area (48 symbols) | `.claude/skills/generated/embeddings/SKILL.md` |
| Work in the Components area (42 symbols) | `.claude/skills/generated/components/SKILL.md` |
| Work in the Local area (36 symbols) | `.claude/skills/generated/local/SKILL.md` |
| Work in the Storage area (36 symbols) | `.claude/skills/generated/storage/SKILL.md` |
| Work in the Services area (35 symbols) | `.claude/skills/generated/services/SKILL.md` |
| Work in the Mcp area (32 symbols) | `.claude/skills/generated/mcp/SKILL.md` |
| Work in the Llm area (30 symbols) | `.claude/skills/generated/llm/SKILL.md` |
| Work in the Eval area (18 symbols) | `.claude/skills/generated/eval/SKILL.md` |
| Work in the Bridge area (15 symbols) | `.claude/skills/generated/bridge/SKILL.md` |
| Work in the Ingestion area (184 symbols) | `.claude/skills/generated/ingestion/SKILL.md` |
| Work in the Cli area (71 symbols) | `.claude/skills/generated/cli/SKILL.md` |
| Work in the Workers area (58 symbols) | `.claude/skills/generated/workers/SKILL.md` |
| Work in the Wiki area (50 symbols) | `.claude/skills/generated/wiki/SKILL.md` |
| Work in the Kuzu area (45 symbols) | `.claude/skills/generated/kuzu/SKILL.md` |
| Work in the Components area (40 symbols) | `.claude/skills/generated/components/SKILL.md` |
| Work in the Embeddings area (32 symbols) | `.claude/skills/generated/embeddings/SKILL.md` |
| Work in the Local area (31 symbols) | `.claude/skills/generated/local/SKILL.md` |
| Work in the Mcp area (31 symbols) | `.claude/skills/generated/mcp/SKILL.md` |
| Work in the Services area (25 symbols) | `.claude/skills/generated/services/SKILL.md` |
| Work in the Resolvers area (15 symbols) | `.claude/skills/generated/resolvers/SKILL.md` |
| Work in the Eval area (15 symbols) | `.claude/skills/generated/eval/SKILL.md` |
| Work in the Llm area (14 symbols) | `.claude/skills/generated/llm/SKILL.md` |
| Work in the Hooks area (14 symbols) | `.claude/skills/generated/hooks/SKILL.md` |
| Work in the Search area (11 symbols) | `.claude/skills/generated/search/SKILL.md` |
| Work in the Bridge area (13 symbols) | `.claude/skills/generated/bridge/SKILL.md` |
| Work in the Environments area (11 symbols) | `.claude/skills/generated/environments/SKILL.md` |
| Work in the Analysis area (10 symbols) | `.claude/skills/generated/analysis/SKILL.md` |
| Work in the Agents area (9 symbols) | `.claude/skills/generated/agents/SKILL.md` |
| Work in the Graph area (6 symbols) | `.claude/skills/generated/graph/SKILL.md` |
| Work in the Server area (9 symbols) | `.claude/skills/generated/server/SKILL.md` |
| Work in the Type-extractors area (8 symbols) | `.claude/skills/generated/type-extractors/SKILL.md` |
| Work in the Unit area (7 symbols) | `.claude/skills/generated/unit/SKILL.md` |
<!-- gitnexus:end -->

View file

@ -320,14 +320,30 @@ GitNexus builds a complete knowledge graph of your codebase through a multi-phas
1. **Structure** — Walks the file tree and maps folder/file relationships
2. **Parsing** — Extracts functions, classes, methods, and interfaces using Tree-sitter ASTs
3. **Resolution** — Resolves imports and function calls across files with language-aware logic
3. **Resolution** — Resolves imports, function calls, heritage, constructor inference, and `self`/`this` receiver types across files with language-aware logic
4. **Clustering** — Groups related symbols into functional communities
5. **Processes** — Traces execution flows from entry points through call chains
6. **Search** — Builds hybrid search indexes for fast retrieval
### Supported Languages
TypeScript, JavaScript, Python, Java, Kotlin, C, C++, C#, Go, Ruby, Rust, PHP, Swift
| Language | Imports | Named Bindings | Exports | Heritage | Type Annotations | Constructor Inference | Config | Frameworks | Entry Points |
|----------|---------|----------------|---------|----------|-----------------|---------------------|--------|------------|-------------|
| TypeScript | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| JavaScript | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ | ✓ | ✓ |
| Python | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Java | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ |
| Kotlin | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ |
| C# | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Go | ✓ | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Rust | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ |
| PHP | ✓ | ✓ | ✓ | — | ✓ | ✓ | ✓ | ✓ | ✓ |
| Ruby | ✓ | — | ✓ | ✓ | — | ✓ | — | ✓ | ✓ |
| Swift | — | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| C | — | — | ✓ | — | ✓ | ✓ | — | ✓ | ✓ |
| C++ | — | — | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ |
**Imports** — cross-file import resolution · **Named Bindings** — `import { X as Y }` / re-export tracking · **Exports** — public/exported symbol detection · **Heritage** — class inheritance, interfaces, mixins · **Type Annotations** — explicit type extraction for receiver resolution · **Constructor Inference** — infer receiver type from constructor calls (`self`/`this` resolution included for all languages) · **Config** — language toolchain config parsing (tsconfig, go.mod, etc.) · **Frameworks** — AST-based framework pattern detection · **Entry Points** — entry point scoring heuristics
---
@ -487,9 +503,10 @@ The wiki generator reads the indexed graph structure, groups files into modules
### Recently Completed
- [X] Constructor-Inferred Type Resolution, `self`/`this` Receiver Mapping
- [X] Wiki Generation, Multi-File Rename, Git-Diff Impact Analysis
- [X] Process-Grouped Search, 360-Degree Context, Claude Code Hooks
- [X] Multi-Repo MCP, Zero-Config Setup, 11 Language Support
- [X] Multi-Repo MCP, Zero-Config Setup, 13 Language Support
- [X] Community Detection, Process Detection, Confidence Scoring
- [X] Hybrid Search, Vector Index

View file

@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"mcp__plugin_claude-mem_mcp-search__get_observations"
]
}
}

9
gitnexus/Dockerfile.test Normal file
View file

@ -0,0 +1,9 @@
FROM node:22-bookworm
WORKDIR /app
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
COPY . .
RUN npm ci --ignore-scripts \
&& node scripts/patch-tree-sitter-swift.cjs \
&& (npm rebuild 2>&1 || true) \
&& cd node_modules/tree-sitter-kotlin && npx --yes node-gyp rebuild 2>&1
CMD ["npx", "vitest", "run", "test/integration", "--reporter=verbose"]

View file

@ -161,23 +161,23 @@ TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust, PHP, Kotlin, Swift,
### Language Feature Matrix
| Language | Imports | Types | Exports | Named Bindings | Config | Frameworks | Entry Points | Heritage |
|----------|---------|-------|---------|----------------|--------|------------|-------------|----------|
| TypeScript | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| JavaScript | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Python | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| C# | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Java | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ | ✓ |
| Kotlin | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ | ✓ |
| Go | ✓ | ✓ | ✓ | — | ✓ | ✓ | ✓ | ✓ |
| Rust | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ | ✓ |
| PHP | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — |
| Swift | — | ✓ | ✓ | — | ✓ | ✓ | ✓ | ✓ |
| Ruby | ✓ | ✓ | ✓ | — | — | ✓ | ✓ | ✓ |
| C | — | ✓ | ✓ | — | — | ✓ | ✓ | ✓ |
| C++ | — | ✓ | ✓ | — | — | ✓ | ✓ | ✓ |
| Language | Imports | Named Bindings | Exports | Heritage | Type Annotations | Constructor Inference | Config | Frameworks | Entry Points |
|----------|---------|----------------|---------|----------|-----------------|---------------------|--------|------------|-------------|
| TypeScript | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| JavaScript | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ | ✓ | ✓ |
| Python | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Java | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ |
| Kotlin | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ |
| C# | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Go | ✓ | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Rust | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ |
| PHP | ✓ | ✓ | ✓ | — | ✓ | ✓ | ✓ | ✓ | ✓ |
| Ruby | ✓ | — | ✓ | ✓ | — | ✓ | — | ✓ | ✓ |
| Swift | — | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| C | — | — | ✓ | — | ✓ | ✓ | — | ✓ | ✓ |
| C++ | — | — | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ |
**Imports** — cross-file import resolution · **Types** — type annotation extraction · **Exports** — public/exported symbol detection · **Named Bindings** — `import { X }` tracking · **Config** — language toolchain config parsing (tsconfig, go.mod, etc.) · **Frameworks** — AST-based framework pattern detection · **Entry Points** — entry point scoring heuristics · **Heritage** — class inheritance / interface implementation
**Imports** — cross-file import resolution · **Named Bindings** — `import { X as Y }` / re-export tracking · **Exports** — public/exported symbol detection · **Heritage** — class inheritance, interfaces, mixins · **Type Annotations** — explicit type extraction for receiver resolution · **Constructor Inference** — infer receiver type from constructor calls (`self`/`this` resolution included for all languages) · **Config** — language toolchain config parsing (tsconfig, go.mod, etc.) · **Frameworks** — AST-based framework pattern detection · **Entry Points** — entry point scoring heuristics
## Agent Skills

View file

@ -0,0 +1,259 @@
---
title: "feat: Close type resolution gaps across all 12 languages"
type: feat
status: active
date: 2026-03-14
---
# Close Type Resolution Gaps Across All 12 Languages
## Overview
The type resolution system (TypeEnv + call-processor) has been hardened through 6 rounds of review on `feat/type-resolution-constructor-inference`. This plan closes the remaining gaps identified by the comprehensive 4-agent gap analysis covering all 12 supported languages.
Every task MUST include per-language integration tests following the established pattern:
- Fixture directory: `test/fixtures/lang-resolution/<lang>-<pattern>/`
- Integration test: `test/integration/resolvers/<lang>.test.ts`
- Unit tests: `test/unit/type-env.test.ts` where applicable
## Problem Statement
The current TypeEnv extracts types from explicit annotations (Tier 0) and constructor calls (Tier 1). Many common patterns across all languages produce no type binding, causing receiver-type resolution to fail silently. The gap analysis identified 14 categories of missing patterns affecting call resolution accuracy.
## Implementation Phases
### Phase 1: Quick Wins (Independent — can run in parallel)
Each task is self-contained. No dependencies between them.
#### Task 1.1: Python walrus operator `:=`
- **File**: `src/core/ingestion/type-extractors/python.ts`
- **Change**: Add `named_expression` to `DECLARATION_NODE_TYPES`
- **Why**: `if (user := get_user()):` is common Python 3.8+ pattern; `user` gets no type binding
- **Pattern**: `named_expression` has `name` (identifier) and `value` (call) children
- **Tests**:
- Unit: `type-env.test.ts` — walrus with call, walrus with annotated type
- Integration fixture: `test/fixtures/lang-resolution/python-walrus-operator/`
- Integration test: `python.test.ts` — `user := User(); user.save()` resolves
#### Task 1.2: PHP typed class properties
- **File**: `src/core/ingestion/type-extractors/php.ts`
- **Change**: Add `property_declaration` to `DECLARATION_NODE_TYPES`, implement `extractDeclaration` for it
- **Why**: `private User $repo;` is the primary PHP 7.4+ property declaration pattern
- **Pattern**: `property_declaration` has `type` (named_type) and child `property_element` with `name` (variable_name)
- **Tests**:
- Unit: `type-env.test.ts` — typed property extraction
- Integration fixture: `test/fixtures/lang-resolution/php-typed-properties/`
- Integration test: `php.test.ts` — `private UserRepo $repo; $repo->save()` resolves
#### Task 1.3: Nullable receiver unwrapping
- **File**: `src/core/ingestion/utils.ts` — `extractReceiverName` function (~line 800)
- **Change**: Before returning `undefined` for non-simple receivers, check if the receiver node wraps a simple identifier with an optional chain operator
- **Patterns to handle**:
- TS/JS: `optional_chain_expression` wrapping `member_expression`
- Kotlin: `safe_navigation_expression`
- C#: `conditional_access_expression`
- Swift: `optional_chaining_expression`
- **Approach**: Add the wrapped expression types to `SIMPLE_RECEIVER_TYPES` or unwrap one level before checking
- **Tests**:
- Integration fixtures per language: `ts-nullable-receiver/`, `kotlin-nullable-receiver/`, etc.
- Integration tests: `user?.save()` resolves same as `user.save()`
#### Task 1.4: Go `make()` builtin
- **File**: `src/core/ingestion/type-extractors/go.ts`
- **Change**: In `extractGoShortVarDeclaration`, add a `call_expression` branch for `make` (similar to existing `new` branch)
- **Pattern**: `make([]User, 0)` — first arg is `slice_type`/`map_type` with element type
- **Approach**: Extract element type from the first argument's type node
- **Tests**:
- Unit: `type-env.test.ts` — `make([]User, 0)`, `make(map[string]User)`
- Integration fixture: `test/fixtures/lang-resolution/go-make-builtin/`
- Integration test: `go.test.ts`
#### Task 1.5: Go type assertions
- **File**: `src/core/ingestion/type-extractors/go.ts`
- **Change**: Handle `type_assertion_expression` in short var declarations
- **Pattern**: `user, ok := iface.(User)` — `type_assertion_expression` has a `type` field
- **Tests**:
- Unit: `type-env.test.ts` — type assertion single and comma-ok forms
- Integration fixture: `test/fixtures/lang-resolution/go-type-assertion/`
- Integration test: `go.test.ts`
---
### Phase 2: Medium Effort (Some dependencies noted)
#### Task 2.1: C++ range-for loop variables
- **File**: `src/core/ingestion/type-extractors/c-cpp.ts`
- **Change**: Add `for_range_loop` to `DECLARATION_NODE_TYPES`, extract the type from the declaration part
- **Pattern**: `for (auto& user : users)` — the `for_range_loop` has a `type` and `declarator` child
- **Note**: When type is `auto`, would need collection element type inference (deferred to Phase 3). For explicit types `for (User& u : users)` this works now.
- **Tests**:
- Unit: explicit type in range-for
- Integration fixture: `test/fixtures/lang-resolution/cpp-range-for/`
- Integration test: `cpp.test.ts`
#### Task 2.2: Rust `if let` / `while let` bindings
- **File**: `src/core/ingestion/type-extractors/rust.ts`
- **Change**: Add `if_let_expression` and `while_let_expression` to `DECLARATION_NODE_TYPES` or handle in `extractDeclaration`
- **Pattern**: `if let Some(user) = opt { user.save() }` — pattern binding inside conditional
- **Approach**: Extract the pattern variable and the matched type from the source expression
- **Note**: Full generic unwrapping (Some<User> → User) is Phase 3. Initial version extracts the binding variable with no type — still useful for scope tracking.
- **Tests**:
- Unit: if-let with annotated type, while-let
- Integration fixture: `test/fixtures/lang-resolution/rust-if-let/`
- Integration test: `rust.test.ts`
#### Task 2.3: Swift `guard let` / `if let` bindings
- **File**: `src/core/ingestion/type-extractors/swift.ts`
- **Change**: Add `guard_statement` and `if_statement` with `optional_binding_condition` to declaration handling
- **Pattern**: `guard let user = fetchUser() else { return }` — `optional_binding_condition` has `pattern` and `value`
- **Note**: Swift parser availability varies (Node 22 issue). Tests must use `describe.skipIf(!swiftAvailable)`.
- **Tests**:
- Integration fixture: `test/fixtures/lang-resolution/swift-guard-let/`
- Integration test: `swift.test.ts` with skipIf guard
#### Task 2.4: C# pattern matching `is Type variable`
- **File**: `src/core/ingestion/type-extractors/csharp.ts`
- **Change**: Handle `is_pattern_expression` or `declaration_pattern` in the type extractor
- **Pattern**: `if (obj is User user) { user.Save(); }` — `declaration_pattern` has `type` and `name`
- **Tests**:
- Unit: `type-env.test.ts` — is-pattern with type
- Integration fixture: `test/fixtures/lang-resolution/csharp-pattern-matching/`
- Integration test: `csharp.test.ts`
#### Task 2.5: Python class-level type annotations
- **File**: `src/core/ingestion/type-extractors/python.ts`
- **Change**: Ensure `assignment` declarationNodeTypes also captures class body assignments with type annotations
- **Pattern**: `class User: name: str = "default"` — tree-sitter may use `expression_statement` > `assignment` inside class body
- **Note**: Check if this already works via existing `assignment` handling — may just need scope key fix
- **Tests**:
- Unit: class-level annotation
- Integration fixture: `test/fixtures/lang-resolution/python-class-annotations/`
---
### Phase 3: Architecture Changes (Sequential — requires design decisions)
#### Task 3.1: Return type inference
- **Files**: `src/core/ingestion/type-env.ts`, `src/core/ingestion/call-processor.ts`
- **Change**: When processing calls, look up the callee's return type from SymbolTable and bind the assignment target
- **Approach**:
1. `extractMethodSignature` already stores `returnType` in symbol metadata
2. In `buildTypeEnv` or as a post-processing step, for assignments like `let x = foo()`, look up `foo` in SymbolTable
3. If `foo.returnType` exists, add binding `x → returnType`
4. Must strip nullable wrappers and generic containers to get base type
- **Risk**: Circular dependencies if two functions return each other's types. Mitigate with depth limit.
- **Tests**: Per-language integration tests for return-type-inferred receiver resolution
#### Task 3.2: Chained property access resolution
- **Files**: `src/core/ingestion/utils.ts` (`extractReceiverName`), `src/core/ingestion/call-processor.ts`
- **Change**: When receiver is `this.repo`, resolve `this` → class type, then look up `repo` property type on that class
- **Approach**:
1. Extend `extractReceiverName` to return structured data: `{ chain: ['this', 'repo'] }` for multi-level access
2. In call-processor, resolve chain iteratively: lookup first element, find property type, continue
3. Requires property-type tracking: store class property types in TypeEnv or SymbolTable
- **Risk**: Performance impact from recursive lookups. Limit chain depth to 3.
- **Tests**: Per-language `this.repo.save()` / `self.db.query()` patterns
#### Task 3.3: For-loop variable typing
- **Files**: Per-language type extractors + `src/core/ingestion/type-env.ts`
- **Change**: Extract loop variable type from explicit annotations or inferred from collection type
- **Patterns per language**:
- TS/JS: `for (const x of collection)` — `for_in_statement` with `left` and `right`
- Java/C#: `for (Type x : collection)` — explicit type already works
- Go: `for _, v := range slice` — `range_clause` with identifier
- Python: `for x in collection` — `for_statement` with `left`
- Rust: `for x in iter` — `for_expression` with `pattern`
- **Depends on**: Task 3.1 (return type inference) for inferring collection element types
- **Tests**: Per-language integration tests
#### Task 3.4: Generic type parameter extraction
- **File**: `src/core/ingestion/type-extractors/shared.ts` — `extractSimpleTypeName`
- **Change**: Add optional `extractGenericArgs` mode that returns type parameters alongside base type
- **Current**: `List<User>` → `List` (base only)
- **New**: `List<User>` → `{ base: 'List', args: ['User'] }` when requested
- **Use case**: For-loop element type inference, collection method resolution
- **Tests**: Unit tests for each language's generic syntax
#### Task 3.5: Block-level type narrowing
- **File**: `src/core/ingestion/type-env.ts` — scope handling
- **Change**: Add sub-function scope support for if/match/guard blocks
- **Current**: Scope keys are `funcName@startIndex` or `''` (file)
- **New**: Add block scope keys like `funcName@startIndex#if@lineN`
- **Risk**: Scope lookup complexity increases. Need to walk up scope chain on miss.
- **Tests**: Per-language pattern matching, instanceof, type guards
#### Task 3.6: Ruby dedicated type extractor
- **File**: New `src/core/ingestion/type-extractors/ruby.ts`
- **Change**: Replace the stub in `index.ts` with a real type extractor
- **Features**:
- YARD annotation parsing (`@param name [Type]`, `@return [Type]`)
- Instance variable type inference from constructor assignments
- Block parameter typing heuristics
- **Tests**: Integration fixture: `test/fixtures/lang-resolution/ruby-typed-methods/`
---
## Acceptance Criteria
### Functional Requirements
- [ ] Each Phase 1 task has unit tests AND per-language integration tests with fixtures
- [ ] Each Phase 2 task has per-language integration tests with fixtures
- [ ] Each Phase 3 task has cross-language integration tests
- [ ] All existing 101 unit tests continue to pass
- [ ] All existing integration tests continue to pass
- [ ] No regressions in existing call resolution
### Quality Gates
- [ ] `npx vitest run test/unit/type-env.test.ts` — all pass
- [ ] `npx vitest run test/integration/resolvers/ --no-file-parallelism` — all pass
- [ ] No new TypeScript compilation errors
### Testing Pattern (MANDATORY for every task)
```
1. Create fixture: test/fixtures/lang-resolution/<lang>-<pattern>/
- Source file with the pattern (e.g., main.py, models.py)
- Class/struct definitions with methods to resolve against
2. Add describe block: test/integration/resolvers/<lang>.test.ts
- beforeAll: runPipelineFromRepo(fixture_path)
- Tests: verify CALLS edges resolve to correct file/method
3. Add unit tests: test/unit/type-env.test.ts (where applicable)
- Parse code snippet, verify TypeEnv bindings
```
## Task Dependency Graph
```
Phase 1 (all parallel, no dependencies):
1.1 Python walrus ─┐
1.2 PHP properties ├─ All independent
1.3 Nullable unwrap ├─ Can run as parallel swarm agents
1.4 Go make() │
1.5 Go type assert ─┘
Phase 2 (mostly parallel):
2.1 C++ range-for ─┐
2.2 Rust if-let ├─ Independent of each other
2.3 Swift guard-let ├─ Can run as parallel swarm agents
2.4 C# pattern match │
2.5 Python class ann ─┘
Phase 3 (sequential dependencies):
3.1 Return type inference ──→ 3.3 For-loop typing
↑
3.4 Generic extraction ────────────┘
3.2 Chained property access (independent)
3.5 Block-level scoping (independent)
3.6 Ruby type extractor (independent)
```
## Sources & References
- PR #274: `feat/type-resolution-constructor-inference` — 6 rounds of review fixes
- Existing patterns: `test/fixtures/lang-resolution/` (150 fixture dirs)
- Integration tests: `test/integration/resolvers/*.test.ts` (12 languages)
- Type extractors: `src/core/ingestion/type-extractors/*.ts` (9 files + index)
- TypeEnv: `src/core/ingestion/type-env.ts`
- Call processor: `src/core/ingestion/call-processor.ts`
- Receiver extraction: `src/core/ingestion/utils.ts:extractReceiverName`

View file

@ -1,14 +1,12 @@
import { KnowledgeGraph } from '../graph/types.js';
import { ASTCache } from './ast-cache.js';
import type { SymbolDefinition, SymbolTable } from './symbol-table.js';
import { ImportMap, PackageMap, NamedImportMap, isFileInPackageDir } from './import-processor.js';
import { resolveSymbolInternal } from './symbol-resolver.js';
import { walkBindingChain } from './named-binding-extraction.js';
import type { SymbolDefinition } from './symbol-table.js';
import Parser from 'tree-sitter';
import type { ResolutionContext } from './resolution-context.js';
import { TIER_CONFIDENCE, type ResolutionTier } from './resolution-context.js';
import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
import { generateId } from '../../lib/utils.js';
import { SupportedLanguages } from '../../config/supported-languages.js';
import {
getLanguageFromFilename,
isVerboseIngestionEnabled,
@ -21,9 +19,9 @@ import {
extractReceiverName,
findEnclosingClassId,
} from './utils.js';
import { buildTypeEnv, lookupTypeEnv } from './type-env.js';
import { buildTypeEnv } from './type-env.js';
import { getTreeSitterBufferSize } from './constants.js';
import type { ExtractedCall, ExtractedHeritage, ExtractedRoute } from './workers/parse-worker.js';
import type { ExtractedCall, ExtractedHeritage, ExtractedRoute, FileConstructorBindings } from './workers/parse-worker.js';
import { callRouters } from './call-routing.js';
/**
@ -33,7 +31,7 @@ import { callRouters } from './call-routing.js';
const findEnclosingFunction = (
node: any,
filePath: string,
symbolTable: SymbolTable
ctx: ResolutionContext
): string | null => {
let current = node.parent;
@ -42,8 +40,10 @@ const findEnclosingFunction = (
const { funcName, label } = extractFunctionName(current);
if (funcName) {
const nodeId = symbolTable.lookupExact(filePath, funcName);
if (nodeId) return nodeId;
const resolved = ctx.resolve(funcName, filePath);
if (resolved?.tier === 'same-file' && resolved.candidates.length > 0) {
return resolved.candidates[0].nodeId;
}
return generateId(label, `${filePath}:${funcName}`);
}
@ -58,11 +58,8 @@ export const processCalls = async (
graph: KnowledgeGraph,
files: { path: string; content: string }[],
astCache: ASTCache,
symbolTable: SymbolTable,
importMap: ImportMap,
packageMap?: PackageMap,
ctx: ResolutionContext,
onProgress?: (current: number, total: number) => void,
namedImportMap?: NamedImportMap,
): Promise<ExtractedHeritage[]> => {
const parser = await loadParser();
const collectedHeritage: ExtractedHeritage[] = [];
@ -74,7 +71,6 @@ export const processCalls = async (
onProgress?.(i + 1, files.length);
if (i % 20 === 0) await yieldToEventLoop();
// 1. Check language support first
const language = getLanguageFromFilename(file.path);
if (!language) continue;
if (!isLanguageAvailable(language)) {
@ -87,24 +83,15 @@ export const processCalls = async (
const queryStr = LANGUAGE_QUERIES[language];
if (!queryStr) continue;
// 2. ALWAYS load the language before querying (parser is stateful)
await loadLanguage(language, file.path);
// 3. Get AST (Try Cache First)
let tree = astCache.get(file.path);
let wasReparsed = false;
if (!tree) {
// Cache Miss: Re-parse
// Use larger bufferSize for files > 32KB
try {
tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) });
} catch (parseError) {
// Skip files that can't be parsed
continue;
}
wasReparsed = true;
// Cache re-parsed tree so heritage phase gets hits
astCache.set(file.path, tree);
}
@ -119,17 +106,16 @@ export const processCalls = async (
continue;
}
// Build per-file TypeEnv for receiver resolution
const lang = getLanguageFromFilename(file.path);
const typeEnv = lang ? buildTypeEnv(tree, lang) : new Map();
const typeEnv = lang ? buildTypeEnv(tree, lang, ctx.symbols) : null;
const callRouter = callRouters[language];
// 3. Process each call match
ctx.enableCache(file.path);
matches.forEach(match => {
const captureMap: Record<string, any> = {};
match.captures.forEach(c => captureMap[c.name] = c.node);
// Only process @call captures
if (!captureMap['call']) return;
const nameNode = captureMap['call.name'];
@ -137,12 +123,11 @@ export const processCalls = async (
const calledName = nameNode.text;
// Dispatch: route language-specific calls (heritage, properties, imports)
const routed = callRouter(calledName, captureMap['call']);
if (routed) {
switch (routed.kind) {
case 'skip':
case 'import': // handled by import-processor
case 'import':
return;
case 'heritage':
@ -171,7 +156,7 @@ export const processCalls = async (
description: item.accessorType,
},
});
symbolTable.add(file.path, item.propName, nodeId, 'Property',
ctx.symbols.add(file.path, item.propName, nodeId, 'Property',
propEnclosingClassId ? { ownerId: propEnclosingClassId } : undefined);
const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
graph.addRelationship({
@ -190,34 +175,28 @@ export const processCalls = async (
}
case 'call':
break; // fall through to normal call processing below
break;
}
}
// Skip common built-ins and noise
if (isBuiltInOrNoise(calledName)) return;
const callNode = captureMap['call'];
const callForm = inferCallForm(callNode, nameNode);
const receiverName = callForm === 'member' ? extractReceiverName(nameNode) : undefined;
const receiverTypeName = receiverName ? lookupTypeEnv(typeEnv, receiverName, callNode) : undefined;
const receiverTypeName = receiverName && typeEnv ? typeEnv.lookup(receiverName, callNode) : undefined;
// 4. Resolve the target using priority strategy (returns confidence)
const resolved = resolveCallTarget({
calledName,
argCount: countCallArguments(callNode),
callForm,
receiverTypeName,
}, file.path, symbolTable, importMap, packageMap, namedImportMap);
}, file.path, ctx);
if (!resolved) return;
// 5. Find the enclosing function (caller)
const enclosingFuncId = findEnclosingFunction(callNode, file.path, symbolTable);
// Use enclosing function as source, fallback to file for top-level calls
const enclosingFuncId = findEnclosingFunction(callNode, file.path, ctx);
const sourceId = enclosingFuncId || generateId('File', file.path);
const relId = generateId('CALLS', `${sourceId}:${calledName}->${resolved.nodeId}`);
graph.addRelationship({
@ -230,7 +209,7 @@ export const processCalls = async (
});
});
// Tree is now owned by the LRU cache — no manual delete needed
ctx.clearCache();
}
if (skippedByLang && skippedByLang.size > 0) {
@ -249,15 +228,8 @@ export const processCalls = async (
*/
interface ResolveResult {
nodeId: string;
confidence: number; // 0-1: how sure are we?
reason: string; // 'import-resolved' | 'same-file' | 'unique-global'
}
type ResolutionTier = 'same-file' | 'import-scoped' | 'unique-global';
interface TieredCandidates {
candidates: SymbolDefinition[];
tier: ResolutionTier;
confidence: number;
reason: string;
}
const CALLABLE_SYMBOL_TYPES = new Set([
@ -268,73 +240,16 @@ const CALLABLE_SYMBOL_TYPES = new Set([
'Delegate',
]);
const collectTieredCandidates = (
calledName: string,
currentFile: string,
symbolTable: SymbolTable,
importMap: ImportMap,
packageMap?: PackageMap,
namedImportMap?: NamedImportMap,
): TieredCandidates | null => {
const allDefs = symbolTable.lookupFuzzy(calledName);
// Tier 1: Same-file — highest priority, prevents imports from shadowing local defs
// (matches resolveSymbolInternal which checks lookupExactFull before named bindings)
const localDefs = allDefs.filter(def => def.filePath === currentFile);
if (localDefs.length > 0) {
return { candidates: localDefs, tier: 'same-file' };
}
// Tier 2a-named: Check named bindings with re-export chain following.
// Aliased imports (import { User as U }) mean lookupFuzzy('U') returns
// empty but we can resolve via the exported name.
// Re-exports (export { User } from './base') are followed up to 5 hops.
if (namedImportMap) {
const chainResult = resolveNamedBindingChainForCandidates(
calledName, currentFile, symbolTable, namedImportMap, allDefs,
);
if (chainResult) return chainResult;
}
if (allDefs.length === 0) return null;
const importedFiles = importMap.get(currentFile);
if (importedFiles) {
const importedDefs = allDefs.filter(def => importedFiles.has(def.filePath));
if (importedDefs.length > 0) {
return { candidates: importedDefs, tier: 'import-scoped' };
}
}
const importedPackages = packageMap?.get(currentFile);
if (importedPackages) {
const packageDefs = allDefs.filter(def => {
for (const dirSuffix of importedPackages) {
if (isFileInPackageDir(def.filePath, dirSuffix)) return true;
}
return false;
});
if (packageDefs.length > 0) {
return { candidates: packageDefs, tier: 'import-scoped' };
}
}
// Tier 3: Global — pass all candidates through; filterCallableCandidates
// will narrow by kind/arity and resolveCallTarget only emits when exactly 1 remains.
return { candidates: allDefs, tier: 'unique-global' };
};
const CONSTRUCTOR_TARGET_TYPES = new Set(['Constructor', 'Class', 'Struct', 'Record']);
const filterCallableCandidates = (
candidates: SymbolDefinition[],
candidates: readonly SymbolDefinition[],
argCount?: number,
callForm?: 'free' | 'member' | 'constructor',
): SymbolDefinition[] => {
let kindFiltered: SymbolDefinition[];
if (callForm === 'constructor') {
// For constructor calls, prefer Constructor > Class/Struct/Record > callable fallback
const constructors = candidates.filter(c => c.type === 'Constructor');
if (constructors.length > 0) {
kindFiltered = constructors;
@ -360,19 +275,15 @@ const filterCallableCandidates = (
const toResolveResult = (
definition: SymbolDefinition,
tier: ResolutionTier,
): ResolveResult => {
if (tier === 'same-file') {
return { nodeId: definition.nodeId, confidence: 0.95, reason: 'same-file' };
}
if (tier === 'import-scoped') {
return { nodeId: definition.nodeId, confidence: 0.9, reason: 'import-resolved' };
}
return { nodeId: definition.nodeId, confidence: 0.5, reason: 'unique-global' };
};
): ResolveResult => ({
nodeId: definition.nodeId,
confidence: TIER_CONFIDENCE[tier],
reason: tier === 'same-file' ? 'same-file' : tier === 'import-scoped' ? 'import-resolved' : 'global',
});
/**
* Resolve a function call to its target node ID using priority strategy:
* A. Narrow candidates by scope tier (same-file, import-scoped, unique-global)
* A. Narrow candidates by scope tier via ctx.resolve()
* B. Filter to callable symbol kinds (constructor-aware when callForm is set)
* C. Apply arity filtering when parameter metadata is available
* D. Apply receiver-type filtering for member calls with typed receivers
@ -382,29 +293,48 @@ const toResolveResult = (
const resolveCallTarget = (
call: Pick<ExtractedCall, 'calledName' | 'argCount' | 'callForm' | 'receiverTypeName'>,
currentFile: string,
symbolTable: SymbolTable,
importMap: ImportMap,
packageMap?: PackageMap,
namedImportMap?: NamedImportMap,
ctx: ResolutionContext,
): ResolveResult | null => {
const tiered = collectTieredCandidates(call.calledName, currentFile, symbolTable, importMap, packageMap, namedImportMap);
const tiered = ctx.resolve(call.calledName, currentFile);
if (!tiered) return null;
const filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, call.callForm);
// D. Receiver-type filtering: for member calls with a known receiver type,
// filter candidates by ownerId matching the resolved type's nodeId
if (call.callForm === 'member' && call.receiverTypeName && filteredCandidates.length > 1) {
const typeDefs = symbolTable.lookupFuzzy(call.receiverTypeName);
if (typeDefs.length > 0) {
const typeNodeIds = new Set(typeDefs.map(d => d.nodeId));
const ownerFiltered = filteredCandidates.filter(c => c.ownerId && typeNodeIds.has(c.ownerId));
// resolve the type through the same tiered import infrastructure, then
// filter method candidates to the type's defining file. Fall back to
// fuzzy ownerId matching only when file-based narrowing is inconclusive.
//
// Applied regardless of candidate count — the sole same-file candidate may
// belong to the wrong class (e.g. super.save() should hit the parent's save,
// not the child's own save method in the same file).
if (call.callForm === 'member' && call.receiverTypeName) {
// D1. Resolve the receiver type
const typeResolved = ctx.resolve(call.receiverTypeName, currentFile);
if (typeResolved && typeResolved.candidates.length > 0) {
const typeNodeIds = new Set(typeResolved.candidates.map(d => d.nodeId));
const typeFiles = new Set(typeResolved.candidates.map(d => d.filePath));
// D2. Widen candidates: same-file tier may miss the parent's method when
// it lives in another file. Query the symbol table directly for all
// global methods with this name, then apply arity/kind filtering.
const methodPool = filteredCandidates.length <= 1
? filterCallableCandidates(ctx.symbols.lookupFuzzy(call.calledName), call.argCount, call.callForm)
: filteredCandidates;
// D3. File-based: prefer candidates whose filePath matches the resolved type's file
const fileFiltered = methodPool.filter(c => typeFiles.has(c.filePath));
if (fileFiltered.length === 1) {
return toResolveResult(fileFiltered[0], tiered.tier);
}
// D4. ownerId fallback: narrow by ownerId matching the type's nodeId
const pool = fileFiltered.length > 0 ? fileFiltered : methodPool;
const ownerFiltered = pool.filter(c => c.ownerId && typeNodeIds.has(c.ownerId));
if (ownerFiltered.length === 1) {
return toResolveResult(ownerFiltered[0], tiered.tier);
}
// If receiver filtering narrows to 0, fall through to name-only resolution
// If still 2+, refuse (don't guess)
if (ownerFiltered.length > 1) return null;
if (fileFiltered.length > 1 || ownerFiltered.length > 1) return null;
}
}
@ -413,62 +343,100 @@ const resolveCallTarget = (
return toResolveResult(filteredCandidates[0], tiered.tier);
};
// ── Scope key helpers ────────────────────────────────────────────────────
// Scope keys use the format "funcName@startIndex" (produced by type-env.ts).
// Source IDs use "Label:filepath:funcName" (produced by parse-worker.ts).
// NUL (\0) is used as a composite-key separator because it cannot appear
// in source-code identifiers, preventing ambiguous concatenation.
/** Extract the function name from a scope key ("funcName@startIndex" → "funcName"). */
const extractFuncNameFromScope = (scope: string): string =>
scope.slice(0, scope.indexOf('@'));
/** Extract the trailing function name from a sourceId ("Function:filepath:funcName" → "funcName"). */
const extractFuncNameFromSourceId = (sourceId: string): string => {
const lastColon = sourceId.lastIndexOf(':');
return lastColon >= 0 ? sourceId.slice(lastColon + 1) : '';
};
/** Build a scope-aware composite key for receiver type lookup. */
const receiverKey = (funcName: string, varName: string): string =>
`${funcName}\0${varName}`;
/**
* Fast path: resolve pre-extracted call sites from workers.
* No AST parsing — workers already extracted calledName + sourceId.
* This function only does symbol table lookups + graph mutations.
*/
export const processCallsFromExtracted = async (
graph: KnowledgeGraph,
extractedCalls: ExtractedCall[],
symbolTable: SymbolTable,
importMap: ImportMap,
packageMap?: PackageMap,
ctx: ResolutionContext,
onProgress?: (current: number, total: number) => void,
namedImportMap?: NamedImportMap,
constructorBindings?: FileConstructorBindings[],
) => {
// Group by file for progress reporting
// Scope-aware receiver types: keyed by filePath → "funcName\0varName" → typeName.
// The scope dimension prevents collisions when two functions in the same file
// have same-named locals pointing to different constructor types.
const fileReceiverTypes = new Map<string, Map<string, string>>();
if (constructorBindings) {
for (const { filePath, bindings } of constructorBindings) {
for (const { scope, varName, calleeName } of bindings) {
const tiered = ctx.resolve(calleeName, filePath);
const isClass = tiered?.candidates.some(def => def.type === 'Class') ?? false;
if (isClass) {
if (!fileReceiverTypes.has(filePath)) fileReceiverTypes.set(filePath, new Map());
fileReceiverTypes.get(filePath)!.set(receiverKey(extractFuncNameFromScope(scope), varName), calleeName);
}
}
}
}
const byFile = new Map<string, ExtractedCall[]>();
for (const call of extractedCalls) {
let list = byFile.get(call.filePath);
if (!list) {
list = [];
byFile.set(call.filePath, list);
}
if (!list) { list = []; byFile.set(call.filePath, list); }
list.push(call);
}
const totalFiles = byFile.size;
let filesProcessed = 0;
for (const [_filePath, calls] of byFile) {
for (const [filePath, calls] of byFile) {
filesProcessed++;
if (filesProcessed % 100 === 0) {
onProgress?.(filesProcessed, totalFiles);
await yieldToEventLoop();
}
ctx.enableCache(filePath);
const receiverMap = fileReceiverTypes.get(filePath);
for (const call of calls) {
const resolved = resolveCallTarget(
call,
call.filePath,
symbolTable,
importMap,
packageMap,
namedImportMap,
);
let effectiveCall = call;
if (!call.receiverTypeName && call.receiverName && receiverMap) {
const callFuncName = extractFuncNameFromSourceId(call.sourceId);
const resolvedType = receiverMap.get(receiverKey(callFuncName, call.receiverName))
?? receiverMap.get(receiverKey('', call.receiverName)); // fall back to file-level scope
if (resolvedType) {
effectiveCall = { ...call, receiverTypeName: resolvedType };
}
}
const resolved = resolveCallTarget(effectiveCall, effectiveCall.filePath, ctx);
if (!resolved) continue;
const relId = generateId('CALLS', `${call.sourceId}:${call.calledName}->${resolved.nodeId}`);
const relId = generateId('CALLS', `${effectiveCall.sourceId}:${effectiveCall.calledName}->${resolved.nodeId}`);
graph.addRelationship({
id: relId,
sourceId: call.sourceId,
sourceId: effectiveCall.sourceId,
targetId: resolved.nodeId,
type: 'CALLS',
confidence: resolved.confidence,
reason: resolved.reason,
});
}
ctx.clearCache();
}
onProgress?.(totalFiles, totalFiles);
@ -480,10 +448,8 @@ export const processCallsFromExtracted = async (
export const processRoutesFromExtracted = async (
graph: KnowledgeGraph,
extractedRoutes: ExtractedRoute[],
symbolTable: SymbolTable,
importMap: ImportMap,
packageMap?: PackageMap,
onProgress?: (current: number, total: number) => void
ctx: ResolutionContext,
onProgress?: (current: number, total: number) => void,
) => {
for (let i = 0; i < extractedRoutes.length; i++) {
const route = extractedRoutes[i];
@ -494,23 +460,18 @@ export const processRoutesFromExtracted = async (
if (!route.controllerName || !route.methodName) continue;
// Resolve controller class using shared resolver (Tier 1: same file,
// Tier 2: import-scoped, Tier 3: unique global).
const resolution = resolveSymbolInternal(route.controllerName, route.filePath, symbolTable, importMap, packageMap);
if (!resolution) continue;
const controllerResolved = ctx.resolve(route.controllerName, route.filePath);
if (!controllerResolved || controllerResolved.candidates.length === 0) continue;
if (controllerResolved.tier === 'global' && controllerResolved.candidates.length > 1) continue;
const controllerDef = resolution.definition;
// Derive confidence from the resolution tier
const confidence = resolution.tier === 'same-file' ? 0.95
: resolution.tier === 'import-scoped' ? 0.9
: 0.7;
const controllerDef = controllerResolved.candidates[0];
const confidence = TIER_CONFIDENCE[controllerResolved.tier];
// Find the method on the controller
const methodId = symbolTable.lookupExact(controllerDef.filePath, route.methodName);
const methodResolved = ctx.resolve(route.methodName, controllerDef.filePath);
const methodId = methodResolved?.tier === 'same-file' ? methodResolved.candidates[0]?.nodeId : undefined;
const sourceId = generateId('File', route.filePath);
if (!methodId) {
// Construct method ID manually
const guessedId = generateId('Method', `${controllerDef.filePath}:${route.methodName}`);
const relId = generateId('CALLS', `${sourceId}:route->${guessedId}`);
graph.addRelationship({
@ -537,22 +498,3 @@ export const processRoutesFromExtracted = async (
onProgress?.(extractedRoutes.length, extractedRoutes.length);
};
/**
* Follow re-export chains through NamedImportMap for call candidate collection.
* Delegates chain-walking to the shared walkBindingChain utility, then
* applies call-processor semantics: any number of matches accepted.
*/
const resolveNamedBindingChainForCandidates = (
calledName: string,
currentFile: string,
symbolTable: SymbolTable,
namedImportMap: NamedImportMap,
allDefs: SymbolDefinition[],
): TieredCandidates | null => {
const defs = walkBindingChain(calledName, currentFile, symbolTable, namedImportMap, allDefs);
if (defs && defs.length > 0) {
return { candidates: defs, tier: 'import-scoped' };
}
return null;
};

View file

@ -16,7 +16,6 @@
import { KnowledgeGraph } from '../graph/types.js';
import { ASTCache } from './ast-cache.js';
import { SymbolTable, SymbolDefinition } from './symbol-table.js';
import Parser from 'tree-sitter';
import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
@ -25,8 +24,7 @@ import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop }
import { SupportedLanguages } from '../../config/supported-languages.js';
import { getTreeSitterBufferSize } from './constants.js';
import type { ExtractedHeritage } from './workers/parse-worker.js';
import { resolveSymbol } from './symbol-resolver.js';
import type { ImportMap, PackageMap } from './import-processor.js';
import type { ResolutionContext } from './resolution-context.js';
/** C#/Java convention: interfaces start with I followed by an uppercase letter */
const INTERFACE_NAME_RE = /^I[A-Z]/;
@ -42,14 +40,12 @@ const INTERFACE_NAME_RE = /^I[A-Z]/;
const resolveExtendsType = (
parentName: string,
currentFilePath: string,
symbolTable: SymbolTable,
importMap: ImportMap,
ctx: ResolutionContext,
language: SupportedLanguages,
packageMap?: PackageMap,
): { type: 'EXTENDS' | 'IMPLEMENTS'; idPrefix: string } => {
const resolved = resolveSymbol(parentName, currentFilePath, symbolTable, importMap, packageMap);
if (resolved) {
const isInterface = resolved.type === 'Interface';
const resolved = ctx.resolve(parentName, currentFilePath);
if (resolved && resolved.candidates.length > 0) {
const isInterface = resolved.candidates[0].type === 'Interface';
return isInterface
? { type: 'IMPLEMENTS', idPrefix: 'Interface' }
: { type: 'EXTENDS', idPrefix: 'Class' };
@ -66,14 +62,34 @@ const resolveExtendsType = (
return { type: 'EXTENDS', idPrefix: 'Class' };
};
/**
* Resolve a symbol ID for heritage, with fallback to generated ID.
* Uses ctx.resolve() → pick first candidate's nodeId → generate synthetic ID.
*/
const resolveHeritageId = (
name: string,
filePath: string,
ctx: ResolutionContext,
fallbackLabel: string,
fallbackKey?: string,
): string => {
const resolved = ctx.resolve(name, filePath);
if (resolved && resolved.candidates.length > 0) {
// For global with multiple candidates, refuse (a wrong edge is worse than no edge)
if (resolved.tier === 'global' && resolved.candidates.length > 1) {
return generateId(fallbackLabel, fallbackKey ?? name);
}
return resolved.candidates[0].nodeId;
}
return generateId(fallbackLabel, fallbackKey ?? name);
};
export const processHeritage = async (
graph: KnowledgeGraph,
files: { path: string; content: string }[],
astCache: ASTCache,
symbolTable: SymbolTable,
importMap: ImportMap,
packageMap?: PackageMap,
onProgress?: (current: number, total: number) => void
ctx: ResolutionContext,
onProgress?: (current: number, total: number) => void,
) => {
const parser = await loadParser();
const logSkipped = isVerboseIngestionEnabled();
@ -102,8 +118,6 @@ export const processHeritage = async (
// 3. Get AST
let tree = astCache.get(file.path);
let wasReparsed = false;
if (!tree) {
// Use larger bufferSize for files > 32KB
try {
@ -112,7 +126,6 @@ export const processHeritage = async (
// Skip files that can't be parsed
continue;
}
wasReparsed = true;
// Cache re-parsed tree for potential future use
astCache.set(file.path, tree);
}
@ -148,14 +161,10 @@ export const processHeritage = async (
const className = captureMap['heritage.class'].text;
const parentClassName = captureMap['heritage.extends'].text;
const { type: relType, idPrefix } = resolveExtendsType(parentClassName, file.path, symbolTable, importMap, language, packageMap);
const { type: relType, idPrefix } = resolveExtendsType(parentClassName, file.path, ctx, language);
const childId = symbolTable.lookupExact(file.path, className) ||
resolveSymbol(className, file.path, symbolTable, importMap, packageMap)?.nodeId ||
generateId('Class', `${file.path}:${className}`);
const parentId = resolveSymbol(parentClassName, file.path, symbolTable, importMap, packageMap)?.nodeId ||
generateId(idPrefix, `${parentClassName}`);
const childId = resolveHeritageId(className, file.path, ctx, 'Class', `${file.path}:${className}`);
const parentId = resolveHeritageId(parentClassName, file.path, ctx, idPrefix);
if (childId && parentId && childId !== parentId) {
graph.addRelationship({
@ -174,19 +183,12 @@ export const processHeritage = async (
const className = captureMap['heritage.class'].text;
const interfaceName = captureMap['heritage.implements'].text;
// Resolve class and interface IDs
const classId = symbolTable.lookupExact(file.path, className) ||
resolveSymbol(className, file.path, symbolTable, importMap, packageMap)?.nodeId ||
generateId('Class', `${file.path}:${className}`);
const interfaceId = resolveSymbol(interfaceName, file.path, symbolTable, importMap, packageMap)?.nodeId ||
generateId('Interface', `${interfaceName}`);
const classId = resolveHeritageId(className, file.path, ctx, 'Class', `${file.path}:${className}`);
const interfaceId = resolveHeritageId(interfaceName, file.path, ctx, 'Interface');
if (classId && interfaceId) {
const relId = generateId('IMPLEMENTS', `${classId}->${interfaceId}`);
graph.addRelationship({
id: relId,
id: generateId('IMPLEMENTS', `${classId}->${interfaceId}`),
sourceId: classId,
targetId: interfaceId,
type: 'IMPLEMENTS',
@ -201,19 +203,12 @@ export const processHeritage = async (
const structName = captureMap['heritage.class'].text;
const traitName = captureMap['heritage.trait'].text;
// Resolve struct and trait IDs
const structId = symbolTable.lookupExact(file.path, structName) ||
resolveSymbol(structName, file.path, symbolTable, importMap, packageMap)?.nodeId ||
generateId('Struct', `${file.path}:${structName}`);
const traitId = resolveSymbol(traitName, file.path, symbolTable, importMap, packageMap)?.nodeId ||
generateId('Trait', `${traitName}`);
const structId = resolveHeritageId(structName, file.path, ctx, 'Struct', `${file.path}:${structName}`);
const traitId = resolveHeritageId(traitName, file.path, ctx, 'Trait');
if (structId && traitId) {
const relId = generateId('IMPLEMENTS', `${structId}->${traitId}`);
graph.addRelationship({
id: relId,
id: generateId('IMPLEMENTS', `${structId}->${traitId}`),
sourceId: structId,
targetId: traitId,
type: 'IMPLEMENTS',
@ -243,10 +238,8 @@ export const processHeritage = async (
export const processHeritageFromExtracted = async (
graph: KnowledgeGraph,
extractedHeritage: ExtractedHeritage[],
symbolTable: SymbolTable,
importMap: ImportMap,
packageMap?: PackageMap,
onProgress?: (current: number, total: number) => void
ctx: ResolutionContext,
onProgress?: (current: number, total: number) => void,
) => {
const total = extractedHeritage.length;
@ -261,14 +254,10 @@ export const processHeritageFromExtracted = async (
if (h.kind === 'extends') {
const fileLanguage = getLanguageFromFilename(h.filePath);
if (!fileLanguage) continue;
const { type: relType, idPrefix } = resolveExtendsType(h.parentName, h.filePath, symbolTable, importMap, fileLanguage, packageMap);
const { type: relType, idPrefix } = resolveExtendsType(h.parentName, h.filePath, ctx, fileLanguage);
const childId = symbolTable.lookupExact(h.filePath, h.className) ||
resolveSymbol(h.className, h.filePath, symbolTable, importMap, packageMap)?.nodeId ||
generateId('Class', `${h.filePath}:${h.className}`);
const parentId = resolveSymbol(h.parentName, h.filePath, symbolTable, importMap, packageMap)?.nodeId ||
generateId(idPrefix, `${h.parentName}`);
const childId = resolveHeritageId(h.className, h.filePath, ctx, 'Class', `${h.filePath}:${h.className}`);
const parentId = resolveHeritageId(h.parentName, h.filePath, ctx, idPrefix);
if (childId && parentId && childId !== parentId) {
graph.addRelationship({
@ -281,12 +270,8 @@ export const processHeritageFromExtracted = async (
});
}
} else if (h.kind === 'implements') {
const classId = symbolTable.lookupExact(h.filePath, h.className) ||
resolveSymbol(h.className, h.filePath, symbolTable, importMap, packageMap)?.nodeId ||
generateId('Class', `${h.filePath}:${h.className}`);
const interfaceId = resolveSymbol(h.parentName, h.filePath, symbolTable, importMap, packageMap)?.nodeId ||
generateId('Interface', `${h.parentName}`);
const classId = resolveHeritageId(h.className, h.filePath, ctx, 'Class', `${h.filePath}:${h.className}`);
const interfaceId = resolveHeritageId(h.parentName, h.filePath, ctx, 'Interface');
if (classId && interfaceId) {
graph.addRelationship({
@ -299,12 +284,8 @@ export const processHeritageFromExtracted = async (
});
}
} else if (h.kind === 'trait-impl' || h.kind === 'include' || h.kind === 'extend' || h.kind === 'prepend') {
const structId = symbolTable.lookupExact(h.filePath, h.className) ||
resolveSymbol(h.className, h.filePath, symbolTable, importMap, packageMap)?.nodeId ||
generateId('Struct', `${h.filePath}:${h.className}`);
const traitId = resolveSymbol(h.parentName, h.filePath, symbolTable, importMap, packageMap)?.nodeId ||
generateId('Trait', `${h.parentName}`);
const structId = resolveHeritageId(h.className, h.filePath, ctx, 'Struct', `${h.filePath}:${h.className}`);
const traitId = resolveHeritageId(h.parentName, h.filePath, ctx, 'Trait');
if (structId && traitId) {
graph.addRelationship({

View file

@ -33,6 +33,7 @@ import {
resolveRubyImport,
} from './resolvers/index.js';
import { callRouters } from './call-routing.js';
import type { ResolutionContext } from './resolution-context.js';
import type {
SuffixIndex,
TsconfigPaths,
@ -56,15 +57,11 @@ const isDev = process.env.NODE_ENV === 'development';
// Stores all files that a given file imports from
export type ImportMap = Map<string, Set<string>>;
export const createImportMap = (): ImportMap => new Map();
// Type: Map<FilePath, Set<PackageDirSuffix>>
// Stores Go package directory suffixes imported by a file (e.g., "/internal/auth/").
// Avoids expanding every Go package import into N individual ImportMap edges.
export type PackageMap = Map<string, Set<string>>;
export const createPackageMap = (): PackageMap => new Map();
// Type: Map<ImportingFilePath, Map<LocalName, {sourcePath, exportedName}>>
// Tracks which specific names a file imports from which sources (TS/Python only).
// Used to tighten Tier 2a resolution: `import { User } from './models'`
@ -74,8 +71,6 @@ export const createPackageMap = (): PackageMap => new Map();
export interface NamedImportBinding { sourcePath: string; exportedName: string }
export type NamedImportMap = Map<string, Map<string, NamedImportBinding>>;
export const createNamedImportMap = (): NamedImportMap => new Map();
/**
* Check if a file path is directly inside a package directory identified by its suffix.
* Used by the symbol resolver for Go and C# directory-level import matching.
@ -309,13 +304,14 @@ export const processImports = async (
graph: KnowledgeGraph,
files: { path: string; content: string }[],
astCache: ASTCache,
importMap: ImportMap,
ctx: ResolutionContext,
onProgress?: (current: number, total: number) => void,
repoRoot?: string,
allPaths?: string[],
packageMap?: PackageMap,
namedImportMap?: NamedImportMap,
) => {
const importMap = ctx.importMap;
const packageMap = ctx.packageMap;
const namedImportMap = ctx.namedImportMap;
// Use allPaths (full repo) when available for cross-chunk resolution, else fall back to chunk files
const allFileList = allPaths ?? files.map(f => f.path);
const allFilePaths = new Set(allFileList);
@ -341,7 +337,7 @@ export const processImports = async (
swiftPackageConfig: await loadSwiftPackageConfig(effectiveRoot),
csharpConfigs: await loadCSharpProjectConfig(effectiveRoot),
};
const ctx: ResolveCtx = { allFilePaths, allFileList, normalizedFileList, index, resolveCache };
const resolveCtx: ResolveCtx = { allFilePaths, allFileList, normalizedFileList, index, resolveCache };
// Helper: add an IMPORTS edge to the graph only (no ImportMap update)
const addImportGraphEdge = (filePath: string, resolvedPath: string) => {
@ -448,7 +444,7 @@ export const processImports = async (
: sourceNode.text.replace(/['"<>]/g, '');
totalImportsFound++;
const result = resolveLanguageImport(file.path, rawImportPath, language, configs, ctx);
const result = resolveLanguageImport(file.path, rawImportPath, language, configs, resolveCtx);
const bindings = namedImportMap ? extractNamedBindings(captureMap['import'], language) : undefined;
applyImportResult(result, file.path, importMap, packageMap, addImportEdge, addImportGraphEdge, bindings, namedImportMap);
}
@ -461,7 +457,7 @@ export const processImports = async (
const routed = callRouter(callNameNode.text, captureMap['call']);
if (routed && routed.kind === 'import') {
totalImportsFound++;
const result = resolveLanguageImport(file.path, routed.importPath, language, configs, ctx);
const result = resolveLanguageImport(file.path, routed.importPath, language, configs, resolveCtx);
applyImportResult(result, file.path, importMap, packageMap, addImportEdge, addImportGraphEdge);
}
}
@ -492,15 +488,16 @@ export const processImportsFromExtracted = async (
graph: KnowledgeGraph,
files: { path: string }[],
extractedImports: ExtractedImport[],
importMap: ImportMap,
ctx: ResolutionContext,
onProgress?: (current: number, total: number) => void,
repoRoot?: string,
prebuiltCtx?: ImportResolutionContext,
packageMap?: PackageMap,
namedImportMap?: NamedImportMap,
) => {
const ctx = prebuiltCtx ?? buildImportResolutionContext(files.map(f => f.path));
const { allFilePaths, allFileList, normalizedFileList, suffixIndex: index, resolveCache } = ctx;
const importMap = ctx.importMap;
const packageMap = ctx.packageMap;
const namedImportMap = ctx.namedImportMap;
const importCtx = prebuiltCtx ?? buildImportResolutionContext(files.map(f => f.path));
const { allFilePaths, allFileList, normalizedFileList, suffixIndex: index, resolveCache } = importCtx;
let totalImportsFound = 0;
let totalImportsResolved = 0;

View file

@ -9,7 +9,7 @@ import { getLanguageFromFilename, yieldToEventLoop, DEFINITION_CAPTURE_KEYS, get
import { isNodeExported } from './export-detection.js';
import { detectFrameworkFromAST } from './framework-detection.js';
import { WorkerPool } from './workers/worker-pool.js';
import type { ParseWorkerResult, ParseWorkerInput, ExtractedImport, ExtractedCall, ExtractedHeritage, ExtractedRoute } from './workers/parse-worker.js';
import type { ParseWorkerResult, ParseWorkerInput, ExtractedImport, ExtractedCall, ExtractedHeritage, ExtractedRoute, FileConstructorBindings } from './workers/parse-worker.js';
import { getTreeSitterBufferSize, TREE_SITTER_MAX_BUFFER } from './constants.js';
export type FileProgressCallback = (current: number, total: number, filePath: string) => void;
@ -19,6 +19,7 @@ export interface WorkerExtractedData {
calls: ExtractedCall[];
heritage: ExtractedHeritage[];
routes: ExtractedRoute[];
constructorBindings: FileConstructorBindings[];
}
// isNodeExported imported from ./export-detection.js (shared module)
@ -44,7 +45,7 @@ const processParsingWithWorkers = async (
if (lang) parseableFiles.push({ path: file.path, content: file.content });
}
if (parseableFiles.length === 0) return { imports: [], calls: [], heritage: [], routes: [] };
if (parseableFiles.length === 0) return { imports: [], calls: [], heritage: [], routes: [], constructorBindings: [] };
const total = files.length;
@ -61,6 +62,7 @@ const processParsingWithWorkers = async (
const allCalls: ExtractedCall[] = [];
const allHeritage: ExtractedHeritage[] = [];
const allRoutes: ExtractedRoute[] = [];
const allConstructorBindings: FileConstructorBindings[] = [];
for (const result of chunkResults) {
for (const node of result.nodes) {
graph.addNode({
@ -85,11 +87,12 @@ const processParsingWithWorkers = async (
allCalls.push(...result.calls);
allHeritage.push(...result.heritage);
allRoutes.push(...result.routes);
allConstructorBindings.push(...result.constructorBindings);
}
// Final progress
onFileProgress?.(total, total, 'done');
return { imports: allImports, calls: allCalls, heritage: allHeritage, routes: allRoutes };
return { imports: allImports, calls: allCalls, heritage: allHeritage, routes: allRoutes, constructorBindings: allConstructorBindings };
};
// ============================================================================

View file

@ -4,9 +4,6 @@ import { processParsing } from './parsing-processor.js';
import {
processImports,
processImportsFromExtracted,
createImportMap,
createPackageMap,
createNamedImportMap,
buildImportResolutionContext
} from './import-processor.js';
import { processCalls, processCallsFromExtracted, processRoutesFromExtracted } from './call-processor.js';
@ -14,7 +11,7 @@ import { processHeritage, processHeritageFromExtracted } from './heritage-proces
import { computeMRO } from './mro-processor.js';
import { processCommunities } from './community-processor.js';
import { processProcesses } from './process-processor.js';
import { createSymbolTable } from './symbol-table.js';
import { createResolutionContext } from './resolution-context.js';
import { createASTCache } from './ast-cache.js';
import { PipelineProgress, PipelineResult } from '../../types/pipeline.js';
import { walkRepositoryPaths, readFileContents } from './filesystem-walker.js';
@ -41,15 +38,13 @@ export const runPipelineFromRepo = async (
onProgress: (progress: PipelineProgress) => void
): Promise<PipelineResult> => {
const graph = createKnowledgeGraph();
const symbolTable = createSymbolTable();
const ctx = createResolutionContext();
const symbolTable = ctx.symbols;
let astCache = createASTCache(AST_CACHE_CAP);
const importMap = createImportMap();
const packageMap = createPackageMap();
const namedImportMap = createNamedImportMap();
const cleanup = () => {
astCache.clear();
symbolTable.clear();
ctx.clear();
};
try {
@ -221,38 +216,69 @@ export const runPipelineFromRepo = async (
workerPool,
);
const chunkBasePercent = 20 + ((filesParsedSoFar / totalParseable) * 62);
if (chunkWorkerData) {
// Imports
await processImportsFromExtracted(graph, allPathObjects, chunkWorkerData.imports, importMap, undefined, repoPath, importCtx, packageMap, namedImportMap);
await processImportsFromExtracted(graph, allPathObjects, chunkWorkerData.imports, ctx, (current, total) => {
onProgress({
phase: 'parsing',
percent: Math.round(chunkBasePercent),
message: `Resolving imports (chunk ${chunkIdx + 1}/${numChunks})...`,
detail: `${current}/${total} files`,
stats: { filesProcessed: filesParsedSoFar, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
});
}, repoPath, importCtx);
// Calls + Heritage + Routes — resolve in parallel (no shared mutable state between them)
// This is safe because each writes disjoint relationship types into idempotent id-keyed Maps,
// and the single-threaded event loop prevents races between synchronous addRelationship calls.
await Promise.all([
processCallsFromExtracted(
graph,
chunkWorkerData.calls,
symbolTable, importMap,
packageMap,
undefined,
namedImportMap
graph,
chunkWorkerData.calls,
ctx,
(current, total) => {
onProgress({
phase: 'parsing',
percent: Math.round(chunkBasePercent),
message: `Resolving calls (chunk ${chunkIdx + 1}/${numChunks})...`,
detail: `${current}/${total} files`,
stats: { filesProcessed: filesParsedSoFar, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
});
},
chunkWorkerData.constructorBindings,
),
processHeritageFromExtracted(
graph,
chunkWorkerData.heritage,
symbolTable,
importMap,
packageMap
graph,
chunkWorkerData.heritage,
ctx,
(current, total) => {
onProgress({
phase: 'parsing',
percent: Math.round(chunkBasePercent),
message: `Resolving heritage (chunk ${chunkIdx + 1}/${numChunks})...`,
detail: `${current}/${total} records`,
stats: { filesProcessed: filesParsedSoFar, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
});
},
),
processRoutesFromExtracted(
graph,
chunkWorkerData.routes ?? [],
symbolTable,
importMap,
packageMap
graph,
chunkWorkerData.routes ?? [],
ctx,
(current, total) => {
onProgress({
phase: 'parsing',
percent: Math.round(chunkBasePercent),
message: `Resolving routes (chunk ${chunkIdx + 1}/${numChunks})...`,
detail: `${current}/${total} routes`,
stats: { filesProcessed: filesParsedSoFar, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
});
},
),
]);
} else {
await processImports(graph, chunkFiles, astCache, importMap, undefined, repoPath, allPaths, packageMap, namedImportMap);
await processImports(graph, chunkFiles, astCache, ctx, undefined, repoPath, allPaths);
sequentialChunkPaths.push(chunkPaths);
}
@ -273,14 +299,22 @@ export const runPipelineFromRepo = async (
.filter(p => chunkContents.has(p))
.map(p => ({ path: p, content: chunkContents.get(p)! }));
astCache = createASTCache(chunkFiles.length);
const rubyHeritage = await processCalls(graph, chunkFiles, astCache, symbolTable, importMap, packageMap, undefined, namedImportMap);
await processHeritage(graph, chunkFiles, astCache, symbolTable, importMap, packageMap);
const rubyHeritage = await processCalls(graph, chunkFiles, astCache, ctx);
await processHeritage(graph, chunkFiles, astCache, ctx);
if (rubyHeritage.length > 0) {
await processHeritageFromExtracted(graph, rubyHeritage, symbolTable, importMap, packageMap);
await processHeritageFromExtracted(graph, rubyHeritage, ctx);
}
astCache.clear();
}
// Log resolution cache stats
if (isDev) {
const rcStats = ctx.getStats();
const total = rcStats.cacheHits + rcStats.cacheMisses;
const hitRate = total > 0 ? ((rcStats.cacheHits / total) * 100).toFixed(1) : '0';
console.log(`🔍 Resolution cache: ${rcStats.cacheHits} hits, ${rcStats.cacheMisses} misses (${hitRate}% hit rate)`);
}
// Free import resolution context — suffix index + resolve cache no longer needed
// (allPathObjects and importCtx hold ~94MB+ for large repos)
allPathObjects.length = 0;

View file

@ -0,0 +1,192 @@
/**
* Resolution Context
*
* Single implementation of tiered name resolution. Replaces the duplicated
* tier-selection logic previously split between symbol-resolver.ts and
* call-processor.ts.
*
* Resolution tiers (highest confidence first):
* 1. Same file (lookupExactFull — authoritative)
* 2a-named. Named binding chain (walkBindingChain via NamedImportMap)
* 2a. Import-scoped (lookupFuzzy filtered by ImportMap)
* 2b. Package-scoped (lookupFuzzy filtered by PackageMap)
* 3. Global (all candidates — consumers must check candidate count)
*/
import type { SymbolTable, SymbolDefinition } from './symbol-table.js';
import { createSymbolTable } from './symbol-table.js';
import type { NamedImportBinding } from './import-processor.js';
import { isFileInPackageDir } from './import-processor.js';
import { walkBindingChain } from './named-binding-extraction.js';
/** Resolution tier for tracking, logging, and test assertions. */
export type ResolutionTier = 'same-file' | 'import-scoped' | 'global';
/** Tier-selected candidates with metadata. */
export interface TieredCandidates {
readonly candidates: readonly SymbolDefinition[];
readonly tier: ResolutionTier;
}
/** Confidence scores per resolution tier. */
export const TIER_CONFIDENCE: Record<ResolutionTier, number> = {
'same-file': 0.95,
'import-scoped': 0.9,
'global': 0.5,
};
// --- Map types ---
export type ImportMap = Map<string, Set<string>>;
export type PackageMap = Map<string, Set<string>>;
export type NamedImportMap = Map<string, Map<string, NamedImportBinding>>;
export interface ResolutionContext {
/**
* The only resolution API. Returns all candidates at the winning tier.
*
* Tier 3 ('global') returns ALL candidates regardless of count —
* consumers must check candidates.length and refuse ambiguous matches.
*/
resolve(name: string, fromFile: string): TieredCandidates | null;
// --- Data access (for pipeline wiring, not resolution) ---
/** Symbol table — used by parsing-processor to populate symbols. */
readonly symbols: SymbolTable;
/** Raw maps — used by import-processor to populate import data. */
readonly importMap: ImportMap;
readonly packageMap: PackageMap;
readonly namedImportMap: NamedImportMap;
// --- Per-file cache lifecycle ---
enableCache(filePath: string): void;
clearCache(): void;
// --- Operational ---
getStats(): { fileCount: number; globalSymbolCount: number; cacheHits: number; cacheMisses: number };
clear(): void;
}
export const createResolutionContext = (): ResolutionContext => {
const symbols = createSymbolTable();
const importMap: ImportMap = new Map();
const packageMap: PackageMap = new Map();
const namedImportMap: NamedImportMap = new Map();
// Per-file cache state
let cacheFile: string | null = null;
let cache: Map<string, TieredCandidates | null> | null = null;
let cacheHits = 0;
let cacheMisses = 0;
// --- Core resolution (single implementation of tier logic) ---
const resolveUncached = (name: string, fromFile: string): TieredCandidates | null => {
// Tier 1: Same file — authoritative match
const localDef = symbols.lookupExactFull(fromFile, name);
if (localDef) {
return { candidates: [localDef], tier: 'same-file' };
}
// Get all global definitions for subsequent tiers
const allDefs = symbols.lookupFuzzy(name);
// Tier 2a-named: Check named bindings BEFORE empty-allDefs early return
// because aliased imports mean lookupFuzzy('U') returns empty but we
// can resolve via the exported name.
const chainResult = walkBindingChain(name, fromFile, symbols, namedImportMap, allDefs);
if (chainResult && chainResult.length > 0) {
return { candidates: chainResult, tier: 'import-scoped' };
}
if (allDefs.length === 0) return null;
// Tier 2a: Import-scoped — definition in a file imported by fromFile
const importedFiles = importMap.get(fromFile);
if (importedFiles) {
const importedDefs = allDefs.filter(def => importedFiles.has(def.filePath));
if (importedDefs.length > 0) {
return { candidates: importedDefs, tier: 'import-scoped' };
}
}
// Tier 2b: Package-scoped — definition in a package dir imported by fromFile
const importedPackages = packageMap.get(fromFile);
if (importedPackages) {
const packageDefs = allDefs.filter(def => {
for (const dirSuffix of importedPackages) {
if (isFileInPackageDir(def.filePath, dirSuffix)) return true;
}
return false;
});
if (packageDefs.length > 0) {
return { candidates: packageDefs, tier: 'import-scoped' };
}
}
// Tier 3: Global — pass all candidates through.
// Consumers must check candidate count and refuse ambiguous matches.
return { candidates: allDefs, tier: 'global' };
};
const resolve = (name: string, fromFile: string): TieredCandidates | null => {
// Check cache (only when enabled AND fromFile matches cached file)
if (cache && cacheFile === fromFile) {
if (cache.has(name)) {
cacheHits++;
return cache.get(name)!;
}
cacheMisses++;
}
const result = resolveUncached(name, fromFile);
// Store in cache if active and file matches
if (cache && cacheFile === fromFile) {
cache.set(name, result);
}
return result;
};
// --- Cache lifecycle ---
const enableCache = (filePath: string): void => {
cacheFile = filePath;
if (!cache) cache = new Map();
else cache.clear();
};
const clearCache = (): void => {
cacheFile = null;
// Reuse the Map instance — just clear entries to reduce GC pressure at scale.
cache?.clear();
};
const getStats = () => ({
...symbols.getStats(),
cacheHits,
cacheMisses,
});
const clear = (): void => {
symbols.clear();
importMap.clear();
packageMap.clear();
namedImportMap.clear();
clearCache();
cacheHits = 0;
cacheMisses = 0;
};
return {
resolve,
symbols,
importMap,
packageMap,
namedImportMap,
enableCache,
clearCache,
getStats,
clear,
};
};

View file

@ -1,123 +0,0 @@
/**
* Symbol Resolver
*
* Import-filtered candidate narrowing for bare identifier resolution.
* NOT FQN resolution — does not parse qualifiers (ns::Bar, com.foo.Bar).
*
* Shared between heritage-processor.ts and call-processor.ts.
*/
import type { SymbolTable, SymbolDefinition } from './symbol-table.js';
import type { ImportMap, PackageMap, NamedImportMap } from './import-processor.js';
import { isFileInPackageDir } from './import-processor.js';
import { walkBindingChain } from './named-binding-extraction.js';
/** Resolution tier for internal tracking, logging, and test assertions. */
export type ResolutionTier = 'same-file' | 'import-scoped' | 'unique-global';
/** Internal resolution result preserving tier metadata. */
export interface InternalResolution {
definition: SymbolDefinition;
tier: ResolutionTier;
candidateCount: number;
}
/**
* Resolve a bare identifier to its best-matching definition using import context.
*
* Resolution tiers (highest confidence first):
* 1. Same file (lookupExactFull — authoritative)
* 2. Import-scoped (lookupFuzzy filtered by importMap — acceptable)
* 3. Unique global (lookupFuzzy with exactly 1 match — acceptable fallback)
*
* If multiple global candidates remain after filtering, returns null.
* A wrong edge is worse than no edge.
*/
export const resolveSymbol = (
name: string,
currentFilePath: string,
symbolTable: SymbolTable,
importMap: ImportMap,
packageMap?: PackageMap,
namedImportMap?: NamedImportMap,
): SymbolDefinition | null => {
return resolveSymbolInternal(name, currentFilePath, symbolTable, importMap, packageMap, namedImportMap)?.definition ?? null;
};
/** Internal resolver preserving tier metadata for logging and test assertions. */
export const resolveSymbolInternal = (
name: string,
currentFilePath: string,
symbolTable: SymbolTable,
importMap: ImportMap,
packageMap?: PackageMap,
namedImportMap?: NamedImportMap,
): InternalResolution | null => {
// Tier 1: Same file — authoritative match
const localDef = symbolTable.lookupExactFull(currentFilePath, name);
if (localDef) return { definition: localDef, tier: 'same-file', candidateCount: 1 };
// Get all global definitions for subsequent tiers
const allDefs = symbolTable.lookupFuzzy(name);
// Tier 2a-named: Check named bindings BEFORE the empty-allDefs early return,
// because aliased imports (import { User as U }) mean lookupFuzzy('U') returns
// empty but we can resolve via the exported name.
if (namedImportMap) {
const result = resolveNamedBindingChain(name, currentFilePath, symbolTable, namedImportMap, allDefs);
if (result) return result;
}
if (allDefs.length === 0) return null;
// Tier 2a: Import-scoped — check if any definition is in a file imported by currentFile
const importedFiles = importMap.get(currentFilePath);
if (importedFiles) {
for (const def of allDefs) {
if (importedFiles.has(def.filePath)) {
return { definition: def, tier: 'import-scoped', candidateCount: allDefs.length };
}
}
}
// Tier 2b: Package-scoped — check if any definition is in a package/namespace dir imported by currentFile
// Used for Go packages and C# namespace imports to avoid ImportMap expansion bloat
const importedPackages = packageMap?.get(currentFilePath);
if (importedPackages) {
for (const def of allDefs) {
for (const dirSuffix of importedPackages) {
if (isFileInPackageDir(def.filePath, dirSuffix)) {
return { definition: def, tier: 'import-scoped', candidateCount: allDefs.length };
}
}
}
}
// Tier 3: Unique global — ONLY if exactly one candidate exists
// Ambiguous global matches are refused. A wrong edge is worse than no edge.
if (allDefs.length === 1) {
return { definition: allDefs[0], tier: 'unique-global', candidateCount: 1 };
}
// Ambiguous: multiple global candidates, no import or same-file match → refuse
return null;
};
/**
* Follow re-export chains through NamedImportMap.
* Delegates chain-walking to the shared walkBindingChain utility, then
* applies symbol-resolver semantics: exactly one match required.
*/
const resolveNamedBindingChain = (
name: string,
currentFilePath: string,
symbolTable: SymbolTable,
namedImportMap: NamedImportMap,
allDefs: SymbolDefinition[],
): InternalResolution | null => {
const defs = walkBindingChain(name, currentFilePath, symbolTable, namedImportMap, allDefs);
if (defs?.length === 1) {
return { definition: defs[0], tier: 'import-scoped', candidateCount: defs.length };
}
return null;
};

View file

@ -306,7 +306,7 @@ export const CPP_QUERIES = `
(field_declaration_list
(function_definition
declarator: (function_declarator
declarator: [(field_identifier) (identifier) (operator_name) (destructor_name)] @name))) @definition.method
declarator: [(field_identifier) (identifier) (operator_name) (destructor_name)] @name)) @definition.method)
; Templates
(template_declaration (class_specifier name: (type_identifier) @name)) @definition.template

View file

@ -1,7 +1,10 @@
import type { SyntaxNode } from './utils.js';
import { FUNCTION_NODE_TYPES, extractFunctionName } from './utils.js';
import { FUNCTION_NODE_TYPES, extractFunctionName, CLASS_CONTAINER_TYPES } from './utils.js';
import { SupportedLanguages } from '../../config/supported-languages.js';
import { typeConfigs, TYPED_PARAMETER_TYPES } from './type-extractors/index.js';
import type { ClassNameLookup } from './type-extractors/types.js';
import { extractSimpleTypeName } from './type-extractors/shared.js';
import type { SymbolTable } from './symbol-table.js';
/**
* Per-file scoped type environment: maps (scope, variableName) → typeName.
@ -19,15 +22,47 @@ export type TypeEnv = Map<string, Map<string, string>>;
/** File-level scope key */
const FILE_SCOPE = '';
/** Fallback for languages where class names aren't in a 'name' field (e.g. Kotlin uses type_identifier). */
const findTypeIdentifierChild = (node: SyntaxNode): SyntaxNode | null => {
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child && child.type === 'type_identifier') return child;
}
return null;
};
/**
* Look up a variable's type in the TypeEnv, trying the call's enclosing
* function scope first, then falling back to file-level scope.
* Per-file type environment with receiver resolution.
* Built once per file via `buildTypeEnv`, used for receiver-type filtering,
* then discarded. Encapsulates scope-aware type lookup and self/this/super
* AST resolution behind a single `.lookup()` method.
*/
export const lookupTypeEnv = (
export interface TypeEnvironment {
/** Look up a variable's resolved type, with self/this/super AST resolution. */
lookup(varName: string, callNode: SyntaxNode): string | undefined;
/** Unverified cross-file constructor bindings for SymbolTable verification. */
readonly constructorBindings: readonly ConstructorBinding[];
/** Raw per-scope type bindings — for testing and debugging. */
readonly env: TypeEnv;
}
/** Implementation of the lookup logic — shared between TypeEnvironment and the legacy export. */
const lookupInEnv = (
env: TypeEnv,
varName: string,
callNode: SyntaxNode,
): string | undefined => {
// Self/this receiver: resolve to enclosing class name via AST walk
if (varName === 'self' || varName === 'this' || varName === '$this') {
return findEnclosingClassName(callNode);
}
// Super/base/parent receiver: resolve to the parent class name via AST walk.
// Walks up to the enclosing class, then extracts the superclass from its heritage node.
if (varName === 'super' || varName === 'base' || varName === 'parent') {
return findEnclosingParentClassName(callNode);
}
// Determine the enclosing function scope for the call
const scopeKey = findEnclosingScopeKey(callNode);
@ -45,6 +80,154 @@ export const lookupTypeEnv = (
return fileEnv?.get(varName);
};
/**
* Walk up the AST from a node to find the enclosing class/module name.
* Used to resolve `self`/`this` receivers to their containing type.
*/
const findEnclosingClassName = (node: SyntaxNode): string | undefined => {
let current = node.parent;
while (current) {
if (CLASS_CONTAINER_TYPES.has(current.type)) {
const nameNode = current.childForFieldName('name')
?? findTypeIdentifierChild(current);
if (nameNode) return nameNode.text;
}
current = current.parent;
}
return undefined;
};
/**
* Walk up the AST to find the enclosing class, then extract its parent class name
* from the heritage/superclass AST node. Used to resolve `super`/`base`/`parent`.
*
* Supported patterns per tree-sitter grammar:
* - Java/Ruby: `superclass` field → type_identifier/constant
* - Python: `superclasses` field → argument_list → first identifier
* - TypeScript/JS: unnamed `class_heritage` child → `extends_clause` → identifier
* - C#: unnamed `base_list` child → first identifier
* - PHP: unnamed `base_clause` child → name
* - Kotlin: unnamed `delegation_specifier` child → constructor_invocation → user_type → type_identifier
* - C++: unnamed `base_class_clause` child → type_identifier
* - Swift: unnamed `inheritance_specifier` child → user_type → type_identifier
*/
const findEnclosingParentClassName = (node: SyntaxNode): string | undefined => {
let current = node.parent;
while (current) {
if (CLASS_CONTAINER_TYPES.has(current.type)) {
return extractParentClassFromNode(current);
}
current = current.parent;
}
return undefined;
};
/** Extract the parent/superclass name from a class declaration AST node. */
const extractParentClassFromNode = (classNode: SyntaxNode): string | undefined => {
// 1. Named fields: Java (superclass), Ruby (superclass), Python (superclasses)
const superclassNode = classNode.childForFieldName('superclass');
if (superclassNode) {
// Java: superclass > type_identifier or generic_type, Ruby: superclass > constant
const inner = superclassNode.childForFieldName('type')
?? superclassNode.firstNamedChild
?? superclassNode;
return extractSimpleTypeName(inner) ?? inner.text;
}
const superclassesNode = classNode.childForFieldName('superclasses');
if (superclassesNode) {
// Python: argument_list with identifiers or attribute nodes (e.g. models.Model)
const first = superclassesNode.firstNamedChild;
if (first) return extractSimpleTypeName(first) ?? first.text;
}
// 2. Unnamed children: walk class node's children looking for heritage nodes
for (let i = 0; i < classNode.childCount; i++) {
const child = classNode.child(i);
if (!child) continue;
switch (child.type) {
// TypeScript: class_heritage > extends_clause > type_identifier
// JavaScript: class_heritage > identifier (no extends_clause wrapper)
case 'class_heritage': {
for (let j = 0; j < child.childCount; j++) {
const clause = child.child(j);
if (clause?.type === 'extends_clause') {
const typeNode = clause.firstNamedChild;
if (typeNode) return extractSimpleTypeName(typeNode) ?? typeNode.text;
}
// JS: direct identifier child (no extends_clause wrapper)
if (clause?.type === 'identifier' || clause?.type === 'type_identifier') {
return clause.text;
}
}
break;
}
// C#: base_list > identifier or generic_name > identifier
case 'base_list': {
const first = child.firstNamedChild;
if (first) {
// generic_name wraps the identifier: BaseClass<T>
if (first.type === 'generic_name') {
const inner = first.childForFieldName('name') ?? first.firstNamedChild;
if (inner) return inner.text;
}
return first.text;
}
break;
}
// PHP: base_clause > name
case 'base_clause': {
const name = child.firstNamedChild;
if (name) return name.text;
break;
}
// C++: base_class_clause > type_identifier (with optional access_specifier before it)
case 'base_class_clause': {
for (let j = 0; j < child.childCount; j++) {
const inner = child.child(j);
if (inner?.type === 'type_identifier') return inner.text;
}
break;
}
// Kotlin: delegation_specifier > constructor_invocation > user_type > type_identifier
case 'delegation_specifier': {
const delegate = child.firstNamedChild;
if (delegate?.type === 'constructor_invocation') {
const userType = delegate.firstNamedChild;
if (userType?.type === 'user_type') {
const typeId = userType.firstNamedChild;
if (typeId) return typeId.text;
}
}
// Also handle plain user_type (interface conformance without parentheses)
if (delegate?.type === 'user_type') {
const typeId = delegate.firstNamedChild;
if (typeId) return typeId.text;
}
break;
}
// Swift: inheritance_specifier > user_type > type_identifier
case 'inheritance_specifier': {
const userType = child.childForFieldName('inherits_from') ?? child.firstNamedChild;
if (userType?.type === 'user_type') {
const typeId = userType.firstNamedChild;
if (typeId) return typeId.text;
}
break;
}
}
}
return undefined;
};
/** Find the enclosing function name for scope lookup. */
const findEnclosingScopeKey = (node: SyntaxNode): string | undefined => {
let current = node.parent;
@ -58,67 +241,289 @@ const findEnclosingScopeKey = (node: SyntaxNode): string | undefined => {
return undefined;
};
/**
* Create a lookup that checks both local AST class names AND the SymbolTable's
* global index. This allows extractInitializer functions to distinguish
* constructor calls from function calls (e.g. Kotlin `User()` vs `getUser()`)
* using cross-file type information when available.
*
* Only `.has()` is exposed — the SymbolTable doesn't support iteration.
* Results are memoized to avoid redundant lookupFuzzy scans across declarations.
*/
const createClassNameLookup = (
localNames: Set<string>,
symbolTable?: SymbolTable,
): ClassNameLookup => {
if (!symbolTable) return localNames;
const memo = new Map<string, boolean>();
return {
has(name: string): boolean {
if (localNames.has(name)) return true;
const cached = memo.get(name);
if (cached !== undefined) return cached;
const result = symbolTable.lookupFuzzy(name).some(def => def.type === 'Class');
memo.set(name, result);
return result;
},
};
};
/**
* Build a scoped TypeEnv from a tree-sitter AST for a given language.
* Walks the tree tracking enclosing function scopes, so that variables
* inside different functions don't collide.
* Single-pass: collects class/struct names AND type bindings in one walk.
* Class names are accumulated incrementally — this is safe because no
* language allows constructing a class before its definition.
*
* When a symbolTable is provided (call-processor path), class names from across
* the project are available for constructor inference in languages like Kotlin
* where constructors are syntactically identical to function calls.
*/
/**
* Build a TypeEnvironment from a tree-sitter AST for a given language.
* Single-pass: collects class/struct names, type bindings, AND constructor
* bindings that couldn't be resolved locally — all in one AST walk.
*/
export const buildTypeEnv = (
tree: { rootNode: SyntaxNode },
language: SupportedLanguages,
): TypeEnv => {
symbolTable?: SymbolTable,
): TypeEnvironment => {
const env: TypeEnv = new Map();
walkForTypes(tree.rootNode, language, env, FILE_SCOPE);
return env;
};
const localClassNames = new Set<string>();
const classNames = createClassNameLookup(localClassNames, symbolTable);
const config = typeConfigs[language];
const scanner = CONSTRUCTOR_BINDING_SCANNERS[language];
const bindings: ConstructorBinding[] = [];
const walkForTypes = (
node: SyntaxNode,
language: SupportedLanguages,
env: TypeEnv,
currentScope: string,
): void => {
// Detect scope boundaries (function/method definitions)
let scope = currentScope;
if (FUNCTION_NODE_TYPES.has(node.type)) {
const { funcName } = extractFunctionName(node);
if (funcName) scope = `${funcName}@${node.startIndex}`;
}
/**
* Try to extract a (variableName → typeName) binding from a single AST node.
*
* Resolution tiers (first match wins):
* - Tier 0: explicit type annotations via extractDeclaration
* - Tier 1: constructor-call inference via extractInitializer (fallback)
*/
const extractTypeBinding = (node: SyntaxNode, scopeEnv: Map<string, string>): void => {
// This guard eliminates 90%+ of calls before any language dispatch.
if (TYPED_PARAMETER_TYPES.has(node.type)) {
config.extractParameter(node, scopeEnv);
return;
}
if (config.declarationNodeTypes.has(node.type)) {
config.extractDeclaration(node, scopeEnv);
// Tier 1: constructor-call inference as fallback.
// Always called when available — each language's extractInitializer
// internally skips declarators that already have explicit annotations,
// so this handles mixed cases like `const a: A = x, b = new B()`.
if (config.extractInitializer) {
config.extractInitializer(node, scopeEnv, classNames);
}
}
};
// Get or create the sub-map for this scope
if (!env.has(scope)) env.set(scope, new Map());
const scopeEnv = env.get(scope)!;
const walk = (node: SyntaxNode, currentScope: string): void => {
// Collect class/struct names as we encounter them (used by extractInitializer
// to distinguish constructor calls from function calls, e.g. C++ `User()` vs `getUser()`)
// Currently only C++ uses this locally; other languages rely on the SymbolTable path.
if (CLASS_CONTAINER_TYPES.has(node.type)) {
// Most languages use 'name' field; Kotlin uses a type_identifier child instead
const nameNode = node.childForFieldName('name')
?? findTypeIdentifierChild(node);
if (nameNode) localClassNames.add(nameNode.text);
}
// Check if this node provides type information
extractTypeBinding(node, language, scopeEnv);
// Detect scope boundaries (function/method definitions)
let scope = currentScope;
if (FUNCTION_NODE_TYPES.has(node.type)) {
const { funcName } = extractFunctionName(node);
if (funcName) scope = `${funcName}@${node.startIndex}`;
}
// Recurse into children
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child) walkForTypes(child, language, env, scope);
}
// Get or create the sub-map for this scope
if (!env.has(scope)) env.set(scope, new Map());
const scopeEnv = env.get(scope)!;
extractTypeBinding(node, scopeEnv);
// Scan for constructor bindings that couldn't be resolved locally.
// Only collect if TypeEnv didn't already resolve this binding.
if (scanner) {
const result = scanner(node);
if (result && !scopeEnv.has(result.varName)) {
bindings.push({ scope, ...result });
}
}
// Recurse into children
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child) walk(child, scope);
}
};
walk(tree.rootNode, FILE_SCOPE);
return {
lookup: (varName, callNode) => lookupInEnv(env, varName, callNode),
constructorBindings: bindings,
env,
};
};
/**
* Try to extract a (variableName → typeName) binding from a single AST node.
* Delegates to per-language type configurations.
* Unverified constructor binding: a `val x = Callee()` pattern where we
* couldn't confirm the callee is a class (because it's defined in another file).
* The caller must verify `calleeName` against the SymbolTable before trusting.
*/
const extractTypeBinding = (
node: SyntaxNode,
language: SupportedLanguages,
env: Map<string, string>,
): void => {
// === PARAMETERS (most languages) ===
// This guard eliminates 90%+ of calls before any language dispatch.
if (TYPED_PARAMETER_TYPES.has(node.type)) {
const config = typeConfigs[language];
config.extractParameter(node, env);
return;
}
export interface ConstructorBinding {
/** Function scope key (matches TypeEnv scope keys) */
scope: string;
/** Variable name that received the constructor result */
varName: string;
/** Name of the callee (potential class constructor) */
calleeName: string;
}
// === Per-language declaration extraction ===
const config = typeConfigs[language];
if (config.declarationNodeTypes.has(node.type)) {
config.extractDeclaration(node, env);
/** C/C++: auto x = User() where function is an identifier (not type_identifier) */
const extractCppConstructorBinding = (node: SyntaxNode): { varName: string; calleeName: string } | undefined => {
if (node.type !== 'declaration') return undefined;
const typeNode = node.childForFieldName('type');
if (!typeNode) return undefined;
const typeText = typeNode.text;
if (typeText !== 'auto' && typeText !== 'decltype(auto)' && typeNode.type !== 'placeholder_type_specifier') return undefined;
const declarator = node.childForFieldName('declarator');
if (!declarator || declarator.type !== 'init_declarator') return undefined;
const value = declarator.childForFieldName('value');
if (!value || value.type !== 'call_expression') return undefined;
const func = value.childForFieldName('function');
// Match plain identifiers (type_identifier is already resolved by extractInitializer)
// and qualified/scoped identifiers for namespaced calls like ns::HttpClient()
if (!func) return undefined;
if (func.type === 'qualified_identifier' || func.type === 'scoped_identifier') {
// ns::HttpClient → extract "HttpClient" (last segment)
const last = func.lastNamedChild;
if (!last) return undefined;
const nameNode = declarator.childForFieldName('declarator');
if (!nameNode) return undefined;
const finalName = nameNode.type === 'pointer_declarator' || nameNode.type === 'reference_declarator'
? nameNode.firstNamedChild : nameNode;
if (!finalName) return undefined;
return { varName: finalName.text, calleeName: last.text };
}
if (func.type !== 'identifier') return undefined;
const nameNode = declarator.childForFieldName('declarator');
if (!nameNode) return undefined;
const finalName = nameNode.type === 'pointer_declarator' || nameNode.type === 'reference_declarator'
? nameNode.firstNamedChild : nameNode;
if (!finalName) return undefined;
const varName = finalName.text;
if (!varName) return undefined;
return { varName, calleeName: func.text };
};
/** Ruby: user = User.new — assignment with call where method is 'new' and receiver is a constant */
const extractRubyConstructorBinding = (node: SyntaxNode): { varName: string; calleeName: string } | undefined => {
if (node.type !== 'assignment') return undefined;
const left = node.childForFieldName('left');
const right = node.childForFieldName('right');
if (!left || !right) return undefined;
// Support both local variables (identifier) and constants (USER = User.new)
if (left.type !== 'identifier' && left.type !== 'constant') return undefined;
if (right.type !== 'call') return undefined;
const method = right.childForFieldName('method');
if (!method || method.text !== 'new') return undefined;
const receiver = right.childForFieldName('receiver');
if (!receiver || receiver.type !== 'constant') return undefined;
return { varName: left.text, calleeName: receiver.text };
};
/** Language-specific constructor-binding scanners. */
const CONSTRUCTOR_BINDING_SCANNERS: Partial<Record<SupportedLanguages, (node: SyntaxNode) => { varName: string; calleeName: string } | undefined>> = {
// Kotlin: val x = User(...) — property_declaration with call_expression
[SupportedLanguages.Kotlin]: (node) => {
if (node.type !== 'property_declaration') return undefined;
const varDecl = node.namedChildren.find(c => c.type === 'variable_declaration');
if (!varDecl) return undefined;
if (varDecl.namedChildren.some(c => c.type === 'user_type')) return undefined;
const callExpr = node.namedChildren.find(c => c.type === 'call_expression');
if (!callExpr) return undefined;
const callee = callExpr.firstNamedChild;
if (!callee || callee.type !== 'simple_identifier') return undefined;
const nameNode = varDecl.namedChildren.find(c => c.type === 'simple_identifier');
if (!nameNode) return undefined;
return { varName: nameNode.text, calleeName: callee.text };
},
// Python: user = User("alice") — assignment with call
// Also handles walrus operator: (user := User("alice"))
[SupportedLanguages.Python]: (node) => {
let left: SyntaxNode | null;
let right: SyntaxNode | null;
if (node.type === 'named_expression') {
// Walrus operator: (user := User("alice"))
left = node.childForFieldName('name');
right = node.childForFieldName('value');
} else if (node.type === 'assignment') {
left = node.childForFieldName('left');
right = node.childForFieldName('right');
// Skip annotated assignments — extractDeclaration handles those
if (node.childForFieldName('type')) return undefined;
} else {
return undefined;
}
if (!left || !right) return undefined;
if (left.type !== 'identifier') return undefined;
if (right.type !== 'call') return undefined;
const func = right.childForFieldName('function');
if (!func) return undefined;
// Support both direct calls (User()) and qualified calls (models.User())
const calleeName = extractSimpleTypeName(func);
if (!calleeName) return undefined;
return { varName: left.text, calleeName };
},
// Swift: let user = User(name: "alice") — property_declaration with call_expression
[SupportedLanguages.Swift]: (node) => {
if (node.type !== 'property_declaration') return undefined;
// Skip if has type annotation
if (node.childForFieldName('type')) return undefined;
for (let i = 0; i < node.namedChildCount; i++) {
if (node.namedChild(i)?.type === 'type_annotation') return undefined;
}
const pattern = node.childForFieldName('pattern');
if (!pattern) return undefined;
const varName = pattern.text;
if (!varName) return undefined;
// Find call_expression child
let callExpr: SyntaxNode | null = null;
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child?.type === 'call_expression') { callExpr = child; break; }
}
if (!callExpr) return undefined;
const callee = callExpr.firstNamedChild;
if (!callee) return undefined;
// Direct call: User(name: "alice") — simple_identifier callee
if (callee.type === 'simple_identifier') {
return { varName, calleeName: callee.text };
}
// Explicit init: User.init(name: "alice") — navigation_expression with .init suffix
if (callee.type === 'navigation_expression') {
const receiver = callee.firstNamedChild;
const suffix = callee.lastNamedChild;
if (receiver?.type === 'simple_identifier' && suffix?.text === 'init') {
return { varName, calleeName: receiver.text };
}
}
return undefined;
},
// C++: auto x = User() where User is parsed as identifier (cross-file)
// Note: C is excluded — C has no constructors and `auto` is a storage-class specifier, not type inference.
[SupportedLanguages.CPlusPlus]: extractCppConstructorBinding,
// Ruby: user = User.new — assignment with call where method is 'new' and receiver is a constant
[SupportedLanguages.Ruby]: extractRubyConstructorBinding,
};

View file

@ -1,9 +1,10 @@
import type { SyntaxNode } from '../utils.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor } from './types.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup } from './types.js';
import { extractSimpleTypeName, extractVarName } from './shared.js';
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
'declaration',
'for_range_loop',
]);
/** C++: Type x = ...; Type* x; Type& x; */
@ -32,6 +33,75 @@ const extractDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map<str
if (varName) env.set(varName, typeName);
};
/** C++: auto x = new User(); auto x = User(); */
const extractInitializer: InitializerExtractor = (node: SyntaxNode, env: Map<string, string>, classNames: ClassNameLookup): void => {
const typeNode = node.childForFieldName('type');
if (!typeNode) return;
// Only handle auto/placeholder — typed declarations are handled by extractDeclaration
const typeText = typeNode.text;
if (
typeText !== 'auto' &&
typeText !== 'decltype(auto)' &&
typeNode.type !== 'placeholder_type_specifier'
) return;
const declarator = node.childForFieldName('declarator');
if (!declarator) return;
// Must be an init_declarator (i.e., has an initializer value)
if (declarator.type !== 'init_declarator') return;
const value = declarator.childForFieldName('value');
if (!value) return;
// Resolve the variable name, unwrapping pointer/reference declarators
const nameNode = declarator.childForFieldName('declarator');
if (!nameNode) return;
const finalName =
nameNode.type === 'pointer_declarator' || nameNode.type === 'reference_declarator'
? nameNode.firstNamedChild
: nameNode;
if (!finalName) return;
const varName = extractVarName(finalName);
if (!varName) return;
// auto x = new User() — new_expression
if (value.type === 'new_expression') {
const ctorType = value.childForFieldName('type');
if (ctorType) {
const typeName = extractSimpleTypeName(ctorType);
if (typeName) env.set(varName, typeName);
}
return;
}
// auto x = User() — call_expression where function is a type name
// tree-sitter-cpp may parse the constructor name as type_identifier or identifier.
// For plain identifiers, verify against known class names from the file's AST
// to distinguish constructor calls (User()) from function calls (getUser()).
if (value.type === 'call_expression') {
const func = value.childForFieldName('function');
if (!func) return;
if (func.type === 'type_identifier') {
const typeName = func.text;
if (typeName) env.set(varName, typeName);
} else if (func.type === 'identifier') {
const text = func.text;
if (text && classNames.has(text)) env.set(varName, text);
}
return;
}
// auto x = User{} — compound_literal_expression (brace initialization)
// AST: compound_literal_expression > type_identifier + initializer_list
if (value.type === 'compound_literal_expression') {
const typeId = value.firstNamedChild;
const typeName = typeId ? extractSimpleTypeName(typeId) : undefined;
if (typeName) env.set(varName, typeName);
}
};
/** C/C++: parameter_declaration → type declarator */
const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string, string>): void => {
let nameNode: SyntaxNode | null = null;
@ -60,4 +130,5 @@ export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: DECLARATION_NODE_TYPES,
extractDeclaration,
extractParameter,
extractInitializer,
};

View file

@ -6,10 +6,26 @@ const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
'local_declaration_statement',
'variable_declaration',
'field_declaration',
'is_pattern_expression',
]);
/** C#: Type x = ...; var x = new Type(); */
/** C#: Type x = ...; var x = new Type(); obj is Type x */
const extractDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map<string, string>): void => {
// C# pattern matching: `obj is User user` → is_pattern_expression > declaration_pattern
if (node.type === 'is_pattern_expression') {
const pattern = node.childForFieldName('pattern');
if (pattern?.type === 'declaration_pattern') {
const typeNode = pattern.childForFieldName('type');
const nameNode = pattern.childForFieldName('name');
if (typeNode && nameNode) {
const typeName = extractSimpleTypeName(typeNode);
const varName = extractVarName(nameNode);
if (typeName && varName) env.set(varName, typeName);
}
}
return;
}
// C# tree-sitter: local_declaration_statement > variable_declaration > ...
// Recursively descend through wrapper nodes
for (let i = 0; i < node.namedChildCount; i++) {
@ -44,7 +60,8 @@ const extractDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map<str
let typeName: string | undefined;
if (typeNode.type === 'implicit_type' && typeNode.text === 'var') {
// Try to infer from initializer: var x = new Foo()
// C# tree-sitter puts object_creation_expression as direct child of variable_declarator
// tree-sitter-c-sharp may put object_creation_expression as direct child
// or inside equals_value_clause depending on grammar version
if (declarators.length === 1) {
const initializer = findChildByType(declarators[0], 'object_creation_expression')
?? findChildByType(declarators[0], 'equals_value_clause')?.firstNamedChild;

View file

@ -59,7 +59,51 @@ const extractGoShortVarDeclaration = (node: SyntaxNode, env: Map<string, string>
// Pair each LHS name with its corresponding RHS value
const count = Math.min(lhsNodes.length, rhsNodes.length);
for (let i = 0; i < count; i++) {
const valueNode = rhsNodes[i];
let valueNode = rhsNodes[i];
// Unwrap &User{} — unary_expression (address-of) wrapping composite_literal
if (valueNode.type === 'unary_expression' && valueNode.firstNamedChild?.type === 'composite_literal') {
valueNode = valueNode.firstNamedChild;
}
// Go built-in new(User) — call_expression with 'new' callee and type argument
// Go built-in make([]User, 0) / make(map[string]User) — extract element/value type
if (valueNode.type === 'call_expression') {
const funcNode = valueNode.childForFieldName('function');
if (funcNode?.text === 'new') {
const args = valueNode.childForFieldName('arguments');
if (args?.firstNamedChild) {
const typeName = extractSimpleTypeName(args.firstNamedChild);
const varName = extractVarName(lhsNodes[i]);
if (varName && typeName) env.set(varName, typeName);
}
} else if (funcNode?.text === 'make') {
const args = valueNode.childForFieldName('arguments');
const firstArg = args?.firstNamedChild;
if (firstArg) {
let innerType: SyntaxNode | null = null;
if (firstArg.type === 'slice_type') {
innerType = firstArg.childForFieldName('element');
} else if (firstArg.type === 'map_type') {
innerType = firstArg.childForFieldName('value');
}
if (innerType) {
const typeName = extractSimpleTypeName(innerType);
const varName = extractVarName(lhsNodes[i]);
if (varName && typeName) env.set(varName, typeName);
}
}
}
continue;
}
// Go type assertion: user := iface.(User) — type_assertion_expression with 'type' field
if (valueNode.type === 'type_assertion_expression') {
const typeNode = valueNode.childForFieldName('type');
if (typeNode) {
const typeName = extractSimpleTypeName(typeNode);
const varName = extractVarName(lhsNodes[i]);
if (varName && typeName) env.set(varName, typeName);
}
continue;
}
if (valueNode.type !== 'composite_literal') continue;
const typeNode = valueNode.childForFieldName('type');
if (!typeNode) continue;

View file

@ -33,7 +33,7 @@ export const typeConfigs = {
declarationNodeTypes: new Set<string>(),
extractDeclaration: () => {},
extractParameter: () => {},
},
} as LanguageTypeConfig,
} satisfies Record<SupportedLanguages, LanguageTypeConfig>;
export type { LanguageTypeConfig, TypeBindingExtractor, ParameterExtractor } from './types.js';

View file

@ -1,5 +1,5 @@
import type { SyntaxNode } from '../utils.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor } from './types.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup } from './types.js';
import { extractSimpleTypeName, extractVarName, findChildByType } from './shared.js';
// ── Java ──────────────────────────────────────────────────────────────────
@ -14,7 +14,7 @@ const extractJavaDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map
const typeNode = node.childForFieldName('type');
if (!typeNode) return;
const typeName = extractSimpleTypeName(typeNode);
if (!typeName) return;
if (!typeName || typeName === 'var') return; // skip Java 10 var — handled by extractInitializer
// Find variable_declarator children
for (let i = 0; i < node.namedChildCount; i++) {
@ -28,6 +28,25 @@ const extractJavaDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map
}
};
/** Java 10+: var x = new User() — infer type from object_creation_expression */
const extractJavaInitializer: InitializerExtractor = (node: SyntaxNode, env: Map<string, string>, _classNames: ClassNameLookup): void => {
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child?.type !== 'variable_declarator') continue;
const nameNode = child.childForFieldName('name');
const valueNode = child.childForFieldName('value');
if (!nameNode || !valueNode) continue;
// Skip declarators that already have a binding from extractDeclaration
const varName = extractVarName(nameNode);
if (!varName || env.has(varName)) continue;
if (valueNode.type !== 'object_creation_expression') continue;
const ctorType = valueNode.childForFieldName('type');
if (!ctorType) continue;
const typeName = extractSimpleTypeName(ctorType);
if (typeName) env.set(varName, typeName);
}
};
/** Java: formal_parameter → type name */
const extractJavaParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string, string>): void => {
let nameNode: SyntaxNode | null = null;
@ -52,6 +71,7 @@ export const javaTypeConfig: LanguageTypeConfig = {
declarationNodeTypes: JAVA_DECLARATION_NODE_TYPES,
extractDeclaration: extractJavaDeclaration,
extractParameter: extractJavaParameter,
extractInitializer: extractJavaInitializer,
};
// ── Kotlin ────────────────────────────────────────────────────────────────
@ -115,8 +135,40 @@ const extractKotlinParameter: ParameterExtractor = (node: SyntaxNode, env: Map<s
if (varName && typeName) env.set(varName, typeName);
};
/** Kotlin: val user = User() — infer type from call_expression when callee is a known class.
* Kotlin constructors are syntactically identical to function calls, so we verify
* against classNames (which may include cross-file SymbolTable lookups). */
const extractKotlinInitializer: InitializerExtractor = (node: SyntaxNode, env: Map<string, string>, classNames: ClassNameLookup): void => {
if (node.type !== 'property_declaration') return;
// Skip if there's an explicit type annotation — Tier 0 already handled it
const varDecl = findChildByType(node, 'variable_declaration');
if (varDecl && findChildByType(varDecl, 'user_type')) return;
// Get the initializer value — the call_expression after '='
const value = node.childForFieldName('value')
?? findChildByType(node, 'call_expression');
if (!value || value.type !== 'call_expression') return;
// The callee is the first child of call_expression (simple_identifier for direct calls)
const callee = value.firstNamedChild;
if (!callee || callee.type !== 'simple_identifier') return;
const calleeName = callee.text;
if (!calleeName || !classNames.has(calleeName)) return;
// Extract the variable name from the variable_declaration inside property_declaration
const nameNode = varDecl
? findChildByType(varDecl, 'simple_identifier')
: findChildByType(node, 'simple_identifier');
if (!nameNode) return;
const varName = extractVarName(nameNode);
if (varName) env.set(varName, calleeName);
};
export const kotlinTypeConfig: LanguageTypeConfig = {
declarationNodeTypes: KOTLIN_DECLARATION_NODE_TYPES,
extractDeclaration: extractKotlinDeclaration,
extractParameter: extractKotlinParameter,
extractInitializer: extractKotlinInitializer,
};

View file

@ -1,13 +1,94 @@
import type { SyntaxNode } from '../utils.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor } from './types.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup } from './types.js';
import { extractSimpleTypeName, extractVarName } from './shared.js';
// PHP has no local variable type annotations; only params carry types
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set<string>();
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
'assignment_expression', // For constructor inference: $x = new User()
'property_declaration', // PHP 7.4+ typed properties: private UserRepo $repo;
]);
/** PHP: no typed local variable declarations */
const extractDeclaration: TypeBindingExtractor = (_node: SyntaxNode, _env: Map<string, string>): void => {
// PHP has no local variable type annotations
/** Walk up the AST to find the enclosing class declaration. */
const findEnclosingClass = (node: SyntaxNode): SyntaxNode | null => {
let current = node.parent;
while (current) {
if (current.type === 'class_declaration') return current;
current = current.parent;
}
return null;
};
/**
* Resolve PHP self/static/parent to the actual class name.
* - self/static → enclosing class name
* - parent → superclass from base_clause
*/
const resolvePhpKeyword = (keyword: string, node: SyntaxNode): string | undefined => {
if (keyword === 'self' || keyword === 'static') {
const cls = findEnclosingClass(node);
if (!cls) return undefined;
const nameNode = cls.childForFieldName('name');
return nameNode?.text;
}
if (keyword === 'parent') {
const cls = findEnclosingClass(node);
if (!cls) return undefined;
// base_clause contains the parent class name
for (let i = 0; i < cls.namedChildCount; i++) {
const child = cls.namedChild(i);
if (child?.type === 'base_clause') {
const parentName = child.firstNamedChild;
if (parentName) return extractSimpleTypeName(parentName);
}
}
return undefined;
}
return undefined;
};
/** PHP: typed class properties (PHP 7.4+): private UserRepo $repo; */
const extractDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map<string, string>): void => {
if (node.type !== 'property_declaration') return;
const typeNode = node.childForFieldName('type');
if (!typeNode) return;
const typeName = extractSimpleTypeName(typeNode);
if (!typeName) return;
// The variable name is inside property_element > variable_name
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child?.type === 'property_element') {
const varNameNode = child.firstNamedChild; // variable_name
if (varNameNode) {
const varName = extractVarName(varNameNode);
if (varName) env.set(varName, typeName);
}
break;
}
}
};
/** PHP: $x = new User() — infer type from object_creation_expression */
const extractInitializer: InitializerExtractor = (node: SyntaxNode, env: Map<string, string>, _classNames: ClassNameLookup): void => {
if (node.type !== 'assignment_expression') return;
const left = node.childForFieldName('left');
const right = node.childForFieldName('right');
if (!left || !right) return;
if (right.type !== 'object_creation_expression') return;
// The class name is the first named child of object_creation_expression
// (tree-sitter-php uses 'name' or 'qualified_name' nodes here)
const ctorType = right.firstNamedChild;
if (!ctorType) return;
const typeName = extractSimpleTypeName(ctorType);
if (!typeName) return;
// Resolve PHP self/static/parent to actual class names
const resolvedType = (typeName === 'self' || typeName === 'static' || typeName === 'parent')
? resolvePhpKeyword(typeName, node)
: typeName;
if (!resolvedType) return;
const varName = extractVarName(left);
if (varName) env.set(varName, resolvedType);
};
/** PHP: simple_parameter → type $name */
@ -33,4 +114,5 @@ export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: DECLARATION_NODE_TYPES,
extractDeclaration,
extractParameter,
extractInitializer,
};

View file

@ -1,9 +1,10 @@
import type { SyntaxNode } from '../utils.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor } from './types.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup } from './types.js';
import { extractSimpleTypeName, extractVarName } from './shared.js';
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
'assignment',
'named_expression',
]);
/** Python: x: Foo = ... (PEP 484 annotations) */
@ -37,8 +38,46 @@ const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string,
if (varName && typeName) env.set(varName, typeName);
};
/** Python: user = User("alice") — infer type from call when callee is a known class.
* Python constructors are syntactically identical to function calls, so we verify
* against classNames (which may include cross-file SymbolTable lookups).
* Also handles walrus operator: if (user := User("alice")): */
const extractInitializer: InitializerExtractor = (node: SyntaxNode, env: Map<string, string>, classNames: ClassNameLookup): void => {
let left: SyntaxNode | null;
let right: SyntaxNode | null;
if (node.type === 'named_expression') {
// Walrus operator: (user := User("alice"))
// tree-sitter-python: named_expression has 'name' and 'value' fields
left = node.childForFieldName('name');
right = node.childForFieldName('value');
} else if (node.type === 'assignment') {
left = node.childForFieldName('left');
right = node.childForFieldName('right');
// Skip if already has type annotation — extractDeclaration handled it
if (node.childForFieldName('type')) return;
} else {
return;
}
if (!left || !right) return;
const varName = extractVarName(left);
if (!varName || env.has(varName)) return;
if (right.type !== 'call') return;
const func = right.childForFieldName('function');
if (!func) return;
// Support both direct calls (User()) and qualified calls (models.User())
// tree-sitter-python: direct → identifier, qualified → attribute
const calleeName = extractSimpleTypeName(func);
if (!calleeName) return;
if (classNames.has(calleeName)) {
env.set(varName, calleeName);
}
};
export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: DECLARATION_NODE_TYPES,
extractDeclaration,
extractParameter,
extractInitializer,
};

View file

@ -1,13 +1,90 @@
import type { SyntaxNode } from '../utils.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor } from './types.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup } from './types.js';
import { extractSimpleTypeName, extractVarName } from './shared.js';
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
'let_declaration',
'let_condition',
]);
/** Rust: let x: Foo = ... */
/** Walk up the AST to find the enclosing impl block and extract the implementing type name. */
const findEnclosingImplType = (node: SyntaxNode): string | undefined => {
let current = node.parent;
while (current) {
if (current.type === 'impl_item') {
// The 'type' field holds the implementing type (e.g., `impl User { ... }`)
const typeNode = current.childForFieldName('type');
if (typeNode) return extractSimpleTypeName(typeNode);
}
current = current.parent;
}
return undefined;
};
/**
* Extract the type name from a struct_pattern's 'type' field.
* Handles both simple `User { .. }` and scoped `Message::Data { .. }`.
*/
const extractStructPatternType = (structPattern: SyntaxNode): string | undefined => {
const typeNode = structPattern.childForFieldName('type');
if (!typeNode) return undefined;
return extractSimpleTypeName(typeNode);
};
/**
* Recursively scan a pattern tree for captured_pattern nodes (x @ StructType { .. })
* and extract variable → type bindings from them.
*/
const extractCapturedPatternBindings = (pattern: SyntaxNode, env: Map<string, string>): void => {
if (pattern.type === 'captured_pattern') {
// captured_pattern: identifier @ inner_pattern
// The first named child is the identifier, followed by the inner pattern.
const nameNode = pattern.firstNamedChild;
if (!nameNode || nameNode.type !== 'identifier') return;
// Find the struct_pattern child — that gives us the type
for (let i = 0; i < pattern.namedChildCount; i++) {
const child = pattern.namedChild(i);
if (child?.type === 'struct_pattern') {
const typeName = extractStructPatternType(child);
if (typeName) env.set(nameNode.text, typeName);
return;
}
}
return;
}
// Recurse into tuple_struct_pattern children to find nested captured_patterns
// e.g., Some(user @ User { .. })
if (pattern.type === 'tuple_struct_pattern') {
for (let i = 0; i < pattern.namedChildCount; i++) {
const child = pattern.namedChild(i);
if (child) extractCapturedPatternBindings(child, env);
}
}
};
/** Rust: let x: Foo = ... | if let / while let pattern bindings */
const extractDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map<string, string>): void => {
if (node.type === 'let_condition') {
// if let / while let: extract type bindings from pattern matching.
//
// Supported patterns:
// - captured_pattern: `if let user @ User { .. } = expr` → user: User
// - tuple_struct_pattern with nested captured_pattern:
// `if let Some(user @ User { .. }) = expr` → user: User
//
// NOT supported (requires generic unwrapping — Phase 3):
// - `if let Some(x) = opt` where opt: Option<T> → x: T
//
// struct_pattern without capture (`if let User { name } = expr`)
// destructures fields — individual field types are unknown without
// field-type resolution, so no bindings are extracted.
const pattern = node.childForFieldName('pattern');
if (!pattern) return;
extractCapturedPatternBindings(pattern, env);
return;
}
// Standard let_declaration: let x: Foo = ...
const pattern = node.childForFieldName('pattern');
const typeNode = node.childForFieldName('type');
if (!pattern || !typeNode) return;
@ -16,6 +93,46 @@ const extractDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map<str
if (varName && typeName) env.set(varName, typeName);
};
/** Rust: let x = User::new(), let x = User::default(), or let x = User { ... } */
const extractInitializer: InitializerExtractor = (node: SyntaxNode, env: Map<string, string>, _classNames: ClassNameLookup): void => {
// Skip if there's an explicit type annotation — Tier 0 already handled it
if (node.childForFieldName('type') !== null) return;
const pattern = node.childForFieldName('pattern');
const value = node.childForFieldName('value');
if (!pattern || !value) return;
// Rust struct literal: let user = User { name: "alice", age: 30 }
// tree-sitter-rust: struct_expression with 'name' field holding the type
if (value.type === 'struct_expression') {
const typeNode = value.childForFieldName('name');
if (!typeNode) return;
const rawType = extractSimpleTypeName(typeNode);
if (!rawType) return;
// Resolve Self to the actual struct/enum name from the enclosing impl block
const typeName = rawType === 'Self' ? findEnclosingImplType(node) : rawType;
const varName = extractVarName(pattern);
if (varName && typeName) env.set(varName, typeName);
return;
}
if (value.type !== 'call_expression') return;
const func = value.childForFieldName('function');
if (!func || func.type !== 'scoped_identifier') return;
const nameField = func.childForFieldName('name');
// Only match ::new() and ::default() — the two idiomatic Rust constructors.
// Deliberately excludes ::from(), ::with_capacity(), etc. to avoid false positives
// (e.g. String::from("x") is not necessarily the "String" type we want for method resolution).
if (!nameField || (nameField.text !== 'new' && nameField.text !== 'default')) return;
const pathField = func.childForFieldName('path');
if (!pathField) return;
const rawType = extractSimpleTypeName(pathField);
if (!rawType) return;
// Resolve Self to the actual struct/enum name from the enclosing impl block
const typeName = rawType === 'Self' ? findEnclosingImplType(node) : rawType;
const varName = extractVarName(pattern);
if (varName && typeName) env.set(varName, typeName);
};
/** Rust: parameter → pattern: type */
const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string, string>): void => {
let nameNode: SyntaxNode | null = null;
@ -38,5 +155,6 @@ const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string,
export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: DECLARATION_NODE_TYPES,
extractDeclaration,
extractInitializer,
extractParameter,
};

View file

@ -39,6 +39,24 @@ export const extractSimpleTypeName = (typeNode: SyntaxNode): string | undefined
if (inner) return extractSimpleTypeName(inner);
}
// Nullable union types (TS/JS: User | null, User | undefined, User | null | undefined)
// Extract the single non-null/undefined type from the union.
if (typeNode.type === 'union_type') {
const nonNullTypes: SyntaxNode[] = [];
for (let i = 0; i < typeNode.namedChildCount; i++) {
const child = typeNode.namedChild(i);
if (!child) continue;
// Skip null/undefined/void literal types
const text = child.text;
if (text === 'null' || text === 'undefined' || text === 'void') continue;
nonNullTypes.push(child);
}
// Only unwrap if exactly one meaningful type remains
if (nonNullTypes.length === 1) {
return extractSimpleTypeName(nonNullTypes[0]);
}
}
// Type annotations that wrap the actual type (TS/Python: `: Foo`, Kotlin: user_type)
if (typeNode.type === 'type_annotation' || typeNode.type === 'type'
|| typeNode.type === 'user_type') {
@ -52,6 +70,11 @@ export const extractSimpleTypeName = (typeNode: SyntaxNode): string | undefined
if (inner) return extractSimpleTypeName(inner);
}
// PHP primitive_type (string, int, float, bool)
if (typeNode.type === 'primitive_type') {
return typeNode.text;
}
// PHP named_type / optional_type
if (typeNode.type === 'named_type' || typeNode.type === 'optional_type') {
const inner = typeNode.childForFieldName('name') ?? typeNode.firstNamedChild;
@ -80,6 +103,11 @@ export const extractVarName = (node: SyntaxNode): string | undefined => {
const nameChild = node.childForFieldName('name');
if (nameChild) return extractVarName(nameChild);
}
// Rust: let mut x = ... — mut_pattern wraps an identifier
if (node.type === 'mut_pattern') {
const inner = node.firstNamedChild;
if (inner) return extractVarName(inner);
}
return undefined;
};
@ -91,6 +119,7 @@ export const TYPED_PARAMETER_TYPES = new Set([
'parameter', // C#/Rust/Go/Python/Swift
'parameter_declaration', // C/C++ void f(Type name)
'simple_parameter', // PHP function(Foo $x)
'property_promotion_parameter', // PHP 8.0+ constructor promotion: __construct(private Foo $x)
]);
/** Find the first named child with the given node type */

View file

@ -1,5 +1,5 @@
import type { SyntaxNode } from '../utils.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor } from './types.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup } from './types.js';
import { extractSimpleTypeName, extractVarName, findChildByType } from './shared.js';
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
@ -39,8 +39,47 @@ const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string,
if (varName && typeName) env.set(varName, typeName);
};
/** Swift: let user = User(name: "alice") — infer type from call when callee is a known class.
* Swift initializers are syntactically identical to function calls, so we verify
* against classNames (which may include cross-file SymbolTable lookups). */
const extractInitializer: InitializerExtractor = (node: SyntaxNode, env: Map<string, string>, classNames: ClassNameLookup): void => {
if (node.type !== 'property_declaration') return;
// Skip if has type annotation — extractDeclaration handled it
if (node.childForFieldName('type') || findChildByType(node, 'type_annotation')) return;
// Find pattern (variable name)
const pattern = node.childForFieldName('pattern') ?? findChildByType(node, 'pattern');
if (!pattern) return;
const varName = extractVarName(pattern) ?? pattern.text;
if (!varName || env.has(varName)) return;
// Find call_expression in the value
const callExpr = findChildByType(node, 'call_expression');
if (!callExpr) return;
const callee = callExpr.firstNamedChild;
if (!callee) return;
// Direct call: User(name: "alice")
if (callee.type === 'simple_identifier') {
const calleeName = callee.text;
if (calleeName && classNames.has(calleeName)) {
env.set(varName, calleeName);
}
return;
}
// Explicit init: User.init(name: "alice") — navigation_expression with .init suffix
if (callee.type === 'navigation_expression') {
const receiver = callee.firstNamedChild;
const suffix = callee.lastNamedChild;
if (receiver?.type === 'simple_identifier' && suffix?.text === 'init') {
const calleeName = receiver.text;
if (calleeName && classNames.has(calleeName)) {
env.set(varName, calleeName);
}
}
}
};
export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: DECLARATION_NODE_TYPES,
extractDeclaration,
extractParameter,
extractInitializer,
};

View file

@ -6,6 +6,13 @@ export type TypeBindingExtractor = (node: SyntaxNode, env: Map<string, string>)
/** Extracts type bindings from a parameter node into the env map */
export type ParameterExtractor = (node: SyntaxNode, env: Map<string, string>) => void;
/** Minimal interface for checking whether a name is a known class/struct.
* Narrower than ReadonlySet — only `.has()` is used by extractors. */
export type ClassNameLookup = { has(name: string): boolean };
/** Extracts type bindings from a constructor-call initializer, with access to known class names */
export type InitializerExtractor = (node: SyntaxNode, env: Map<string, string>, classNames: ClassNameLookup) => void;
/** Per-language type extraction configuration */
export interface LanguageTypeConfig {
/** Node types that represent typed declarations for this language */
@ -14,4 +21,9 @@ export interface LanguageTypeConfig {
extractDeclaration: TypeBindingExtractor;
/** Extract a (varName → typeName) binding from a parameter node */
extractParameter: ParameterExtractor;
/** Extract a (varName → typeName) binding from a constructor-call initializer.
* Called as fallback when extractDeclaration produces no binding for a declaration node.
* Only for languages with syntactic constructor markers (new, composite_literal, ::new).
* Receives classNames — the set of class/struct names visible in the current file's AST. */
extractInitializer?: InitializerExtractor;
}

View file

@ -1,5 +1,5 @@
import type { SyntaxNode } from '../utils.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor } from './types.js';
import type { LanguageTypeConfig, ParameterExtractor, TypeBindingExtractor, InitializerExtractor, ClassNameLookup } from './types.js';
import { extractSimpleTypeName, extractVarName } from './shared.js';
const DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
@ -41,8 +41,33 @@ const extractParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string,
if (varName && typeName) env.set(varName, typeName);
};
/** TypeScript: const x = new User() — infer type from new_expression */
const extractInitializer: InitializerExtractor = (node: SyntaxNode, env: Map<string, string>, _classNames: ClassNameLookup): void => {
for (let i = 0; i < node.namedChildCount; i++) {
const declarator = node.namedChild(i);
if (declarator?.type !== 'variable_declarator') continue;
// Only activate when there is no explicit type annotation — extractDeclaration already
// handles the annotated case and this function is called as a fallback.
if (declarator.childForFieldName('type') !== null) continue;
let valueNode = declarator.childForFieldName('value');
// Unwrap `new User() as T`, `new User()!`, and double-cast `new User() as unknown as T`
while (valueNode?.type === 'as_expression' || valueNode?.type === 'non_null_expression') {
valueNode = valueNode.firstNamedChild;
}
if (valueNode?.type !== 'new_expression') continue;
const constructorNode = valueNode.childForFieldName('constructor');
if (!constructorNode) continue;
const nameNode = declarator.childForFieldName('name');
if (!nameNode) continue;
const varName = extractVarName(nameNode);
const typeName = extractSimpleTypeName(constructorNode);
if (varName && typeName) env.set(varName, typeName);
}
};
export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: DECLARATION_NODE_TYPES,
extractDeclaration,
extractParameter,
extractInitializer,
};

View file

@ -122,7 +122,7 @@ export const BUILT_IN_NAMES = new Set([
'print', 'len', 'range', 'str', 'int', 'float', 'list', 'dict', 'set', 'tuple',
'append', 'extend', 'update',
// NOTE: 'open', 'read', 'write', 'close' removed — these are real C POSIX syscalls
'super', 'type', 'isinstance', 'issubclass', 'getattr', 'setattr', 'hasattr',
'type', 'isinstance', 'issubclass', 'getattr', 'setattr', 'hasattr',
'enumerate', 'zip', 'sorted', 'reversed', 'min', 'max', 'sum', 'abs',
// Kotlin stdlib
'println', 'print', 'readLine', 'require', 'requireNotNull', 'check', 'assert', 'lazy', 'error',
@ -271,6 +271,9 @@ export const CLASS_CONTAINER_TYPES = new Set([
// Ruby
'class',
'module',
// Kotlin
'object_declaration',
'companion_object',
]);
export const CONTAINER_TYPE_TO_LABEL: Record<string, string> = {
@ -288,6 +291,8 @@ export const CONTAINER_TYPE_TO_LABEL: Record<string, string> = {
protocol_declaration: 'Interface',
class: 'Class',
module: 'Module',
object_declaration: 'Class',
companion_object: 'Class',
};
/** Walk up AST to find enclosing class/struct/interface/impl, return its generateId or null.
@ -787,6 +792,10 @@ const SIMPLE_RECEIVER_TYPES = new Set([
'name', // PHP name node
'this', // TS/JS/Java/C# this.method()
'self', // Rust/Python self.method()
'super', // TS/JS/Java/Kotlin/Ruby super.method()
'super_expression', // Kotlin wraps super in super_expression
'base', // C# base.Method()
'parent', // PHP parent::method()
]);
export const extractReceiverName = (
@ -824,6 +833,17 @@ export const extractReceiverName = (
receiver = parent.childForFieldName('receiver');
}
// PHP scoped_call_expression (parent::method(), self::method()):
// nameNode's direct parent IS the scoped_call_expression (name is a direct child)
if (!receiver && (parent.type === 'scoped_call_expression' || callNode.type === 'scoped_call_expression')) {
const scopedCall = parent.type === 'scoped_call_expression' ? parent : callNode;
receiver = scopedCall.childForFieldName('scope');
// relative_scope wraps 'parent'/'self'/'static' — unwrap to get the keyword
if (receiver?.type === 'relative_scope') {
receiver = receiver.firstChild;
}
}
// Kotlin/Swift: navigation_expression target is the first child
if (!receiver && parent.type === 'navigation_suffix') {
const navExpr = parent.parent;
@ -845,6 +865,12 @@ export const extractReceiverName = (
return receiver.text;
}
// Python super().method(): receiver is a call node `super()` — extract the function name
if (receiver.type === 'call') {
const func = receiver.childForFieldName('function');
if (func?.text === 'super') return 'super';
}
return undefined;
};

View file

@ -33,7 +33,8 @@ import {
inferCallForm,
extractReceiverName
} from '../utils.js';
import { buildTypeEnv, lookupTypeEnv } from '../type-env.js';
import { buildTypeEnv } from '../type-env.js';
import type { ConstructorBinding } from '../type-env.js';
import { isNodeExported } from '../export-detection.js';
import { detectFrameworkFromAST } from '../framework-detection.js';
import { generateId } from '../../../lib/utils.js';
@ -122,6 +123,12 @@ export interface ExtractedRoute {
lineNumber: number;
}
/** Constructor bindings keyed by filePath for cross-file type resolution */
export interface FileConstructorBindings {
filePath: string;
bindings: ConstructorBinding[];
}
export interface ParseWorkerResult {
nodes: ParsedNode[];
relationships: ParsedRelationship[];
@ -130,6 +137,7 @@ export interface ParseWorkerResult {
calls: ExtractedCall[];
heritage: ExtractedHeritage[];
routes: ExtractedRoute[];
constructorBindings: FileConstructorBindings[];
fileCount: number;
}
@ -241,6 +249,7 @@ const processBatch = (files: ParseWorkerInput[], onProgress?: (filesProcessed: n
calls: [],
heritage: [],
routes: [],
constructorBindings: [],
fileCount: 0,
};
@ -824,10 +833,15 @@ const processFileGroup = (
result.fileCount++;
onFileProcessed?.();
// Build per-file TypeEnv from explicit type annotations (for receiver resolution)
// Build per-file type environment + constructor bindings in a single AST walk.
// Constructor bindings are verified against the SymbolTable in processCallsFromExtracted.
const typeEnv = buildTypeEnv(tree, language);
const callRouter = callRouters[language];
if (typeEnv.constructorBindings.length > 0) {
result.constructorBindings.push({ filePath: file.path, bindings: [...typeEnv.constructorBindings] });
}
let matches;
try {
matches = query.matches(tree.rootNode);
@ -946,7 +960,7 @@ const processFileGroup = (
|| generateId('File', file.path);
const callForm = inferCallForm(callNode, callNameNode);
const receiverName = callForm === 'member' ? extractReceiverName(callNameNode) : undefined;
const receiverTypeName = receiverName ? lookupTypeEnv(typeEnv, receiverName, callNode) : undefined;
const receiverTypeName = receiverName ? typeEnv.lookup(receiverName, callNode) : undefined;
result.calls.push({
filePath: file.path,
calledName,
@ -1106,7 +1120,7 @@ const processFileGroup = (
/** Accumulated result across sub-batches */
let accumulated: ParseWorkerResult = {
nodes: [], relationships: [], symbols: [],
imports: [], calls: [], heritage: [], routes: [], fileCount: 0,
imports: [], calls: [], heritage: [], routes: [], constructorBindings: [], fileCount: 0,
};
let cumulativeProcessed = 0;
@ -1118,6 +1132,7 @@ const mergeResult = (target: ParseWorkerResult, src: ParseWorkerResult) => {
target.calls.push(...src.calls);
target.heritage.push(...src.heritage);
target.routes.push(...src.routes);
target.constructorBindings.push(...src.constructorBindings);
target.fileCount += src.fileCount;
};
@ -1139,7 +1154,7 @@ parentPort!.on('message', (msg: any) => {
if (msg && msg.type === 'flush') {
parentPort!.postMessage({ type: 'result', data: accumulated });
// Reset for potential reuse
accumulated = { nodes: [], relationships: [], symbols: [], imports: [], calls: [], heritage: [], routes: [], fileCount: 0 };
accumulated = { nodes: [], relationships: [], symbols: [], imports: [], calls: [], heritage: [], routes: [], constructorBindings: [], fileCount: 0 };
cumulativeProcessed = 0;
return;
}

View file

@ -0,0 +1,4 @@
class Repo {
public:
void save() {}
};

View file

@ -0,0 +1,4 @@
class User {
public:
void save() {}
};

View file

@ -0,0 +1,10 @@
#include "../models/User.h"
#include "../models/Repo.h"
void process() {
auto user = User{};
user.save();
auto repo = Repo{};
repo.save();
}

View file

@ -0,0 +1,10 @@
#pragma once
#include <string>
class Repo {
public:
Repo(const std::string& dbName) : dbName_(dbName) {}
bool save() { return false; }
private:
std::string dbName_;
};

View file

@ -0,0 +1,10 @@
#pragma once
#include <string>
class User {
public:
User(const std::string& name) : name_(name) {}
bool save() { return true; }
private:
std::string name_;
};

View file

@ -0,0 +1,9 @@
#include "models/User.h"
#include "models/Repo.h"
void processEntities() {
auto user = User("alice");
auto repo = Repo("maindb");
user.save();
repo.save();
}

View file

@ -0,0 +1,6 @@
#pragma once
class BaseModel {
public:
bool save() { return true; }
};

View file

@ -0,0 +1,7 @@
#pragma once
#include "BaseModel.h"
class User : public BaseModel {
public:
const char* serialize() { return ""; }
};

View file

@ -0,0 +1,8 @@
#include "user.h"
#include <vector>
void processUsers(std::vector<User> users) {
for (User& user : users) {
user.save();
}
}

View file

@ -0,0 +1,6 @@
#pragma once
class User {
public:
bool save() { return true; }
};

View file

@ -0,0 +1,7 @@
#include "models.h"
void run() {
auto client = ns::HttpClient{};
client.connect();
client.send();
}

View file

@ -0,0 +1,7 @@
namespace ns {
class HttpClient {
public:
void connect() {}
void send() {}
};
}

View file

@ -0,0 +1,4 @@
class Repo {
public:
bool save() { return true; }
};

View file

@ -0,0 +1,7 @@
class User {
public:
bool save() { return true; }
void process() {
this->save();
}
};

View file

@ -0,0 +1,5 @@
namespace Models;
public class BaseModel<T> {
public virtual bool Save() { return true; }
}

View file

@ -0,0 +1,5 @@
namespace Models;
public class Repo {
public bool Save() { return true; }
}

View file

@ -0,0 +1,8 @@
namespace Models;
public class User : BaseModel<string> {
public override bool Save() {
base.Save();
return true;
}
}

View file

@ -0,0 +1,5 @@
namespace Models;
public class BaseModel {
public bool Save() { return true; }
}

View file

@ -0,0 +1,5 @@
namespace Models;
public interface ISerializable {
string Serialize();
}

View file

@ -0,0 +1,5 @@
namespace Models;
public class User : BaseModel, ISerializable {
public string Serialize() { return ""; }
}

View file

@ -0,0 +1,20 @@
namespace Models;
public class Animal
{
public string Name { get; set; }
}
public class Dog : Animal
{
public void Bark()
{
}
}
public class Cat : Animal
{
public void Meow()
{
}
}

View file

@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,14 @@
using Models;
namespace Services;
public class AnimalService
{
public void HandleAnimal(Animal animal)
{
if (animal is Dog dog)
{
dog.Bark();
}
}
}

View file

@ -0,0 +1,5 @@
namespace Models;
public class Repo {
public bool Save() { return true; }
}

View file

@ -0,0 +1,8 @@
namespace Models;
public class User {
public bool Save() { return true; }
public void Process() {
this.Save();
}
}

View file

@ -0,0 +1,5 @@
namespace Models;
public class BaseModel {
public virtual bool Save() { return true; }
}

View file

@ -0,0 +1,5 @@
namespace Models;
public class Repo {
public bool Save() { return true; }
}

View file

@ -0,0 +1,8 @@
namespace Models;
public class User : BaseModel {
public override bool Save() {
base.Save();
return true;
}
}

View file

@ -0,0 +1,10 @@
package main
import "example.com/go-constructor-type-inference/models"
func processEntities() {
user := models.User{}
repo := models.Repo{}
user.Save()
repo.Save()
}

View file

@ -0,0 +1,3 @@
module example.com/go-constructor-type-inference
go 1.21

View file

@ -0,0 +1,7 @@
package models
type Repo struct{}
func (r *Repo) Save() bool {
return false
}

View file

@ -0,0 +1,7 @@
package models
type User struct{}
func (u *User) Save() bool {
return true
}

View file

@ -0,0 +1,9 @@
package main
func main() {
sl := make([]User, 0)
sl[0].Save()
m := make(map[string]User)
m["key"].Greet()
}

View file

@ -0,0 +1,10 @@
package main
type User struct {
Name string
}
func (u *User) Save() {}
func (u *User) Greet() string {
return "Hello, " + u.Name
}

View file

@ -0,0 +1,7 @@
package main
func main() {
user := new(User)
user.Save()
user.Greet()
}

View file

@ -0,0 +1,11 @@
package main
type User struct {
Name string
Age int
}
func (u *User) Save() {}
func (u *User) Greet() string {
return "Hello, " + u.Name
}

View file

@ -0,0 +1,3 @@
module example.com/app
go 1.21

View file

@ -0,0 +1,7 @@
package models
type BaseModel struct{}
func (b *BaseModel) Save() bool {
return true
}

View file

@ -0,0 +1,9 @@
package models
type User struct {
BaseModel
}
func (u *User) Serialize() string {
return ""
}

View file

@ -0,0 +1,11 @@
package main
import "example.com/pointer-test/models"
func process() {
user := &models.User{Name: "alice"}
user.Save()
repo := &models.Repo{Name: "test"}
repo.Save()
}

View file

@ -0,0 +1,3 @@
module example.com/pointer-test
go 1.21

View file

@ -0,0 +1,9 @@
package models
type Repo struct {
Name string
}
func (r *Repo) Save() bool {
return true
}

View file

@ -0,0 +1,9 @@
package models
type User struct {
Name string
}
func (u *User) Save() bool {
return true
}

View file

@ -0,0 +1,7 @@
package main
func process(s Saver) {
user := s.(User)
user.Save()
user.Greet()
}

View file

@ -0,0 +1,14 @@
package main
type Saver interface {
Save()
}
type User struct {
Name string
}
func (u *User) Save() {}
func (u *User) Greet() string {
return "Hello, " + u.Name
}

View file

@ -0,0 +1,11 @@
import models.User;
import models.Repo;
public class App {
public static void processEntities() {
var user = new User();
var repo = new Repo();
user.save();
repo.save();
}
}

View file

@ -0,0 +1,7 @@
package models;
public class Repo {
public boolean save() {
return false;
}
}

View file

@ -0,0 +1,7 @@
package models;
public class User {
public boolean save() {
return true;
}
}

View file

@ -0,0 +1,5 @@
package models;
public class BaseModel<T> {
public boolean save() { return true; }
}

View file

@ -0,0 +1,5 @@
package models;
public class Repo {
public boolean save() { return true; }
}

View file

@ -0,0 +1,8 @@
package models;
public class User extends BaseModel<String> {
public boolean save() {
super.save();
return true;
}
}

View file

@ -0,0 +1,5 @@
package interfaces;
public interface Serializable {
String serialize();
}

View file

@ -0,0 +1,5 @@
package models;
public class BaseModel {
public boolean save() { return true; }
}

View file

@ -0,0 +1,7 @@
package models;
import interfaces.Serializable;
public class User extends BaseModel implements Serializable {
public String serialize() { return ""; }
}

View file

@ -0,0 +1,5 @@
package models;
public class Repo {
public boolean save() { return true; }
}

View file

@ -0,0 +1,8 @@
package models;
public class User {
public boolean save() { return true; }
public void process() {
this.save();
}
}

View file

@ -0,0 +1,5 @@
package models;
public class BaseModel {
public boolean save() { return true; }
}

View file

@ -0,0 +1,5 @@
package models;
public class Repo {
public boolean save() { return true; }
}

View file

@ -0,0 +1,8 @@
package models;
public class User extends BaseModel {
public boolean save() {
super.save();
return true;
}
}

View file

@ -0,0 +1,11 @@
const { User } = require('./user');
const { Repo } = require('./repo');
function processEntities() {
const user = new User();
const repo = new Repo();
user.save();
repo.save();
}
module.exports = { processEntities };

View file

@ -0,0 +1,7 @@
class Repo {
save() {
return false;
}
}
module.exports = { Repo };

View file

@ -0,0 +1,7 @@
class User {
save() {
return true;
}
}
module.exports = { User };

View file

@ -0,0 +1,4 @@
class BaseModel {
save() { return true; }
}
module.exports = { BaseModel };

View file

@ -0,0 +1,6 @@
const { BaseModel } = require('./Base');
class User extends BaseModel {
serialize() { return ''; }
}
module.exports = { User };

View file

@ -0,0 +1,4 @@
class Repo {
save() { return true; }
}
module.exports = { Repo };

View file

@ -0,0 +1,7 @@
class User {
save() { return true; }
process() {
this.save();
}
}
module.exports = { User };

View file

@ -0,0 +1,4 @@
class BaseModel {
save() { return true; }
}
module.exports = { BaseModel };

View file

@ -0,0 +1,4 @@
class Repo {
save() { return true; }
}
module.exports = { Repo };

Some files were not shown because too many files have changed in this diff Show more