mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-29 06:32:28 +00:00
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:
parent
09a3a42ae9
commit
216a8b5db8
232 changed files with 7302 additions and 924 deletions
2
.github/workflows/ci-integration.yml
vendored
2
.github/workflows/ci-integration.yml
vendored
|
|
@ -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()
|
||||
|
|
|
|||
186
.github/workflows/ci-report.yml
vendored
186
.github/workflows/ci-report.yml
vendored
|
|
@ -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 ""
|
||||
|
|
|
|||
54
AGENTS.md
54
AGENTS.md
|
|
@ -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 -->
|
||||
|
|
|
|||
54
CLAUDE.md
54
CLAUDE.md
|
|
@ -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 -->
|
||||
|
|
|
|||
23
README.md
23
README.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
7
gitnexus/.claude/settings.local.json
Normal file
7
gitnexus/.claude/settings.local.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"mcp__plugin_claude-mem_mcp-search__get_observations"
|
||||
]
|
||||
}
|
||||
}
|
||||
9
gitnexus/Dockerfile.test
Normal file
9
gitnexus/Dockerfile.test
Normal 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"]
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
192
gitnexus/src/core/ingestion/resolution-context.ts
Normal file
192
gitnexus/src/core/ingestion/resolution-context.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
4
gitnexus/test/fixtures/lang-resolution/cpp-brace-init-inference/models/Repo.h
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/cpp-brace-init-inference/models/Repo.h
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
class Repo {
|
||||
public:
|
||||
void save() {}
|
||||
};
|
||||
4
gitnexus/test/fixtures/lang-resolution/cpp-brace-init-inference/models/User.h
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/cpp-brace-init-inference/models/User.h
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
class User {
|
||||
public:
|
||||
void save() {}
|
||||
};
|
||||
10
gitnexus/test/fixtures/lang-resolution/cpp-brace-init-inference/services/App.cpp
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/cpp-brace-init-inference/services/App.cpp
vendored
Normal 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();
|
||||
}
|
||||
10
gitnexus/test/fixtures/lang-resolution/cpp-constructor-type-inference/models/Repo.h
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/cpp-constructor-type-inference/models/Repo.h
vendored
Normal 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_;
|
||||
};
|
||||
10
gitnexus/test/fixtures/lang-resolution/cpp-constructor-type-inference/models/User.h
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/cpp-constructor-type-inference/models/User.h
vendored
Normal 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_;
|
||||
};
|
||||
9
gitnexus/test/fixtures/lang-resolution/cpp-constructor-type-inference/services/App.cpp
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/cpp-constructor-type-inference/services/App.cpp
vendored
Normal 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();
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/cpp-parent-resolution/src/BaseModel.h
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/cpp-parent-resolution/src/BaseModel.h
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
class BaseModel {
|
||||
public:
|
||||
bool save() { return true; }
|
||||
};
|
||||
7
gitnexus/test/fixtures/lang-resolution/cpp-parent-resolution/src/User.h
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/cpp-parent-resolution/src/User.h
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#pragma once
|
||||
#include "BaseModel.h"
|
||||
|
||||
class User : public BaseModel {
|
||||
public:
|
||||
const char* serialize() { return ""; }
|
||||
};
|
||||
8
gitnexus/test/fixtures/lang-resolution/cpp-range-for/app.cpp
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/cpp-range-for/app.cpp
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#include "user.h"
|
||||
#include <vector>
|
||||
|
||||
void processUsers(std::vector<User> users) {
|
||||
for (User& user : users) {
|
||||
user.save();
|
||||
}
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/cpp-range-for/user.h
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/cpp-range-for/user.h
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
class User {
|
||||
public:
|
||||
bool save() { return true; }
|
||||
};
|
||||
7
gitnexus/test/fixtures/lang-resolution/cpp-scoped-brace-init/main.cpp
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/cpp-scoped-brace-init/main.cpp
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#include "models.h"
|
||||
|
||||
void run() {
|
||||
auto client = ns::HttpClient{};
|
||||
client.connect();
|
||||
client.send();
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/cpp-scoped-brace-init/models.h
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/cpp-scoped-brace-init/models.h
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace ns {
|
||||
class HttpClient {
|
||||
public:
|
||||
void connect() {}
|
||||
void send() {}
|
||||
};
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/cpp-self-this-resolution/src/Repo.cpp
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/cpp-self-this-resolution/src/Repo.cpp
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
class Repo {
|
||||
public:
|
||||
bool save() { return true; }
|
||||
};
|
||||
7
gitnexus/test/fixtures/lang-resolution/cpp-self-this-resolution/src/User.cpp
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/cpp-self-this-resolution/src/User.cpp
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
class User {
|
||||
public:
|
||||
bool save() { return true; }
|
||||
void process() {
|
||||
this->save();
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
namespace Models;
|
||||
|
||||
public class BaseModel<T> {
|
||||
public virtual bool Save() { return true; }
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/csharp-generic-parent-resolution/src/Models/Repo.cs
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/csharp-generic-parent-resolution/src/Models/Repo.cs
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
namespace Models;
|
||||
|
||||
public class Repo {
|
||||
public bool Save() { return true; }
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/csharp-generic-parent-resolution/src/Models/User.cs
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/csharp-generic-parent-resolution/src/Models/User.cs
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
namespace Models;
|
||||
|
||||
public class User : BaseModel<string> {
|
||||
public override bool Save() {
|
||||
base.Save();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/csharp-parent-resolution/src/Models/BaseModel.cs
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/csharp-parent-resolution/src/Models/BaseModel.cs
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
namespace Models;
|
||||
|
||||
public class BaseModel {
|
||||
public bool Save() { return true; }
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/csharp-parent-resolution/src/Models/ISerializable.cs
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/csharp-parent-resolution/src/Models/ISerializable.cs
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
namespace Models;
|
||||
|
||||
public interface ISerializable {
|
||||
string Serialize();
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/csharp-parent-resolution/src/Models/User.cs
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/csharp-parent-resolution/src/Models/User.cs
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
namespace Models;
|
||||
|
||||
public class User : BaseModel, ISerializable {
|
||||
public string Serialize() { return ""; }
|
||||
}
|
||||
20
gitnexus/test/fixtures/lang-resolution/csharp-pattern-matching/Models/Animal.cs
vendored
Normal file
20
gitnexus/test/fixtures/lang-resolution/csharp-pattern-matching/Models/Animal.cs
vendored
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/csharp-pattern-matching/PatternMatchProj.csproj
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/csharp-pattern-matching/PatternMatchProj.csproj
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
14
gitnexus/test/fixtures/lang-resolution/csharp-pattern-matching/Services/AnimalService.cs
vendored
Normal file
14
gitnexus/test/fixtures/lang-resolution/csharp-pattern-matching/Services/AnimalService.cs
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
using Models;
|
||||
|
||||
namespace Services;
|
||||
|
||||
public class AnimalService
|
||||
{
|
||||
public void HandleAnimal(Animal animal)
|
||||
{
|
||||
if (animal is Dog dog)
|
||||
{
|
||||
dog.Bark();
|
||||
}
|
||||
}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/csharp-self-this-resolution/src/Models/Repo.cs
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/csharp-self-this-resolution/src/Models/Repo.cs
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
namespace Models;
|
||||
|
||||
public class Repo {
|
||||
public bool Save() { return true; }
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/csharp-self-this-resolution/src/Models/User.cs
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/csharp-self-this-resolution/src/Models/User.cs
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
namespace Models;
|
||||
|
||||
public class User {
|
||||
public bool Save() { return true; }
|
||||
public void Process() {
|
||||
this.Save();
|
||||
}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/csharp-super-resolution/src/Models/BaseModel.cs
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/csharp-super-resolution/src/Models/BaseModel.cs
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
namespace Models;
|
||||
|
||||
public class BaseModel {
|
||||
public virtual bool Save() { return true; }
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/csharp-super-resolution/src/Models/Repo.cs
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/csharp-super-resolution/src/Models/Repo.cs
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
namespace Models;
|
||||
|
||||
public class Repo {
|
||||
public bool Save() { return true; }
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/csharp-super-resolution/src/Models/User.cs
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/csharp-super-resolution/src/Models/User.cs
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
namespace Models;
|
||||
|
||||
public class User : BaseModel {
|
||||
public override bool Save() {
|
||||
base.Save();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
10
gitnexus/test/fixtures/lang-resolution/go-constructor-type-inference/cmd/main.go
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/go-constructor-type-inference/cmd/main.go
vendored
Normal 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()
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/go-constructor-type-inference/go.mod
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/go-constructor-type-inference/go.mod
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module example.com/go-constructor-type-inference
|
||||
|
||||
go 1.21
|
||||
7
gitnexus/test/fixtures/lang-resolution/go-constructor-type-inference/models/repo.go
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/go-constructor-type-inference/models/repo.go
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package models
|
||||
|
||||
type Repo struct{}
|
||||
|
||||
func (r *Repo) Save() bool {
|
||||
return false
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/go-constructor-type-inference/models/user.go
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/go-constructor-type-inference/models/user.go
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package models
|
||||
|
||||
type User struct{}
|
||||
|
||||
func (u *User) Save() bool {
|
||||
return true
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/go-make-builtin/main.go
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/go-make-builtin/main.go
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package main
|
||||
|
||||
func main() {
|
||||
sl := make([]User, 0)
|
||||
sl[0].Save()
|
||||
|
||||
m := make(map[string]User)
|
||||
m["key"].Greet()
|
||||
}
|
||||
10
gitnexus/test/fixtures/lang-resolution/go-make-builtin/models.go
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/go-make-builtin/models.go
vendored
Normal 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
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/go-new-builtin/main.go
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/go-new-builtin/main.go
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
func main() {
|
||||
user := new(User)
|
||||
user.Save()
|
||||
user.Greet()
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/go-new-builtin/models.go
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/go-new-builtin/models.go
vendored
Normal 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
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/go-parent-resolution/go.mod
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/go-parent-resolution/go.mod
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module example.com/app
|
||||
|
||||
go 1.21
|
||||
7
gitnexus/test/fixtures/lang-resolution/go-parent-resolution/models/base.go
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/go-parent-resolution/models/base.go
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package models
|
||||
|
||||
type BaseModel struct{}
|
||||
|
||||
func (b *BaseModel) Save() bool {
|
||||
return true
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/go-parent-resolution/models/user.go
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/go-parent-resolution/models/user.go
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package models
|
||||
|
||||
type User struct {
|
||||
BaseModel
|
||||
}
|
||||
|
||||
func (u *User) Serialize() string {
|
||||
return ""
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/go-pointer-constructor-inference/cmd/main.go
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/go-pointer-constructor-inference/cmd/main.go
vendored
Normal 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()
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/go-pointer-constructor-inference/go.mod
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/go-pointer-constructor-inference/go.mod
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module example.com/pointer-test
|
||||
|
||||
go 1.21
|
||||
9
gitnexus/test/fixtures/lang-resolution/go-pointer-constructor-inference/models/repo.go
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/go-pointer-constructor-inference/models/repo.go
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package models
|
||||
|
||||
type Repo struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (r *Repo) Save() bool {
|
||||
return true
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/go-pointer-constructor-inference/models/user.go
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/go-pointer-constructor-inference/models/user.go
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package models
|
||||
|
||||
type User struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (u *User) Save() bool {
|
||||
return true
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/go-type-assertion/main.go
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/go-type-assertion/main.go
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
func process(s Saver) {
|
||||
user := s.(User)
|
||||
user.Save()
|
||||
user.Greet()
|
||||
}
|
||||
14
gitnexus/test/fixtures/lang-resolution/go-type-assertion/models.go
vendored
Normal file
14
gitnexus/test/fixtures/lang-resolution/go-type-assertion/models.go
vendored
Normal 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
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/java-constructor-type-inference/App.java
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/java-constructor-type-inference/App.java
vendored
Normal 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();
|
||||
}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/java-constructor-type-inference/models/Repo.java
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/java-constructor-type-inference/models/Repo.java
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package models;
|
||||
|
||||
public class Repo {
|
||||
public boolean save() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/java-constructor-type-inference/models/User.java
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/java-constructor-type-inference/models/User.java
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package models;
|
||||
|
||||
public class User {
|
||||
public boolean save() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package models;
|
||||
|
||||
public class BaseModel<T> {
|
||||
public boolean save() { return true; }
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/java-generic-parent-resolution/src/models/Repo.java
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/java-generic-parent-resolution/src/models/Repo.java
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package models;
|
||||
|
||||
public class Repo {
|
||||
public boolean save() { return true; }
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/java-generic-parent-resolution/src/models/User.java
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/java-generic-parent-resolution/src/models/User.java
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package models;
|
||||
|
||||
public class User extends BaseModel<String> {
|
||||
public boolean save() {
|
||||
super.save();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package interfaces;
|
||||
|
||||
public interface Serializable {
|
||||
String serialize();
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/java-parent-resolution/src/models/BaseModel.java
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/java-parent-resolution/src/models/BaseModel.java
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package models;
|
||||
|
||||
public class BaseModel {
|
||||
public boolean save() { return true; }
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/java-parent-resolution/src/models/User.java
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/java-parent-resolution/src/models/User.java
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package models;
|
||||
|
||||
import interfaces.Serializable;
|
||||
|
||||
public class User extends BaseModel implements Serializable {
|
||||
public String serialize() { return ""; }
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/java-self-this-resolution/src/models/Repo.java
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/java-self-this-resolution/src/models/Repo.java
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package models;
|
||||
|
||||
public class Repo {
|
||||
public boolean save() { return true; }
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/java-self-this-resolution/src/models/User.java
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/java-self-this-resolution/src/models/User.java
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package models;
|
||||
|
||||
public class User {
|
||||
public boolean save() { return true; }
|
||||
public void process() {
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/java-super-resolution/src/models/BaseModel.java
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/java-super-resolution/src/models/BaseModel.java
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package models;
|
||||
|
||||
public class BaseModel {
|
||||
public boolean save() { return true; }
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/java-super-resolution/src/models/Repo.java
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/java-super-resolution/src/models/Repo.java
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package models;
|
||||
|
||||
public class Repo {
|
||||
public boolean save() { return true; }
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/java-super-resolution/src/models/User.java
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/java-super-resolution/src/models/User.java
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package models;
|
||||
|
||||
public class User extends BaseModel {
|
||||
public boolean save() {
|
||||
super.save();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
11
gitnexus/test/fixtures/lang-resolution/javascript-constructor-type-inference/src/app.js
vendored
Normal file
11
gitnexus/test/fixtures/lang-resolution/javascript-constructor-type-inference/src/app.js
vendored
Normal 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 };
|
||||
7
gitnexus/test/fixtures/lang-resolution/javascript-constructor-type-inference/src/repo.js
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/javascript-constructor-type-inference/src/repo.js
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
class Repo {
|
||||
save() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Repo };
|
||||
7
gitnexus/test/fixtures/lang-resolution/javascript-constructor-type-inference/src/user.js
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/javascript-constructor-type-inference/src/user.js
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
class User {
|
||||
save() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { User };
|
||||
4
gitnexus/test/fixtures/lang-resolution/javascript-parent-resolution/src/models/Base.js
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/javascript-parent-resolution/src/models/Base.js
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
class BaseModel {
|
||||
save() { return true; }
|
||||
}
|
||||
module.exports = { BaseModel };
|
||||
6
gitnexus/test/fixtures/lang-resolution/javascript-parent-resolution/src/models/User.js
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/javascript-parent-resolution/src/models/User.js
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
const { BaseModel } = require('./Base');
|
||||
|
||||
class User extends BaseModel {
|
||||
serialize() { return ''; }
|
||||
}
|
||||
module.exports = { User };
|
||||
4
gitnexus/test/fixtures/lang-resolution/javascript-self-this-resolution/src/models/Repo.js
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/javascript-self-this-resolution/src/models/Repo.js
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
class Repo {
|
||||
save() { return true; }
|
||||
}
|
||||
module.exports = { Repo };
|
||||
7
gitnexus/test/fixtures/lang-resolution/javascript-self-this-resolution/src/models/User.js
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/javascript-self-this-resolution/src/models/User.js
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
class User {
|
||||
save() { return true; }
|
||||
process() {
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
module.exports = { User };
|
||||
4
gitnexus/test/fixtures/lang-resolution/javascript-super-resolution/src/models/Base.js
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/javascript-super-resolution/src/models/Base.js
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
class BaseModel {
|
||||
save() { return true; }
|
||||
}
|
||||
module.exports = { BaseModel };
|
||||
4
gitnexus/test/fixtures/lang-resolution/javascript-super-resolution/src/models/Repo.js
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/javascript-super-resolution/src/models/Repo.js
vendored
Normal 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
Loading…
Add table
Reference in a new issue