From 9b0fbdef791ed72dc45144d273e63c3f751c2871 Mon Sep 17 00:00:00 2001 From: Puma7 <88577358+Puma7@users.noreply.github.com> Date: Sun, 11 Oct 2026 21:50:04 +0200 Subject: [PATCH 1/2] fix(js): resolve imported constructor methods and explicit super calls --- CHANGELOG.md | 1 + __tests__/js-constructor-super.test.ts | 133 +++++++++++++++++++++++ src/resolution/index.ts | 7 ++ src/resolution/js-constructor-methods.ts | 97 +++++++++++++++++ src/resolution/name-matcher.ts | 3 + 5 files changed, 241 insertions(+) create mode 100644 __tests__/js-constructor-super.test.ts create mode 100644 src/resolution/js-constructor-methods.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d6e834999..4d3e9ff719 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes +- JavaScript and TypeScript calls through an imported constructor alias now reach that class's own or inherited method, and an overridden method's explicit superclass call reaches the parent while genuine recursion remains linked; re-index after upgrading. (#2497) - In VB.NET, every member of a `Structure` is now indexed, including its fields, properties, methods, constructors and nested enums. Before, only the first member was, so the rest could not be found and their callers looked empty. - In VB.NET and C#, what a property's `Get` and `Set` code calls, creates and reads now belongs to that property, as do C#'s `get => …` accessors and `=> …` property bodies. Before, it was dropped, so a method used only from a property looked unused. - In VB.NET, a field or property initializer like `= Compute()` or `As New List(Of Order)` now links what it calls and creates, and so do a `Custom Event`'s `AddHandler`, `RemoveHandler` and `RaiseEvent` blocks. Re-index VB.NET and C# projects after upgrading. diff --git a/__tests__/js-constructor-super.test.ts b/__tests__/js-constructor-super.test.ts new file mode 100644 index 0000000000..7a7bbe674f --- /dev/null +++ b/__tests__/js-constructor-super.test.ts @@ -0,0 +1,133 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; + +let root: string | undefined; +let graph: CodeGraph | undefined; +afterEach(() => { + graph?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); +}); +async function project(files: Record): Promise { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-js-constructors-')); + for (const [file, source] of Object.entries(files)) fs.writeFileSync(path.join(root, file), source); + graph = await CodeGraph.init(root, { index: true }); + return graph; +} +function callees(cg: CodeGraph, name: string): string[] { + const caller = [...cg.getNodesByKind('function'), ...cg.getNodesByKind('method')].find(n => n.name === name)!; + return cg.getOutgoingEdges(caller.id).filter(e => e.kind === 'calls') + .map(e => { const n = cg.getNode(e.target)!; return `${n.filePath}:${n.qualifiedName}`; }).sort(); +} + +describe.each(['js', 'ts', 'jsx', 'tsx'])('imported constructor identity (%s)', ext => { + it.each([false, true])('follows a named alias with a competing method = %s', decoy => { + return project({ + ['left.' + ext]: 'export class Left { show() { return "left"; } }', + ...(decoy ? { ['right.' + ext]: 'export class Right { show() { return "right"; } }' } : {}), + ['consumer.' + ext]: `import { Left as Known } from './left'; export function invoke() { const item = new Known(); return item.show(); }`, + }).then(cg => expect(callees(cg, 'invoke')).toContain(`left.${ext}:Left::show`)); + }); + + it('pins an unaliased import to its declaring file even with same-named classes', async () => { + const cg = await project({ + ['left.' + ext]: 'export class Shared { show() { return "left"; } }', + ['right.' + ext]: 'export class Shared { show() { return "right"; } }', + ['consumer.' + ext]: `import { Shared } from './left'; export function invoke() { const item = new Shared(); return item.show(); }`, + }); + expect(callees(cg, 'invoke')).toContain(`left.${ext}:Shared::show`); + expect(callees(cg, 'invoke')).not.toContain(`right.${ext}:Shared::show`); + }); + + it.each([ + 'export function invoke(Known) { const item = new Known(); return item.show(); }', + 'export function invoke() { const Known = factory(); const item = new Known(); return item.show(); }', + 'export function invoke() { let item = new Known(); item = factory(); return item.show(); }', + 'export function invoke() { let item = new Known(); item += 1; return item.show(); }', + 'export function invoke() { let item = new Known(); item ||= factory(); return item.show(); }', + 'export function invoke() { let item = new Known(); [item] = factory(); return item.show(); }', + 'export function invoke() { let item = new Known(); const msg = `${item = factory()}`; return item.show(); }', + 'export function invoke(item) { return item.show(); }', + ])('does not turn an unknown or shadowed receiver into the imported class: %s', body => { + return project({ + ['left.' + ext]: 'export class Left { show() { return "left"; } }', + ['right.' + ext]: 'export class Right { show() { return "right"; } }', + ['consumer.' + ext]: `import { Left as Known } from './left'; ${body}`, + }).then(cg => expect(callees(cg, 'invoke').filter(n => n.endsWith('::show'))).toEqual([])); + }); + + it('does not select a decoy method absent from the imported class', async () => { + const cg = await project({ + ['left.' + ext]: 'export class Left {}', + ['right.' + ext]: 'export class Right { show() {} }', + ['consumer.' + ext]: `import { Left as Known } from './left'; export function invoke() { const item = new Known(); return item.show(); }`, + }); + expect(callees(cg, 'invoke').filter(n => n.endsWith('::show'))).toEqual([]); + }); + + it('follows a default import, a re-export and the exact superclass', async () => { + const cg = await project({ + ['base.' + ext]: 'export class Base { show() {} }', + ['left.' + ext]: `import { Base } from './base'; export default class Left extends Base {}`, + ['barrel.' + ext]: `export { default as Known } from './left';`, + ['decoy.' + ext]: 'export class Base { show() {} }', + ['consumer.' + ext]: `import { Known } from './barrel'; export function invoke() { const item = new Known(); return item.show(); }`, + }); + expect(callees(cg, 'invoke')).toContain(`base.${ext}:Base::show`); + expect(callees(cg, 'invoke')).not.toContain(`decoy.${ext}:Base::show`); + }); + + it('retains a known local class shadowing the constructor import', async () => { + const cg = await project({ + ['left.' + ext]: 'export class Left { show() {} }', + ['consumer.' + ext]: `import { Left as Known } from './left'; export function invoke() { class Known { show() {} } const item = new Known(); return item.show(); }`, + }); + expect(callees(cg, 'invoke').some(n => n.startsWith('consumer.' + ext + ':') && n.endsWith('Known::show'))).toBe(true); + expect(callees(cg, 'invoke')).not.toContain(`left.${ext}:Left::show`); + }); +}); + +it('preserves calls through an imported TypeScript parameter type', async () => { + const cg = await project({ + 'left.ts': 'export class Left { show() {} }', + 'consumer.ts': `import { Left } from './left'; export function invoke(item: Left) { return item.show(); }`, + }); + expect(callees(cg, 'invoke')).toContain('left.ts:Left::show'); +}); + +describe.each(['js', 'ts', 'jsx', 'tsx'])('explicit superclass calls (%s)', ext => { + it.each(['run() { return super.run(); }', 'work() { return super.run(); }', 'run() { return super\n .run(); }', 'run() { return super\n\n\n\n\n .run(); }', 'run() { return `${super.run()}`; }', 'run() { return super /* parent */ .run(); }'])('reaches the parent: %s', body => { + return project({ + ['base.' + ext]: 'export class Base { run() { return 1; } }', + ['decoy.' + ext]: 'export class Base { run() { return 2; } }', + ['child.' + ext]: `import { Base } from './base'; export class Child extends Base { ${body} }`, + }).then(cg => { + // Locate the child explicitly: Base also declares run. + const child = cg.getNodesInFile('child.' + ext).find(n => n.kind === 'method')!; + const edges = cg.getOutgoingEdges(child.id).filter(e => e.kind === 'calls'); + expect(edges.map(e => cg.getNode(e.target)!.filePath)).toContain('base.' + ext); + expect(edges.some(e => e.target === child.id)).toBe(false); + expect(edges.map(e => cg.getNode(e.target)!.filePath)).not.toContain('decoy.' + ext); + }); + }); + + it('preserves genuine recursion on the same line as a super call', async () => { + const cg = await project({ + ['base.' + ext]: 'export class Base { run() {} }', + ['child.' + ext]: `import { Base } from './base'; export class Child extends Base { run(n) { super.run(); if (n) this.run(n - 1); } }`, + }); + const child = cg.getNodesInFile('child.' + ext).find(n => n.kind === 'method')!; + expect(cg.getOutgoingEdges(child.id)).toContainEqual(expect.objectContaining({ kind: 'calls', target: child.id })); + }); + + it('leaves an external parent unresolved instead of picking a namesake', async () => { + const cg = await project({ + ['decoy.' + ext]: 'export class Local { run() {} }', + ['child.' + ext]: `import { Base } from 'external-package'; export class Child extends Base { run() { return super.run(); } }`, + }); + const child = cg.getNodesInFile('child.' + ext).find(n => n.kind === 'method')!; + expect(cg.getOutgoingEdges(child.id).filter(e => e.kind === 'calls')).toEqual([]); + }); +}); diff --git a/src/resolution/index.ts b/src/resolution/index.ts index 24a5ad5ecf..82ef49f2e6 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -30,6 +30,7 @@ import { clearDartLibraryMemos } from './dart-libraries'; import { clearVbnetReceiverMemos, isVbMemberRead, isVbPathCall, matchVbMemberRead, matchVbPathCall } from './vbnet-receivers'; import { gateTypeParameter, clearTypeParameterMemos } from './type-parameters'; import { gateDartLocal, clearDartLocalScopeMemos } from './dart-local-scope'; +import { matchJsSuperCall, clearJsConstructorMemos } from './js-constructor-methods'; import { clearCppTypeAliasMemos } from './cpp-type-aliases'; import { clearCppIncluderMemos } from './cpp-includers'; import { matchShopifyThemeFile } from './shopify-themes'; @@ -572,6 +573,7 @@ export class ReferenceResolver { clearVbnetReceiverMemos(this.context); clearTypeParameterMemos(this.context); clearDartLocalScopeMemos(this.context); + clearJsConstructorMemos(this.context); clearCppTypeAliasMemos(this.context); clearCppSupertypeMemos(this.context); clearCppIncluderMemos(this.context); @@ -1261,6 +1263,8 @@ export class ReferenceResolver { } private resolveOneInner(ref: UnresolvedRef): ResolvedRef | null { + const jsSuper = matchJsSuperCall(ref, this.context); + if (jsSuper !== undefined) return jsSuper; // A local C++ object construction (`T obj(args)`, ref `ns::T::T/1`) // resolves ONLY to a constructor of the lexically nearest `T` (#1839). if (isCppConstructorRef(ref)) return matchCppConstructor(ref, this.context); @@ -3350,6 +3354,9 @@ export class ReferenceResolver { */ private gateSuperSelfCall(result: ResolvedRef | null, ref: UnresolvedRef): ResolvedRef | null { if (!result || ref.referenceKind !== 'calls' || result.targetNodeId !== ref.fromNodeId) return result; + // JS/TS super calls were already handled at their exact call-site above; + // a real recursive call on the same line must retain its self-edge. + if (['javascript', 'jsx', 'typescript', 'tsx', 'arkts'].includes(ref.language)) return result; const name = ref.referenceName.slice(Math.max(ref.referenceName.lastIndexOf('.'), ref.referenceName.lastIndexOf(':')) + 1); if (!/^[A-Za-z_$][\w$]*$/.test(name)) return result; const line = (this.context.getFileLines?.(ref.filePath) ?? this.context.readFile(ref.filePath)?.split(/\r?\n/))?.[ref.line - 1]; diff --git a/src/resolution/js-constructor-methods.ts b/src/resolution/js-constructor-methods.ts new file mode 100644 index 0000000000..ccaa9dcaf0 --- /dev/null +++ b/src/resolution/js-constructor-methods.ts @@ -0,0 +1,97 @@ +import type { Node } from '../types'; +import type { ResolutionContext, ResolvedRef, UnresolvedRef } from './types'; +import { blankStringContents, stripCommentsForRegex } from './strip-comments'; + +const LANGUAGES = new Set(['javascript', 'jsx', 'typescript', 'tsx', 'arkts']); +const sourceCache = new WeakMap>(); +export function clearJsConstructorMemos(context: ResolutionContext): void { sourceCache.delete(context); } + +function lines(ref: UnresolvedRef, context: ResolutionContext, raw = false): string[] { + let files = sourceCache.get(context); + if (!files) sourceCache.set(context, files = new Map()); + let cached = files.get(ref.filePath); + if (!cached) { + const source = context.getFileLines?.(ref.filePath)?.join('\n') ?? context.readFile(ref.filePath) ?? ''; + cached = { raw: source.split(/\r?\n/), code: blankStringContents(stripCommentsForRegex(source, 'javascript')).split(/\r?\n/) }; + files.set(ref.filePath, cached); + } + return raw ? cached.raw : cached.code; +} +const escape = (name: string): string => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +/** A method on one particular class, never the union of all its namesakes. */ +export function methodOnJsClass(cls: Node, method: string, ref: UnresolvedRef, context: ResolutionContext, + seen = new Set()): ResolvedRef | null { + if (seen.has(cls.id) || seen.size >= 16) return null; + seen.add(cls.id); + const members = context.getMethodMatches?.(cls.name, method, ref.language) ?? context.getNodesByName(method); + const own = members.filter(n => n.kind === 'method' && n.filePath === cls.filePath && + n.qualifiedName === `${cls.qualifiedName}::${method}` && n.startLine >= cls.startLine && n.endLine <= cls.endLine); + if (own.length === 1) return { original: ref, targetNodeId: own[0]!.id, confidence: 0.95, resolvedBy: 'instance-method' }; + if (own.length > 1) return null; + for (const parent of context.getSupertypeNodes?.(cls.id) ?? []) { + if (parent.kind !== 'class') continue; + const match = methodOnJsClass(parent, method, ref, context, seen); + if (match) return match; + } + return null; +} + +/** undefined: no imported constructor binding; null: an import we cannot prove. */ +export function matchImportedConstructorMethod(type: string, receiver: string, method: string, + ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | undefined { + if (!LANGUAGES.has(ref.language) || !/^[\w$]+$/.test(receiver)) return undefined; + if (!context.resolveImport || !context.getNodeById) return undefined; + const binding = context.getImportMappings?.(ref.filePath, ref.language)?.find(i => i.localName === type && !i.isNamespace); + if (!binding) return undefined; + const scopes = context.getNodesInFile(ref.filePath).filter(n => + (n.kind === 'function' || n.kind === 'method') && n.startLine <= ref.line && n.endLine >= ref.line); + const code = lines(ref, context); + const begin = scopes.reduce((line, n) => Math.max(line, n.startLine), 1); + const prefix = code.slice(begin - 1, ref.line).join('\n'); + // The blanker intentionally hides executable template substitutions too. + // Leave that unsupported binding shape to the existing resolver, rather + // than miss a reassignment inside it while adding an alias-derived edge. + if (lines(ref, context, true).slice(begin - 1, ref.line).join('\n').includes('${')) return undefined; + const t = escape(type), r = escape(receiver); + if (!new RegExp(`\\b(?:const|let|var)\\s+${r}\\s*=\\s*new\\s+${t}\\s*\\(`).test(prefix)) return undefined; + // Do not recover an import through a shadowing parameter/local, or recover + // the original instance after reassignment. These guards are conservative: + // unsupported binding forms stay unresolved instead of choosing a namesake. + if (scopes.some(n => new RegExp(`(?:[(,]|\\.\\.\\.)\\s*${t}\\s*(?:[:=,)]|$)`).test(n.signature ?? ''))) return null; + const scopeCode = code.slice(begin - 1, Math.max(ref.line, ...scopes.map(n => n.endLine))).join('\n'); + if (new RegExp(`\\b(?:const|let|var|class|function)\\s+${t}(?![\\w$])`).test(scopeCode)) { + const localClass = context.getNodesInFile(ref.filePath).filter(n => n.kind === 'class' && n.name === type && + n.startLine >= begin && n.startLine <= ref.line).sort((a, b) => b.startLine - a.startLine)[0]; + // A known local class belongs to ordinary local receiver resolution; the + // import recovery must not introduce a new target through a shadow name. + return localClass ? undefined : null; + } + if ([...prefix.matchAll(new RegExp(`(?)|[+*/%&|^\\-]=|\\*\\*=|&&=|\\|\\|=|\\?\\?=|\\+\\+|--)`, 'g'))].length !== 1) return null; + if (new RegExp(`(?:\\[[^\\]\\n]*\\b${r}\\b[^\\]\\n]*\\]|\\{[^}\\n]*\\b${r}\\b[^}\\n]*\\})\\s*=`).test(prefix)) return null; + const imported = context.resolveImport?.({ ...ref, referenceName: type, referenceKind: 'instantiates' }); + const cls = imported && context.getNodeById?.(imported.targetNodeId); + return cls?.kind === 'class' ? methodOnJsClass(cls, method, ref, context) : null; +} + +/** Match explicit super calls by their call-site position and resolved parent. */ +export function matchJsSuperCall(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | undefined { + if (!LANGUAGES.has(ref.language) || ref.referenceKind !== 'calls') return undefined; + const code = lines(ref, context, true); + const head = code[ref.line - 1]?.slice(ref.column) ?? ''; + if (!/^super\b/.test(head)) return undefined; + const at = stripCommentsForRegex([head, ...code.slice(ref.line)].join('\n'), 'javascript'); + const call = at.match(/^super\s*\.\s*([\w$]+)\s*\(/); + if (!call) return null; // an explicit but unsupported super expression is never a name guess + const cls = context.getNodesInFile(ref.filePath).filter(n => n.kind === 'class' && + (n.startLine < ref.line || (n.startLine === ref.line && n.startColumn <= ref.column)) && + (n.endLine > ref.line || (n.endLine === ref.line && n.endColumn >= ref.column))) + .sort((a, b) => b.startLine - a.startLine || b.startColumn - a.startColumn)[0]; + if (!cls) return null; + for (const parent of context.getSupertypeNodes?.(cls.id) ?? []) { + if (parent.kind !== 'class') continue; + const match = methodOnJsClass(parent, call[1]!, ref, context); + if (match) return match; + } + return null; +} diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 6df55a82ef..805ba61552 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -9,6 +9,7 @@ import * as path from 'path'; import { Language, Node } from '../types'; import { UnresolvedRef, ResolvedRef, ResolutionContext, ImportMapping, isSupertypeTarget, CPP_DEFINE_SIGNATURE, isInheritanceRef, isImportableKind } from './types'; import { blankStringContents, stripCommentsForRegex } from './strip-comments'; +import { matchImportedConstructorMethod } from './js-constructor-methods'; import { JS_BUILT_INS, JS_BUILTIN_METHODS, TS_PRIMITIVE_TYPES } from './js-builtins'; import { SWIFT_TYPE_PATH_CALL, resolveSwiftTypePathCall } from './swift-type-visibility'; import { dartImportPrefixes, dartLibrarySees, dartPrefixSees, inSameDartLibrary } from './dart-libraries'; @@ -11933,6 +11934,8 @@ export function matchMethodCall( inferredType = awaited.name; } if (inferredType) { + const importedConstructor = matchImportedConstructorMethod(inferredType, objectOrClass!, methodName!, ref, context); + if (importedConstructor !== undefined) return importedConstructor; // A Go local bound from a type assertion calls a method of the type Go // finds for it, or nothing the project declares: `flusher.Flush()` after // `flusher, ok := w.(http.Flusher)` went to a project type named after it. From e91e3fccdb9a94bbf44aba9566b7785a310cd685 Mon Sep 17 00:00:00 2001 From: Puma7 <88577358+Puma7@users.noreply.github.com> Date: Tue, 13 Oct 2026 00:02:03 +0200 Subject: [PATCH 2/2] Normalize changelog line endings without changing content --- CHANGELOG.md | 2874 +++++++++++++++++++++++++------------------------- 1 file changed, 1437 insertions(+), 1437 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75ebdf3568..c9b69611e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,1437 +1,1437 @@ -# Changelog - -All notable changes to CodeGraph are documented here. Each entry also ships as -a [GitHub Release](https://github.com/colbymchenry/codegraph/releases) tagged -`vX.Y.Z`, which is where most people will look. - -Each release opens with a short **Highlights** list — the handful of things most -users will notice — followed by the full notes. - -This project follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) -and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -### Fixes - -- JavaScript and TypeScript calls through an imported constructor alias now reach that class's own or inherited method, and an overridden method's explicit superclass call reaches the parent while genuine recursion remains linked. Existing unaliased receiver inference is preserved, and source preprocessing is deferred until needed; re-index after upgrading. (#2497) -- In VB.NET, every member of a `Structure` is now indexed, including its fields, properties, methods, constructors and nested enums. Before, only the first member was, so the rest could not be found and their callers looked empty. -- In VB.NET and C#, what a property's `Get` and `Set` code calls, creates and reads now belongs to that property, as do C#'s `get => …` accessors and `=> …` property bodies. Before, it was dropped, so a method used only from a property looked unused. -- In VB.NET, a field or property initializer like `= Compute()` or `As New List(Of Order)` now links what it calls and creates, and so do a `Custom Event`'s `AddHandler`, `RemoveHandler` and `RaiseEvent` blocks. Re-index VB.NET and C# projects after upgrading. -- In C#, a field or property initializer like `private readonly ILogger _log = LogManager.GetLogger(typeof(X));` or `public List Items { get; } = new();` now links what it calls, creates and reads, including inside a lambda, and a target-typed `new()` there counts as creating the declared type. Before, initializers were skipped, so a method or class used only from one — like a converter created in a static list — looked unused, and a method passed as a value there was credited to the whole class instead of the field or property. -- In C# and VB.NET, a constant no longer stands in for a type with the same name: `new Station { … }` links to class `Station` even when another class declares `private const string Station`, `new Version(…)` no longer links to a `const string Version`, and a field like `static readonly Meter Meter` no longer points at itself. Thanks @EvanYu1980 for the report and @drakeo338. (#2337) -- In VB.NET, a call on a variable, parameter, field or property now reaches a method of the type it is declared with — one that type inherits, or an extension method written for it — and nothing when that type comes from outside your project, instead of any project method that merely shares the name. -- In VB.NET, a call to one of .NET's own methods such as `Add`, `Contains`, `Clear` or `Dispose` on a value whose type isn't known is no longer linked to a project method of that name, unless the value is named after that method's class. -- In VB.NET, a type name is looked up the way VB.NET does it — through the namespaces around it, the file's and project's `Imports` (aliases included), and the caller's own project — so a class declared in several namespaces or projects no longer draws every call to whichever copy was indexed first, and two candidates nothing tells apart get no link at all. -- In VB.NET, a call with no receiver, or on `Me`, now reaches only a member of the class it is written in, of a class it inherits, or of a Module, and no longer a nearby class's member of the same name. -- VB.NET designer code such as `New System.Drawing.Point(…)` or `Me.Size = New System.Drawing.Size(…)` is no longer linked to a project type or member that only shares the name, and a class nested inside another is matched by its bare name only from inside that class or one that inherits it. -- Upgrading CodeGraph while an agent session is open no longer leaves the old version's background server in charge of your project: the first session started from the new install stops it and starts a current one in its place, even while sessions opened before the upgrade are still running. That old server could no longer load the language parsers the upgrade removed, so it saved every file it re-indexed with no symbols, while new sessions could only read the index beside it without keeping it up to date. A background server from a newer install is never stopped, and sessions opened before the upgrade keep the old version until you restart them. Thanks @lipchey for the report. (#2335) -- A file is no longer saved with no symbols when its language parser can't be loaded, which is what happened to every file a background server re-indexed after an upgrade removed its install: the file keeps what it had and is indexed again once the parser loads, and files an earlier version emptied this way are re-indexed by the next sync. A background server also exits on its own once its install is upgraded or removed, so the next session starts one from the current install. Thanks @lipchey for the report. (#2335) -- `codegraph status` no longer says the index is up to date while indexed files are missing their symbols: it now names files the parser couldn't read and files stored without their symbols (which `codegraph sync` repairs), `status --json` counts both, and `codegraph files --json` lists each file's recorded errors. Thanks @lipchey for the report. (#2336) -- Indexing large Python projects is much faster again and needs less memory: since 1.6.2, resolving Python references re-read source files over and over, so a project the size of CPython took several times as long to index. The graph it builds is unchanged. Thanks @bompus for the report. (#2332) -- In VB.NET, reading or setting a `Shared` field or property through its class or module name, like `AppSession.SessionId`, `AppSession.CurrentUser = "demo"` or `AppSession.Items(0)`, now counts as a use of that member and of the class, and so do reading an `Enum` value like `Mode.Fast` and calling a `Shared` function without parentheses. Before, only calls written with parentheses were linked, so `codegraph callers` on such a field, property or class came back empty and impact missed most of the code that depends on it. A local, parameter or field that only shares a class's name is not mistaken for the class. Thanks @serkanince for the report and @ChrisPrapas. (#2305) -- In Vue and Nuxt components, calls written in the template (`{{ formatDate(post) }}`, `:to="getAccountRoute(account)"`, `@click="save(item)"`) and calls in a destructuring declaration at the top of `