11# Identifiers, Symbols & Matching
22
3- How knip resolves "is this export used?" across an AST. Matching is name-based;
4- the sections below cover the cases that need extra care.
3+ How knip resolves "is this export used?" across an AST. Matching is name-based.
54
65## Shadow detection
76
8- Name-based matching produces false negatives when a local binding shadows an
9- exported name of the same spelling. ` isShadowed(name, pos) ` returns true if the
10- reference at ` pos ` falls inside a scope that shadows ` name ` . Shadows are
11- registered via:
7+ A local binding can shadow an exported name, producing false negatives.
8+ ` isShadowed(name, pos) ` returns true if the reference at ` pos ` falls inside a
9+ scope that shadows ` name ` . Shadows registered via:
1210
13- - ` _addShadow ` : block-scoped variables and nested function declarations
14- (uses current ` scopeDepth ` range from ` BlockStatement ` )
11+ - ` _addShadow ` : block-scoped variables and nested function declarations (uses
12+ current ` scopeDepth ` range from ` BlockStatement ` )
1513- ` _addParamShadows ` : function/arrow/method parameters (uses function body
1614 range directly, since params are visited before the body's ` BlockStatement ` )
1715- ` CatchClause ` handler: catch binding (uses catch body range)
1816- ` ForInStatement ` /` ForOfStatement ` handlers: loop variable (uses loop body
1917 range, since the ` VariableDeclaration ` fires before the body's ` BlockStatement ` )
2018
2119` _collectBindingNames ` recurses into destructuring patterns (ObjectPattern,
22- ArrayPattern, AssignmentPattern, RestElement) to extract all bound Identifiers.
20+ ArrayPattern, AssignmentPattern, RestElement) to extract bound Identifiers.
2321
2422## ` ignoreExportsUsedInFile `
2523
2624Opt-in config (default ` false ` ).
2725
2826** Default semantics match tsc/tsgo.** An export only referenced in its own file
29- is reported as unused, because removing the ` export ` keyword leaves the program
30- and ` .d.ts ` valid. Types are structurally inlined : a consumer importing
27+ is reported as unused; removing the ` export ` keyword leaves the program and
28+ ` .d.ts ` valid. Type aliases inline structurally : a consumer importing
3129` UserInfo = { address: Address } ` does not require ` Address ` to be exported.
32- Same for ` typeof X ` references inside type aliases. So ` Address ` is correctly
33- flagged. Opting ` true ` is a code-organization preference, not a correctness
34- concern.
30+ Same for ` typeof X ` references inside type aliases. Opting ` true ` is a
31+ code-organization preference, not a correctness concern.
32+
33+ ** Does not apply to types in value signatures.** Types referenced in exported
34+ value signatures (function params/returns, variable annotations, ` typeof ` of an
35+ exported value) stay alive unconditionally. ` tsc --declaration ` cannot inline
36+ an interface or class type into a ` .d.ts ` and errors TS4023 if the name is not
37+ exported. That path is intentionally not gated by ` ignoreExportsUsedInFile ` .
3538
3639** With the config on.** ` localRefsVisitorObject ` populates ` localRefs ` during
3740AST traversal. Exports present in ` localRefs ` get ` hasRefsInFile = true ` .
3841` shouldCountRefs ` gates eligible types. Computed member access
3942(` obj[EXPORTED_KEY] ` ) is handled.
4043
4144` analyze.ts ` reads this via ` isReferencedInUsedExport ` for exports not directly
42- imported: returns true only when a containing export has ` hasRefsInFile ` and is
43- a type/interface (recursively checked). Alive-ness does not cascade through
44- external imports; inner refs stay scoped to the in-file relationship.
45+ imported: returns true when a containing export has ` hasRefsInFile ` and is a
46+ type/interface (recursively checked). Alive-ness does not cascade through
47+ external imports; chains stay in-file.
48+
49+ ## ` referencedInExport ` (chain refs)
50+
51+ Maps exported identifier to set of export names whose declarations reference
52+ it. Populated for type aliases, interfaces (body and ` extends ` ), and the
53+ signature parts of exported values: variable type annotations, function/arrow
54+ expression params and return types, function/method declarations.
55+ Function/arrow bodies are skipped via ` signatureOnly ` .
56+
57+ ` isReferencedInUsedExport ` walks the chain, classifying each hop:
4558
46- ## ` referencedInExport ` (type-chain refs)
59+ - ** Type→type hop** (containing export is ` type ` /` interface ` /` enum ` ):
60+ propagates only when ` ignoreExportsUsedInFile ` is on. Otherwise the chain
61+ breaks here, since tsc structurally inlines the inner type and its export is
62+ removable.
63+ - ** Type→value hop** (containing export is a function/class/variable):
64+ propagates unconditionally. Needed for ` tsc --declaration ` ; removing the
65+ inner type's export would TS4023.
4766
48- Maps exported identifier → set of export names whose type annotations reference
49- it. Type-level only, not function signatures. Type→type chains are followed;
50- type→function chains do not keep types alive. Interface ` extends ` clauses
51- captured via ` addRefInExport ` . Feeds the recursive type/interface check in
52- ` isReferencedInUsedExport ` above.
67+ Interface ` extends ` clauses captured via ` addRefInExport ` .
5368
5469## Namespace/enum member ` hasRefsInFile `
5570
@@ -75,14 +90,20 @@ If neither, reported as unused.
7590
7691** Edge case:** when a namespace/enum has export-level ` hasRefsInFile = true `
7792but is NOT externally imported, ` analyze.ts ` skips the member check entirely.
78- Unused members silently pass. By design (the export itself is considered
79- "used").
93+ Unused members silently pass. By design: the export itself is considered "used".
8094
8195## E2E
8296
83- ` packages/knip/test/e2e/fix-tsgo.test.ts ` is the safety net for the resolution
84- paths above. Each fixture builds clean under tsgo, has ` knip --fix ` run on it,
85- then must build clean under tsgo again. A failing post-fix build means knip
86- removed something tsc/tsgo still needs: a false positive in one of these
87- mechanisms. The ` e2e-lib-* ` variants extend the round-trip to a consumer so
88- type-visibility regressions also fail.
97+ ` packages/knip/test/e2e/fix-tsgo.test.ts ` covers the resolution paths above.
98+ Each fixture builds clean under tsgo, has ` knip --fix ` run on it, then must
99+ build clean under tsgo again. A failing post-fix build means knip removed
100+ something tsc/tsgo still needs: a false positive in one of these mechanisms.
101+ The ` e2e-lib-* ` variants extend the round-trip to a consumer with
102+ ` declaration: true ` so type-visibility regressions (TS4023 etc.) also fail.
103+
104+ When adding a fixture for a value-to-type-visibility scenario, do not also
105+ re-export the type explicitly from the public entry. That path masks
106+ type-chain bugs: the type stays alive via the re-export regardless of whether
107+ knip tracks the value's signature. Test the implicit case (type only
108+ referenced in a function signature, never named at the entry) so the chain is
109+ the only thing keeping the export alive.
0 commit comments