VSExtension: TypeScript StructureWebStorm-style refactorings and test impact analysis for TypeScript, JavaScript and React in VS Code. Everything is deterministic, offline static analysis using the TypeScript Compiler API and Language Service. No AI and no network access. All commands are in the Command Palette under TypeScript Structure, and in the editor's right-click menu under TypeScript Structure. Analyze or Refactor This… (
Analyses show their results in the TypeScript Structure view in the Activity Bar; live inspections report to the Problems panel. See Working with results for what the view offers. RefactoringsHow refactorings are appliedEvery refactoring is computed on an in-memory copy of the project, so nothing is written until you confirm:
Change Signature (
|
| Intention | Example |
|---|---|
| Invert 'if' condition | if (a === b) x(); else y(); → if (a !== b) y(); else x(); |
| Replace 'if else' with '?:' | if (c) { return a; } else { return b; } → return c ? a : b; (also assignments, and if (c) return a; return b;) |
| Replace '?:' with 'if else' | for return, assignments and const initializers (const becomes let) |
| Flip '?:' | a ? b : c → !a ? c : b |
| Merge nested 'if's | if (a) { if (b) {…} } → if (a && b) {…} |
| Split '&&' into nested 'if's | the reverse |
| Replace 'if' chain with 'switch' | if (k === 'a') … else if (k === 'b' \|\| k === 'c') … else … |
| Replace '&&' with '||' (De Morgan) | !(a && !b) → !a \|\| b |
| Flip comparison | a < b → b > a |
Negation is smart throughout: !x becomes x, comparisons flip (=== ↔ !==, < ↔ >=), and && and || follow De Morgan's laws, with parentheses added only where precedence needs them.
Loops, functions and expressions
| Intention | Example |
|---|---|
| Replace indexed 'for' with 'for...of' | for (let i = 0; i < items.length; i++) use(items[i]) → for (const item of items) use(item) |
| Replace 'forEach' with 'for...of' | return; becomes continue;, and (x, i) uses .entries() |
| Replace '+' with template literal | 'Hi ' + name + '!' → `Hi ${name}!`, keeping 1 + 2 + 'a' numeric |
| Convert to arrow function | function expressions |
| Convert arrow function to block or expression body | both directions, handling object literals |
| Add / remove braces | if, else, for, while, do |
| Split into separate declarations | const a = 1, b = 2; |
| Split into declaration and assignment | const x = f(); → let x; x = f(); |
Safety checks. An intention is not offered when it would change behaviour or lose code:
- An
ifchain containing an unlabeledbreakis not turned into aswitch. - An index loop that uses the index for anything but reading the element is not converted.
- A
forEachcallback that isasyncor returns a value is not converted. - A function that uses
thisorargumentsis not made an arrow. - Braces are not removed around
let/constdeclarations, or where a danglingelsewould re-bind. - Nothing is rewritten when that would drop comments.
Indentation follows the editor's tab settings. To hide individual intentions, list their ids in tsStructure.intentions.disabled.
Inspections
Inspections explain why code behaves as it does, following values, promises and errors across functions and files instead of repeating single-file lint rules. Two of them run live on visible files; the others are analyses you run at the cursor, with results in the TypeScript Structure view.
Live inspections
Findings appear in the Problems panel with quick fixes in the lightbulb, plus:
- Fix all problems of one inspection in the file, or every preferred fix with
"editor.codeActionsOnSave": { "source.fixAll.tsStructure": "explicit" }. - Suppress for a line (
// ts-structure-ignore-next-line missingAwait) or a file (// ts-structure-ignore-file missingAwait). Without ids, every inspection is suppressed. - Explain, which opens the matching analysis, for example the Promise Rejection Analyzer for a dropped promise.
- Severity per inspection in
tsStructure.inspections.severity(offdisables one, as does Turn off '…' inspection in the lightbulb, with Undo);tsStructure.inspections.enabledturns them all off. - Timing:
tsStructure.inspections.runistype(after a pause in typing,tsStructure.inspections.delaymilliseconds) orsave, which inspects only when the file is saved, for big projects.
Inspect Code… runs every inspection, including custom ones, over the current file or the whole project, with cancellable progress. Results are grouped by inspection and file, and fixable rows have an Apply Fix button. The status bar shows the count for the active file; click it to inspect the file.
Inspections: missingAwait
The Interprocedural Missing Await detector flags promises that are created and then lost:
- A call statement whose promise nobody awaits, returns or
.catch()es, naming the origin when it comes through wrapper functions: loadUser() (from fetchUser()) is not awaited. - A promise handed to a helper whose parameter doesn't expect one and which never awaits it, following it through further helpers: relay(value) → logLater(value) never awaits it.
- A promise used as a condition (always truthy) or in a template or string (
[object Promise]). forEach(async …), andmap(async …)whose promises are discarded.return promiseinsidetryin an async function, which bypasses thecatch.
Fixes add await, mark the call as intentionally not awaited with void, or wrap in await Promise.all(…).
Inspections: typeAssertion
The Type Assertion Inspector classifies as, <T> and ! assertions:
- Unnecessary: the value already has that type, or
!on a value that is never null. Shown faded, with a fix to remove it. - Widening: the value already fits the type. A fix switches to
satisfies, which keeps the check. - Unchecked narrowing: says exactly what it assumes, such as the value is never number or properties radius exist.
- Unsafe:
as any, assertions onanyvalues such asJSON.parse(…) as T, and double assertions throughunknown. - Non-null
!on a nullable value: a hint, with a fix to use?.where it's a property access.
Analyses at the cursor
| Analysis | Place the cursor on | What it shows |
|---|---|---|
| Data Flow Path Explorer | a variable, parameter or property | Every definition, update, mutation (push, .x = …), condition, hand-off to a call and read, grouped by function in source order, with the narrowed type at each step. For a parameter, the values its call sites pass. |
| Mutation Tracking | anywhere in a file | Functions that change state they don't own, both by function and by the shared state (module variables, imports, this, globals) and who changes it. |
| Side-Effect Analyzer | a function or call | Mutations of arguments, the instance, module or imported state, closures and globals; I/O (console, network, storage, fs); timers; nondeterminism (Math.random, Date.now). Effects of called functions are included with the call chain, and argument mutations map onto what the caller passed. |
| Nullability Path Explorer | a variable, property access or call | Whether it can be null or undefined there, every path that produces it (declarations, let x;, assignments, optional parameters, call sites passing undefined or omitting the argument, nullable returns, falling off the end of a function, optional chaining), or the guards that rule it out. |
| Promise Rejection Analyzer | an async or promise-returning function | Throws, awaits and Promise.reject that can reject, then every path the promise takes through callers, .then/.catch/.finally, Promise.all/allSettled, variables and callbacks, ending in caught (and what the catch block does), handled, or unhandled. |
| Exception Flow Visualizer | a function | Errors thrown directly or by called functions (including JSON.parse, new URL and similar), how each catch, finally and .catch() treats them (swallows, logs, rethrows, transforms, recovers), and how callers handle what escapes. |
| Dead Export Analyzer | anywhere in the project | Modules no entry point reaches, exports nothing reachable uses, and exports only used in their own module. export * and renamed re-exports are followed. Rows offer Safe Delete and Remove export Keyword. |
| Structural Duplicate Finder | anywhere (file or project) | Runs of statements with the same structure even when variables are renamed and literal values differ, reported once at their largest extent, with the renamings between copies. Compare Copies opens them side by side. |
Entry points for dead exports come from tsStructure.deadExports.entryPoints (globs), or else package.json (main, module, exports, bin, mapped from dist/ back to src/), or else conventional index/main files. Test files count as entry points unless tsStructure.deadExports.testFilesAreEntryPoints is off. The duplicate finder's sensitivity is set by tsStructure.duplicates.minStatements and minTokens; test files are skipped unless ignoreTestFiles is off.
Set tsStructure.codeLens.sideEffects, or run Toggle Side-Effect CodeLens, to show a lens above each function: pure, or a summary like mutates arguments · I/O. Click it for the full analysis.
Navigation and search
| Feature | What it does |
|---|---|
Semantic Find Usages (Alt+F7) |
References grouped into definitions, calls, JSX usages, writes, reads, inheritance, type references, imports and exports, per file, with test files marked. |
| Re-export Chain Navigator | Follows a name from the cursor through imports, renamed re-exports and export * barrels to its declaration, lists every module that exports it and under which name, and which paths importers use. |
| Inheritance Hierarchy Explorer | Supertypes and nested subtypes or implementations of a class or interface, and for each member, which subtypes override it and whether they call super. |
| Override Impact Analyzer | For a base member: what it overrides, overrides that call super (affected) or replace it, subtypes that inherit it unchanged, call sites split by the static type they dispatch through, super calls, and impacted tests. |
| Dependency Path Finder | Why module A depends on module B: every shortest chain of imports with the names each hop brings in, whether it is type-only (erased at runtime, with the longer runtime path if there is one), or the reverse direction when there is none. Also in the Explorer's right-click menu. |
| Dependency Graph Diff | Compares the project's import graph with a Git revision (HEAD, the branch point with main, recent commits, branches or any ref): new and removed cycles, added, removed and changed imports, added and removed modules, and the biggest changes in importers. |
| Argument Propagation Explorer | For a parameter, destructured prop or argument: where its values come from (literals, callers' parameters, locals and their assignments, JSX attributes) and where they go (other functions' parameters, component props, derived locals, returns and stores). Depth is set by tsStructure.argumentFlow.maxDepth. |
Structural Search and Replace
Structural Search… opens a builder (prefilled with the selection, if any). Patterns are code with placeholders:
$name$matches any expression, identifier or statement; using a name twice requires the same code ($x$ === $x$).$name*$matches any number of arguments, elements, parameters or statements (console.log($args*$),if ($c$) { $body*$ }).$_$matches anything without binding.- Several statements match consecutive statements.
- Formatting, parentheses and optional type annotations don't affect matching.
Each placeholder can be constrained by a regular expression on its type (^Promise<, string|number) or its text, optionally inverted. Results list in the builder and the TypeScript Structure view.
Replace uses the same placeholders in a template. All matches are rewritten in a copy of the project, which is type-checked first: any new error is listed before anything is written, and changes open in the Refactor Preview.
Save as Inspection turns the pattern into a live inspection with a message (placeholders are filled in from each match), a severity and, from the replacement, a quick fix and a fix-all. They are stored in tsStructure.customInspections and managed with Manage Custom Inspections….
The builder starts from ten examples (async forEach, JSON deep clone, == null, promise conditions, useEffect without dependencies, …) and remembers your last 15 searches.
To run a search from a key, click Copy as Keybinding in the builder and paste the entry into keybindings.json, then fill in the key. The entry looks like this:
{
"key": "ctrl+alt+f",
"command": "tsStructure.structuralSearch",
"args": { "pattern": "$list$.find($callback$)", "constraints": [{ "name": "list", "type": "^LineItem\\[\\]$" }], "run": true }
}
Working with results
The TypeScript Structure view keeps every analysis's report:
- Back and forward through the last 20 reports (
tsStructure.results.historySize), and re-run the current one. Re-running follows the symbol through edits, and the last report is restored after a reload. - Filter rows by text, Show Groups… to hide whole groups, and go to the next or previous result.
- Highlights of the result's locations in open editors: definitions outlined, writes and reads shaded (
tsStructure.highlightResults). - Copy as Markdown or Export to Markdown…, Open to the Side, and Copy Location on a row.
- The view opens when an analysis finishes; set
tsStructure.results.focusViewtofalseto stay where you are and get a status bar message instead. - A badge with the number of rows that need attention, how long the analysis took, and a banner when files change after the analysis. Opening a result in a changed file asks first.
Configuration
Open the settings with TypeScript Structure: Open Settings or from the ⋯ menu of the TypeScript Structure view. They are grouped into sections: Scope, Features, General, Inspections, Intentions, Test Impact, Dead Exports, Duplicates, Argument Propagation and Code Lens. Settings about files, such as the scope, inspection severities and dead-export entry points, can differ per folder of a multi-root workspace.

Choosing which files are analyzed
By default every file in the compiler project is analyzed. To leave generated code, vendored code or old code out:
- In the Explorer, right-click a file or folder (or several) and choose Exclude from TypeScript Structure Analysis. The message that follows has Undo.
- To see or change the whole scope, run TypeScript Structure: Edit Analysis Scope… (also in the ⋯ menu of the TypeScript Structure view). Choose Exclude Files… or Only Analyze… to add a pattern, or select a pattern to remove it.

The scope is stored in two settings, with globs relative to the workspace folder. A plain folder path such as src/legacy covers everything below it.
{
"tsStructure.include": ["src"],
"tsStructure.exclude": ["**/generated/**", "src/legacy"]
}
tsStructure.include limits analysis to the matching files when it is not empty, and tsStructure.exclude is applied after it. The scope applies to live inspections, Inspect Code, the type assertion audit, structural search and replace, duplicate detection, project-wide mutation tracking and dead-export results. Analyses that follow one symbol, such as Find Usages or Test Impact Analysis, still look at the whole project, so they never miss a use.
Turning features on and off
Every feature can be switched off on its own, for example when another extension already covers it:
- Run TypeScript Structure: Choose Features… from the Command Palette, or from the ⋯ menu of the TypeScript Structure view.
- Untick the features you don't use and press
Enter.

A feature that is off disappears from the editor's right-click menu, the Command Palette, Analyze or Refactor This…, its keybindings, lightbulb entries and CodeLens. Running one of its commands anyway, from a keybinding or another extension, offers to turn it back on.
Each switch is also a setting, tsStructure.features.<name>, so a workspace can turn features off for everyone in .vscode/settings.json:
{
"tsStructure.features.testImpact": false,
"tsStructure.features.intentions": false
}
The names are testImpact, refactorings, intentions, inspections, dataFlow, sideEffects, nullability, promiseRejections, deadExports, exceptionFlow, duplicates, usages, reexports, structuralSearch, hierarchy, dependencyPath, graphDiff and argumentFlow.
Step-by-step guides
Each guide uses the small bookshop project in docs/demo. Open that folder in VS Code to follow along. The screenshots are captured from a real VS Code session with npm run screenshots.
Every analysis can be started three ways: from the editor's right-click menu under TypeScript Structure, from the Command Palette (Ctrl+Shift+P, then type its name), or from Analyze or Refactor This… (Ctrl+Alt+Shift+T). Results open in the TypeScript Structure view in the Activity Bar.
Data Flow Path Explorer

Put the cursor on a variable, parameter or property. In the demo, use
totalincheckoutTotal(src/cart.ts).Run Data Flow Path Explorer.
Read the steps in the view, grouped by function in source order:
- a pencil for each assignment;
- an eye for each read;
- an arrow where the value is passed to another function, with the parameter it becomes.
Each function's row summarizes what happens there, for example 3 writes, 1 check. Each step shows its line. A type is shown where it differs from the declared type, so narrowing stands out. The step under the cursor is selected and marked at cursor.
Click a step to jump to it, or use the up and down arrows in the view's title bar to walk through them. The editor shades writes and reads of the value while the report is open.
To keep tracing into a called function, click the arrow button (Follow Into Call) on a hand-off row. It runs Data Flow on the parameter there.
On a parameter, an extra Incoming values from call sites group lists what every caller passes, including omitted arguments and defaults.
Mutation Tracking
![]()
- Open the file you want to audit, for example
src/cart.ts. If the cursor is on a module variable or an import, that state is selected in the report. - Run Mutation Tracking (Active File), or Mutation Tracking (Project) to cover every file. In the project report each function names its file.
- Functions that mutate state lists each function that changes something it doesn't own. Expand one to see each change. A change made inside a called function shows the call chain, such as changes module state events (.push()) via track().
- Shared state and who changes it turns the same data around: one row per module variable, import, global or
thisfield, with every function that writes it. Use it to find the unexpected writer of a piece of state. - Click any row to open the line. The editor shades every write while the report is open.
- Click the lightning button (Show All Side Effects) on a function row to run the Side-Effect Analyzer on it. That also shows I/O, timers and other effects besides mutations.
Nullability Path Explorer

- Put the cursor on a variable, property access or call that might be
nullorundefined. In the demo, usecouponinapplyCoupon(src/pricing.ts). On a function's name, such asfindCoupon, it explains which of its returns givenullorundefined. - Run Nullability Path Explorer.
- The first row gives the verdict: the type, and whether the value may be null or undefined here or is never null or undefined here.
- Paths that produce null or undefined traces every source back to its origin. Examples: an optional parameter, a caller that omits an argument, a
return undefined, a function that can reach its end without returning, or a library call such asArray.find(). - If the value is safe at the cursor, Ruled out by lists the checks that narrow it, such as
if (!coupon) return amount;. - Uses lists every read of the variable and what it is at that point: safe (narrowed), check (a test such as
!couponorcoupon?.code), or may be undefined. A use that may still be null is one that needs a guard. - You can also start from a TypeScript error. On Object is possibly 'undefined' or 'user' is possibly 'null', open the lightbulb (
Ctrl+.) and choose Explain why 'user' may be null or undefined.
Promise Rejection Analyzer

- Put the cursor on an async function, or a function that returns a promise. In the demo, use
saveOrderinsrc/orders.ts. On a call, such asplaceOrder(…)inquickReorder, the report starts with This call, where that call's promise goes. - Run Promise Rejection Analyzer.
- Rejection sources lists what can make the promise reject:
throwstatements, awaited calls that can reject themselves, andPromise.reject. - Where rejections go follows the promise through every caller,
.then()/.catch(),Promise.all, variables and callbacks. Each path ends in one of these:- a red unhandled row: a call that is not awaited, a
void, or a callback whose caller ignores returned promises, such assetTimeout; - a caught in … row that says what the catch block does: rethrows, recovers, logs, or swallows;
- a green handled row.
- a red unhandled row: a call that is not awaited, a
- Unhandled, at the top, lists every unhandled end with the callers it passes through. Click a row to go to the call that drops the rejection, or click its lightbulb button (Show Fixes) to open the fixes there: add
await, mark it intentionally ignored withvoid, or wrap it.
Missing Await (live inspection)

- Nothing to run: the inspection checks every TypeScript and JavaScript file that is visible in an editor. Open
src/orders.tsin the demo. - A dropped promise is underlined, here
placeOrder(id, items, payment);inquickReorder. Hover it to read why. The message names the original function when the promise comes through wrappers. Async callbacks passed to APIs that ignore their promise, such assetTimeout(async () => …)or.addEventListener('click', async …), are flagged too, unless the callback catches its own errors. - Press
Ctrl+.on the underline for fixes:- Add await, in an async function, or Make … async and await the call elsewhere. That also wraps a declared return type in
Promise<…>; - Mark as intentionally not awaited (void);
- Handle the rejection with .catch(), which works outside async functions too;
- Return the promise to the caller, when the call is the last statement of a function without a declared return type;
- Trace where this promise's rejection goes…, which opens the Promise Rejection Analyzer;
- suppress the warning for the line or the file.
- Add await, in an async function, or Make … async and await the call elsewhere. That also wraps a declared return type in
- The Problems panel (
Ctrl+Shift+M) lists the findings, and the status bar shows the count for the active file. - For helpers that are meant to run in the background, such as analytics or logging, list them in
tsStructure.missingAwait.ignoreinstead of suppressing every call. For example,["track", "logger.*"]. - To check the whole project, run Inspect Code… and choose the project. Results open in the TypeScript Structure view, and fixable rows have an Apply Fix button.
Type Assertion Inspector (live inspection)

- Like Missing Await, this inspection runs on every visible TypeScript or JavaScript file. Open
src/api.tsin the demo. - Each
as,<T>and!is classified:- Unchecked narrowing (warning), with what it assumes, such as it assumes properties title, price, weight exist;
- Unsafe (warning):
as any, double assertions throughunknown; - Unverified (information): an assertion on an
anyvalue, such asJSON.parse(body) as Book; - Widening (information);
- Non-null
!on a nullable value (hint); - Unnecessary (hint, shown faded), such as
book as Bookwhenbookis already aBook.
- Hover a finding to read the explanation, or press
Ctrl+.for fixes:- Remove unnecessary assertion;
- Use 'satisfies Shape' for widening, which keeps the compiler's check;
- Use optional chaining '?.' instead of
!; - Use 'as unknown' instead of
as any, so every use has to narrow the value; - Show where … comes from… runs Data Flow on the asserted value, and Explain why … may be null or undefined… runs the Nullability Path Explorer on a
!; - suppression comments for the line or file.
- Hints don't appear in the Problems panel. Run Inspect Code… on the file to list every finding, hints included, in the TypeScript Structure view. To review a codebase, run Audit Type Assertions (Project). It groups every assertion from riskiest (Unsafe) to harmless (Unnecessary), one row per line, with the fix button where there is one.
- Change how loud each inspection is with
tsStructure.inspections.severity, for example{ "typeAssertion": "info" }, or"off"to disable it. - Tests often need assertions for mocks and fixtures. Turn on
tsStructure.typeAssertion.ignoreTestFilesto skip test files, such as*.test.tsor files that import a test framework.
Dead Export Analyzer

- Open any file in the project and run Dead Export Analyzer. The whole project is analyzed.
- Check Entry points at the bottom. The analyzer starts from the files named in
package.json(main,module,exports,bin, heresrc/main.ts) and from test files. Without those it falls back tosrc/index.ts,src/main.tsand similar. SettsStructure.deadExports.entryPoints(globs) if the guess is wrong. - Unreachable modules are files no entry point imports, directly or indirectly, such as
legacy.ts. The trash button (Safe Delete File…) checks for remaining references before deleting. - Unused exports are exported names nothing reachable imports. Each row's trash button runs Safe Delete… on the declaration. Rows say what kind of declaration it is, mark interfaces and type aliases as type, and name any barrel that still forwards the name (re-exported by index.ts).
- Exported but only used locally are names used inside their own module only. The button on the row (Remove export Keyword) keeps the code and drops
export. The button on a file row removes every one in that file at once. - Exports reached through
export *barrels, renamed re-exports,import * as nsand dynamicimport()count as used. Turn offtsStructure.deadExports.testFilesAreEntryPointsto also find exports that only tests use. - To keep an export that is public API on purpose, such as a library function no code in the repository calls, document it with
/** @public */or/** @api */. It is no longer reported.
Exception Flow Visualizer

- Put the cursor on a function or method. In the demo, use
parseConfiginsrc/config.ts. - Run Exception Flow Visualizer.
- Thrown here lists
throwstatements and library calls known to throw. Examples:JSON.parse()(SyntaxError on invalid JSON),JSON.stringify(),new URL(),decodeURIComponent(), Node'sfs.*Syncfile calls andexecSync(). - Thrown by called functions lists errors that come out of functions this one calls, with the call chain down to the original
throw. Each row says whether the error is caught here or escapes to the caller. Click the arrow button (Follow Into Call) to open the exception flow of the called function. - Error types names the error classes that can come out, such as ConfigError, SyntaxError. That's what a
catchwould need to handle. Catch and finally blocks says what each handler does: recovers, rethrows, transforms (throws a different error), logs and continues, or swallows (an emptycatch). - Escaping errors reach callers follows what escapes up through every caller, until it is caught, or reaches code with no callers (errors are uncaught). When it enters an async function it becomes a rejection, and the report keeps following that promise through awaits,
.catch()and dropped calls. In the demo,currencyOrDefaultswallowsparseConfig's errors throughreadSetting, which is worth a look. - When some path ends uncaught, an Uncaught group at the top lists each one with the callers it passes through.
Structural Duplicate Finder

- Open a file in the project and run Find Structural Duplicates….
- Choose Whole Project or Active File. With statements selected, Copies of the Selection instead lists every place in the project with the same structure as the selected code.
- Each Duplicate group is a run of statements with the same structure. It shows the number of lines, the number of copies, and how many lines are repeated, which is what consolidating would remove. The biggest payoff is listed first. Copies count even when variables are renamed or literal values differ. In the demo,
revenueinreports.tsis a copy ofcalculateSubtotalinpricing.ts. - Expand a group to see each copy and the function it's in. The last row says what differs between them, such as total ↔ sum, item ↔ line. Those are the parameters a shared function would need. The wrench button on a copy (Refactor…) selects that code and opens the refactor menu, where VS Code's Extract to function turns it into a shared function.
- Click the diff button on a group (Compare Copies) to open the first two copies side by side. Common indentation is removed so they line up.
- Tune sensitivity with
tsStructure.duplicates.minStatements(default 3) andminTokens(default 40). Turn ontsStructure.duplicates.matchRenamedPropertiesto also match copies that use other property names. For example,parcelWeightinshipping.tssums.weightthe waycalculateSubtotalsums.price. Test files are skipped unlesstsStructure.duplicates.ignoreTestFilesis off.
Side-Effect Analyzer

- Put the cursor on a function name, inside a function body, or on a call. In the demo, use
addToCartinsrc/cart.ts. - Run Side-Effect Analyzer.
- Effects are grouped by kind, each with how many are direct and how many come through calls:
- Mutates arguments, Mutates the instance, Mutates module state or imported state, Mutates captured variables, Writes globals;
- Input/output: console, network, storage,
fs, dialogs; - Schedules work: timers;
- Nondeterministic:
Math.random(),Date.now(),new Date(). - Calls it could not follow: calls to a callback parameter or a function-typed property, which could do anything. A function like that is never labeled pure.
- An effect inherited from a called function says so (changes module state events (.push()) via track()). Expand it to see the call chain, or click its lightning button (Show All Side Effects) to analyze the function it comes from. A mutation of a parameter in a callee is mapped to what this function passed. By call lists the same inherited effects per call, such as track() input/output, mutates module state, to see which call to change or isolate.
- To see this at a glance in the editor, turn on
tsStructure.codeLens.sideEffects. Every function then gets a lens: pure, or a summary such as mutates arguments · mutates module state · I/O. That includes methods, class fields holding arrow functions (handle = () => …) and object properties holding functions ({ onSave: () => … }). Click a lens to open the full analysis. To keep lenses only on functions with effects, turn offtsStructure.codeLens.sideEffectsShowPure.
Semantic Find Usages (Alt+F7)

Put the cursor on any symbol: a function, variable, class, property or type. In the demo, use
calculateSubtotalinsrc/pricing.ts.Press
Alt+F7, or run Semantic Find Usages.References are grouped by what they do:
- Definitions, Calls, JSX usages, Writes, Reads;
- Extended or implemented, Type references;
- Imports, Exports and re-exports.
Each group shows its count, including how many are in test files. To see only some groups, such as just Calls and Writes, choose Show Groups… in the view's
…menu. That works in every report.Inside a group, references are listed per file. Each one shows its line of code and the function it's in. Test files have a beaker icon. Hover a row to see the lines around it. The usage you started from is selected and marked at cursor, and Next and Previous Result continue from it.
Usages through aliases count too. In the demo,
main.tscallscalculateSubtotalassubtotal, imported from theindex.tsbarrel.If nothing actually uses the symbol, because it is only declared, imported or re-exported, the view says so. Its definition then gets a trash button (Safe Delete…).
To rename the symbol after reviewing its usages, click the pencil button (Rename Symbol) on its definition.
Re-export Chain Navigator

- Put the cursor on an imported name, a re-export, or a usage of an imported symbol. In the demo, use
subtotalin the first import ofsrc/main.ts. - Run Re-export Chain Navigator.
- Resolution from the cursor shows each hop the name takes: the import, every
export { … } fromorexport *barrel on the way, and the declaration at the end. Renames are noted, such as as subtotal. The declaration's Find Usages button lists every usage under any of its names. - Exported as lists every module that exports the symbol, under which name, and how many files import it from there. Use it to decide which path to import from, or to find barrels with no importers left over after a move.
- Imported through groups the project's imports of the symbol by the module path they use, here
'./pricing'and'./index'. Mixed paths to the same declaration are a common source of circular-import trouble, so the view warns when there is more than one. - An importer that goes through a barrel has an Apply Fix button. It imports the symbol straight from the module that declares it, keeping any local name:
import { calculateSubtotal as subtotal } from './pricing';.
Structural Search Builder

- Open a file in the project and run Structural Search…. Selected code becomes the starting pattern. The builder opens beside the editor.
- Write the Pattern as code with placeholders, or pick one from Start from an example…. In the demo,
$list$.find($callback$)finds every.find()call. Problems, such as a pattern that isn't valid TypeScript or a broken regular expression, show under the box as you type. - Each placeholder gets a row under Placeholder constraints. Type is a regular expression tested against the matched code's type. Here
^LineItem\[\]$keeps only searches through line item arrays and skipscoupons.find(). Text tests the code itself; Invert negates either one. - Choose the Scope and click Search, or press
Ctrl+Enterin the pattern. The scope is the whole project, the current file, or the code selected in the editor right now. A selection is handy for replacing inside one function. - Matches appear in the builder and in the TypeScript Structure view, grouped by file. Each shows what every placeholder matched, such as $list$ = items. Click one to open it.
- Recent searches… brings back any of your last 15 searches with their constraints.
Type-Safe Structural Replace

- In Structural Search…, write the pattern to find. In the demo, use
Math.round($value$ * 100) / 100. - Under Replace with, write the new code using the same placeholders:
Number($value$.toFixed(2)).$name*$placeholders insert the whole matched list. A placeholder the pattern doesn't define, usually a typo, is flagged under the box. - Click Preview Replacements. Every match is listed with its old code (−) and new code (+). The summary says whether the result type-checks cleanly or lists each new type error, such as calling a method the value doesn't have. To change only some matches, click Replace this one under a match.
- Click Replace All…, or press
Ctrl+Enterin the replacement. The changes open in VS Code's Refactor Preview, where you can untick individual files or changes before applying them. Turn offtsStructure.previewRefactoringsto apply directly. - A replacement that doesn't parse is refused before anything is written, with the file and line where it breaks. Use Undo (
Ctrl+Z) in the edited files to revert an applied replacement.
Custom Inspection Builder

- In Structural Search…, write and test a pattern until it finds exactly what you want to flag. For example,
console.log($args*$). - Optionally write a Replace with template. It becomes the inspection's quick fix.
- Under Save as inspection, write the message, for example Use track() instead of console.log. Placeholders such as
$args$are filled in from each match. Choose a severity and click Save as Inspection. To check only some files, fill in the glob box, such assrc/**or**/*.tsx. - The inspection is saved in the workspace's
tsStructure.customInspectionssetting, so it is shared through.vscode/settings.json. It runs live on visible files like the built-in inspections: an underline, a hover, the Problems panel, the quick fix and a fix-all for the file. - Inspect Code… includes custom inspections, so the whole project can be checked at once.
- Run Manage Custom Inspections… to see them all. Press
Enterto edit one in the builder, where Update Inspection saves over it. Use the buttons on each row to disable, enable or delete it, or pick New inspection… to start another. From a finding, the lightbulb offers Edit custom inspection… and Disable custom inspection directly.
Inheritance Hierarchy Explorer

- Put the cursor on a class or interface name, or anywhere inside its body. In the demo, use
PaymentMethodinsrc/models.ts. - Run Inheritance Hierarchy Explorer.
- Supertypes lists what the type extends and implements, nested up to the top.
- Subtypes and implementations lists every class or interface that extends or implements it, nested down through the project. Each one says which of this type's members it overrides, such as overrides name, charge, describe. Click the hierarchy button on any supertype or subtype (Show Hierarchy of This Type) to re-center the report on it.
- Members lists the type's own members. Each one says whether it is abstract, which supertype it comes from, and how many subtypes override it. Expand a member to see each override and whether it calls super. Run it with the cursor on a member's name, and that member opens selected. The button on a member (Override Impact of This Member) shows what changing it would affect.
- Inherited members lists what the type gets from its supertypes without redeclaring it, each with the type it comes from. That's the full API of a subclass in one place.
Override Impact Analyzer

- Put the cursor on a method or property of a class or interface. In the demo, use
chargeinPaymentMethod(src/models.ts). - Run Override Impact Analyzer.
- The first row names the member and anything it overrides higher up.
- Overrides in subtypes says how each override relates to it: calls super: affected, replaces it: not affected by body changes, or implements it for abstract and interface members.
- Subtypes that inherit it unchanged are affected by any change to its body. The buttons on rows re-run the analysis on an override, or show the hierarchy of an inheriting type.
- Call sites are split by the static type they call through. Calls through the base type may run any implementation, so they are the riskiest. Impacted tests lists the tests that reach the member. Its play button (Run These Tests) runs them.
Dependency Path Finder

- Open the module whose dependency you want to explain, such as
src/main.ts, and run Dependency Path Finder…. In the Explorer, you can also right-click a file. With two files selected, it explains the path between them without asking. - Choose the module it depends on, here
analytics.ts. The modules it depends on are listed first, nearest first, with how many imports away each one is. - The first row says whether it depends on it, and how many imports deep. Its Swap Direction button asks the reverse question.
- Shortest paths lists every shortest chain of imports. Expand one to see each hop: the exact
importorexport … fromline, with the names it brings in. The Find Usages button on an import shows what in that module uses the imported name, which is the code to change to cut the dependency. A path through a type-only import is marked as erased at runtime. If all shortest paths are type-only, Longer runtime path shows the real one. - Modules on these paths that import it directly are the last hops, where the dependency could be cut.
- If there is no dependency, the report says so, and shows the reverse direction if there is one.
- If the two modules depend on each other at runtime, the report warns about the cycle and lists the paths back under Circular.
Dependency Graph Diff

Open a file of the project in a Git repository and run Dependency Graph Diff…. It needs
giton yourPATH.Choose what to compare with:
- HEAD, for uncommitted changes;
- the branch point with
main, for everything on your branch; - HEAD~1, a recent commit, a branch, or any revision you type.
The revision you used last time is offered first.
New import cycles come first, in red, such as analytics.ts ⇄ cart.ts. They are the changes most worth catching in review. Each cycle lists the imports that close it, so you can open the one to cut. A cycle that only exists through
import typeis marked type-only: erased at runtime. Cycles removed lists the ones your change broke up.Added imports and Removed imports are grouped by the importing file, with the names each one brings in. The button on a file (Compare with Revision) opens a diff of that file against the chosen revision. Changed imports are existing imports whose names changed, or that switched between type-only and runtime.
Modules lists added and removed files. Biggest changes in importers shows modules whose number of importers changed most, which hints at new hubs or modules being phased out. Expand one to see which modules started or stopped importing it.
The working tree includes unsaved editor changes, so you can check an edit before saving.
Argument Propagation Explorer

- Put the cursor on a parameter, a destructured prop, or an argument in a call (to explore the callee's parameter). In the demo, use
codeinapplyCoupon(src/pricing.ts). - Run Argument Propagation Explorer. The first row shows the parameter's type, and whether it is optional or has a default.
- Where values come from walks back through every caller: what each one passes, through their own parameters, local variables and their assignments, and JSX attributes, until it reaches literals (
'READ10'), omitted arguments or expressions it can't follow. Values that can arrive collects the distinct literals and omitted arguments at the ends of those paths, such as'READ10'or omitted (undefined). - Where the value goes walks forward: the functions and components it is passed to (and as which parameter or prop), locals derived from it, returns and stores. The arrow button (Follow Into Call) re-runs the explorer from the receiving parameter, which also shows its other callers.
- Use it before changing a parameter's type or meaning, to see every value that can arrive and everything that depends on it. The depth is limited by
tsStructure.argumentFlow.maxDepth(default 6). The view says when a path was cut off by it.
Test Impact Analysis
Usage
- Open a TypeScript or JavaScript file and place the cursor on a symbol.
- Run TypeScript Structure: Test Impact Analysis from the Command Palette or the editor's right-click menu.
- Choose a scope:
- Function or Symbol at Cursor: tests that depend on the selected symbol.
- Active Module: tests that depend on anything in the current file.
- A searchable picker lists each affected test, its file and the dependency chain that explains the match. Select one to open its registration. The Impacted Tests view in the Testing sidebar keeps the results as a tree, with each step of the chain clickable.
Tests never run automatically. Use the play buttons in the picker or the view, or Run Impacted Tests, to run them through Jest, Vitest, Mocha, Playwright or node --test with name filtering.
Try it
Press F5 to open the sample project. In src/impactSource.ts, place the cursor on formatImpactLabel and choose Function or Symbol at Cursor. The two label tests in impactSource.test.ts appear, one of them through displayImpactLabel, and the unrelated totals test is excluded. Active Module includes all three. In src/discount.ts, calculateDiscount reaches tests through calculateTotal and checkout.
How it works
- Symbol resolution. The symbol at the cursor is resolved through imports, aliases and re-exports to its declarations. If the cursor is not on a name, the enclosing function, method, class or test is used.
- Reference index. Every runtime reference in the project is indexed by the declaration it resolves to. Type-only positions (annotations, interfaces,
implements) are skipped, whileclass X extends Ycounts. - Caller tracing. A breadth-first walk goes from each reference up to the code that contains it, which is a test body, a hook, test-file initialization, or the next declaration whose callers are followed. Visited declarations end cycles, and each test keeps its shortest explanation.
- Confidence.
- Confirmed means a resolved chain of references reaches the test body.
- Possible means the chain depends on a conservative assumption: calls through an interface or base class, shared hooks, test-file initialization, or module top-level code.
- Stale results. Results record a hash of every file they point into. Navigation is refused, with an offer to re-run the analysis, if a file changed after the analysis ran.
- Caching. Parsed files are reused across analyses while their text is unchanged. A project whose files are all unchanged is reused whole, including its reference index. Unsaved editor text is analyzed.
Test discovery
- Test files are recognized by
.test.*or.spec.*names, bytest/,tests/or__tests__/directories, and by imports ofnode:test,vitest,@jest/globals,@playwright/testormocha. - Recognized registrations:
test,it,describe,suite,contextand Mocha TDD names, including imported aliases (import { test as check }) and namespace imports.- Modifiers such as
.only,.skip,.todoand.concurrent, and Playwright'stest.describeandbase.extend. .each(...)and.for(...). Parameterized registrations are listed once.
- A reference in a shared hook or in test-file initialization conservatively includes every case in that file.
- A test file with no static registrations is listed as a whole-file entry.
Limitations
- Source and tests must be in the same compiler project. Excluded test projects are not scanned. If the application's
tsconfigexcludes tests, open a test file under a config that includes them and select the imported symbol there. - Static analysis cannot see dynamic dispatch through untyped values, runtime dependency injection, string-based lookups or module mocks. Mocked modules are still reported as impacts.
- Analysis runs on the extension host thread. Very large projects may pause the editor for the first analysis while the index is built. Live inspections only run on files that are on screen.
- The inspections and navigation features follow calls the compiler can resolve. Callbacks passed to library functions, dynamic property access and values that escape into untyped code are reported as unknown rather than guessed.
- Dependency Graph Diff needs
giton thePATH. It resolves imports in both revisions with the current compiler options. - The extension depends on TypeScript 6.0.3, the latest release with the stable JavaScript Compiler API. TypeScript 7 only exposes an unstable API.
Development
npm install
npm test # compile and run the analyzer tests
VSCODE_EXECUTABLE="/path/to/Code.exe" npm run test:integration # run inside a real VS Code
VSCODE_EXECUTABLE="/path/to/Code.exe" npm run screenshots # recapture docs/images from docs/demo
- The engine has no
vscodedependency and is unit-tested directly:src/analysis/is test impact analysis.src/refactor/andsrc/intentions/are the refactorings.src/inspect/holds the inspections,src/navigate/the navigation features, andsrc/search/structural search.src/structure/holds the shared report model and helpers.
src/extension.ts,src/resultsView.ts,src/runner.tsandsrc/ui/are the VS Code layer.test-fixtures/holds a small project per area. For example,edgecovers re-exports, interface dispatch, cycles, hooks,.each, aliased imports, Jest globals andnode:test.