Skip to content

Commit d71fca8

Browse files
committed
UTs & export refactor
1 parent fe36690 commit d71fca8

File tree

3 files changed

+205
-110
lines changed

3 files changed

+205
-110
lines changed

src/codegen/sdk/python/file.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -200,13 +200,6 @@ def remove_unused_imports(self) -> None:
200200

201201
self.G.commit_transactions()
202202

203-
def remove_unused_exports(self) -> None:
204-
"""Removes unused exports from the file.
205-
In Python this is equivalent to removing unused imports since Python doesn't have
206-
explicit export statements. Calls remove_unused_imports() internally.
207-
"""
208-
self.remove_unused_imports()
209-
210203
@cached_property
211204
@noapidoc
212205
@reader(cache=True)

src/codegen/sdk/typescript/file.py

Lines changed: 89 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -238,63 +238,6 @@ def _parse_imports(self) -> None:
238238
if function.type == "import" or (function.type == "identifier" and function.text.decode("utf-8") == "require"):
239239
TSImportStatement(import_node, self.node_id, self.G, self.code_block, 0)
240240

241-
@writer
242-
def remove_unused_exports(self) -> None:
243-
"""Removes unused exports from the file.
244-
245-
Analyzes all exports in the file and removes any that are not used. An export is considered unused if it has no direct
246-
symbol usages and no re-exports that are used elsewhere in the codebase.
247-
248-
When removing unused exports, the method also cleans up any related unused imports. For default exports, it removes
249-
the 'export default' keyword, and for named exports, it removes the 'export' keyword or the entire export statement.
250-
251-
Args:
252-
None
253-
254-
Returns:
255-
None
256-
"""
257-
for export in self.exports:
258-
symbol_export_unused = True
259-
symbols_to_remove = []
260-
261-
exported_symbol = export.resolved_symbol
262-
for export_usage in export.symbol_usages:
263-
if export_usage.node_type == NodeType.IMPORT or (export_usage.node_type == NodeType.EXPORT and export_usage.resolved_symbol != exported_symbol):
264-
# If the import has no usages then we can add the import to the list of symbols to remove
265-
reexport_usages = export_usage.symbol_usages
266-
if len(reexport_usages) == 0:
267-
symbols_to_remove.append(export_usage)
268-
break
269-
270-
# If any of the import's usages are valid symbol usages, export is used.
271-
if any(usage.node_type == NodeType.SYMBOL for usage in reexport_usages):
272-
symbol_export_unused = False
273-
break
274-
275-
symbols_to_remove.append(export_usage)
276-
277-
elif export_usage.node_type == NodeType.SYMBOL:
278-
symbol_export_unused = False
279-
break
280-
281-
# export is not used, remove it
282-
if symbol_export_unused:
283-
# remove the unused imports
284-
for imp in symbols_to_remove:
285-
imp.remove()
286-
287-
if exported_symbol == exported_symbol.export.declared_symbol:
288-
# change this to be more robust
289-
if exported_symbol.source.startswith("export default "):
290-
exported_symbol.replace("export default ", "")
291-
else:
292-
exported_symbol.replace("export ", "")
293-
else:
294-
exported_symbol.export.remove()
295-
if exported_symbol.export != export:
296-
export.remove()
297-
298241
@noapidoc
299242
def _get_export_data(self, relative_path: str, export_type: str = "EXPORT") -> tuple[tuple[str, str], dict[str, callable]]:
300243
quoted_paths = (f"'{relative_path}'", f'"{relative_path}"')
@@ -463,51 +406,99 @@ def get_namespace(self, name: str) -> TSNamespace | None:
463406
return next((x for x in self.symbols if isinstance(x, TSNamespace) and x.name == name), None)
464407

465408
@writer
466-
def remove_unused_imports(self, moved_symbol_names: set[str] | None = None) -> None:
409+
def remove_unused_imports(self) -> None:
467410
"""Removes unused imports from the file.
468411
469-
Args:
470-
moved_symbol_names: Optional set of symbol names that were moved to another file
412+
Handles different TypeScript import styles:
413+
- Single imports (import x from 'y')
414+
- Named imports (import { x } from 'y')
415+
- Multi-imports (import { a, b as c } from 'y')
416+
- Type imports (import type { X } from 'y')
417+
- Side effect imports (import 'y')
418+
- Wildcard imports (import * as x from 'y')
419+
420+
Preserves:
421+
- Comments and whitespace where possible
422+
- Side effect imports (e.g., CSS imports)
423+
- Type imports used in type annotations
424+
"""
425+
# Process each import statement
426+
for import_stmt in self.imports:
427+
# Always preserve side effect imports since we can't track their usage
428+
if import_stmt.import_type == ImportType.SIDE_EFFECT:
429+
continue
430+
431+
# Check if all imports in this statement are unused
432+
import_stmt.remove_if_unused()
433+
434+
self.G.commit_transactions()
435+
436+
@writer
437+
def remove_unused_exports(self) -> None:
438+
"""Removes unused exports from the file.
439+
440+
Handles different TypeScript export styles:
441+
- Default exports (export default x)
442+
- Named exports (export function x, export const x)
443+
- Re-exports (export { x } from 'y')
444+
- Type exports (export type X, export interface X)
445+
446+
Preserves:
447+
- Type exports (these may be used in type positions)
448+
- Default exports (these are often used dynamically)
449+
- Exports used by other files through imports
450+
- Exports used within the same file
471451
"""
472-
for import_statement in self.import_statements:
473-
# Track which symbols in this import statement are still used
474-
used_symbols = []
475-
removed_symbols = []
476-
477-
for import_symbol in import_statement.imports:
478-
# Skip side effect imports
479-
if import_symbol.import_type == ImportType.SIDE_EFFECT:
480-
continue
481-
482-
symbol_name = import_symbol.alias.source if import_symbol.alias else import_symbol.name
483-
484-
# Check if this import is still used in the file
485-
is_used = False
486-
for usage in import_symbol.usages:
487-
# Skip usages from moved symbols if provided
488-
if moved_symbol_names and usage.usage_symbol and usage.usage_symbol.name in moved_symbol_names:
489-
continue
490-
is_used = True
452+
for export in self.exports:
453+
# Skip type exports and default exports
454+
if export.is_type_export() or export.is_default_export():
455+
continue
456+
457+
symbol_export_unused = True
458+
symbols_to_remove = []
459+
460+
exported_symbol = export.resolved_symbol
461+
for export_usage in export.symbol_usages:
462+
if export_usage.node_type == NodeType.IMPORT or (export_usage.node_type == NodeType.EXPORT and export_usage.resolved_symbol != exported_symbol):
463+
# If the import has no usages then we can add the import to the list of symbols to remove
464+
reexport_usages = export_usage.symbol_usages
465+
if len(reexport_usages) == 0:
466+
symbols_to_remove.append(export_usage)
467+
break
468+
469+
# If any of the import's usages are valid symbol usages, export is used.
470+
if any(usage.node_type == NodeType.SYMBOL for usage in reexport_usages):
471+
symbol_export_unused = False
472+
break
473+
474+
symbols_to_remove.append(export_usage)
475+
476+
elif export_usage.node_type == NodeType.SYMBOL:
477+
symbol_export_unused = False
491478
break
492479

493-
if is_used:
494-
used_symbols.append(import_symbol)
495-
else:
496-
removed_symbols.append(import_symbol)
497-
498-
if not used_symbols and removed_symbols:
499-
# If no symbols are used, remove the entire import statement
500-
import_statement.remove()
501-
elif removed_symbols and used_symbols:
502-
# If some symbols are used but others aren't, update the import statement
503-
new_imports = []
504-
for symbol in used_symbols:
505-
if symbol.alias:
506-
new_imports.append(f"{symbol.name} as {symbol.alias.source}")
480+
# export is not used, remove it
481+
if symbol_export_unused:
482+
# remove the unused imports
483+
for imp in symbols_to_remove:
484+
imp.remove()
485+
486+
# Handle different export types
487+
if hasattr(export, 'source') and export.source:
488+
# Re-export case (export { x } from 'y')
489+
export.remove()
490+
elif exported_symbol and hasattr(exported_symbol, 'export') and exported_symbol.export:
491+
if exported_symbol.export.declared_symbol == exported_symbol:
492+
# Direct export case (export function x)
493+
if exported_symbol.source.startswith("export default "):
494+
exported_symbol.replace("export default ", "")
495+
else:
496+
exported_symbol.replace("export ", "")
507497
else:
508-
new_imports.append(symbol.name)
498+
# Export statement case (export { x })
499+
exported_symbol.export.remove()
500+
else:
501+
# Fallback - just remove the export
502+
export.remove()
509503

510-
module = import_statement.module.source
511-
type_prefix = "type " if import_statement.is_type_import else ""
512-
new_statement = f"import {type_prefix}{{ {', '.join(new_imports)} }} from {module};"
513-
import_statement.source = new_statement
504+
self.G.commit_transactions()

tests/unit/codegen/sdk/typescript/file/test_file_remove.py

Lines changed: 116 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,8 @@ def test_remove_unused_imports_with_moved_symbols(tmpdir):
8080
return helper();
8181
}
8282
"""
83-
expected1 = """
84-
export function foo() {
85-
return helper();
86-
}
87-
"""
83+
# The original file should be empty after move since foo was the only content
84+
expected1 = ""
8885

8986
content2 = """
9087
export function helper() {
@@ -100,4 +97,118 @@ def test_remove_unused_imports_with_moved_symbols(tmpdir):
10097
new_file = codebase.create_file("new.ts")
10198
foo.move_to_file(new_file)
10299

100+
# Now explicitly remove unused imports after the move
101+
main_file.remove_unused_imports()
102+
103+
assert main_file.content.strip() == ""
104+
105+
106+
def test_remove_unused_exports_with_side_effects(tmpdir):
107+
content = """
108+
import './styles.css';
109+
export const unused = 5;
110+
export function usedFunction() { return true; }
111+
112+
const x = usedFunction();
113+
"""
114+
expected = """
115+
import './styles.css';
116+
export function usedFunction() { return true; }
117+
118+
const x = usedFunction();
119+
"""
120+
121+
with get_codebase_session(tmpdir=tmpdir, programming_language=ProgrammingLanguage.TYPESCRIPT, files={"test.ts": content}) as codebase:
122+
file = codebase.get_file("test.ts")
123+
file.remove_unused_exports()
124+
assert file.content.strip() == expected.strip()
125+
126+
127+
def test_remove_unused_exports_with_multiple_types(tmpdir):
128+
content = """
129+
export const UNUSED_CONSTANT = 42;
130+
export type UnusedType = string;
131+
export interface UnusedInterface {}
132+
export default function main() { return true; }
133+
export function usedFunction() { return true; }
134+
const x = usedFunction();
135+
"""
136+
# Only value exports that are unused should be removed
137+
expected = """
138+
export type UnusedType = string;
139+
export interface UnusedInterface {}
140+
export default function main() { return true; }
141+
export function usedFunction() { return true; }
142+
const x = usedFunction();
143+
"""
144+
145+
with get_codebase_session(tmpdir=tmpdir, programming_language=ProgrammingLanguage.TYPESCRIPT, files={"test.ts": content}) as codebase:
146+
file = codebase.get_file("test.ts")
147+
file.remove_unused_exports()
148+
assert file.content.strip() == expected.strip()
149+
150+
151+
def test_remove_unused_exports_with_reexports(tmpdir):
152+
content1 = """
153+
export { helper } from './utils';
154+
export { unused } from './other';
155+
export function localFunction() { return true; }
156+
"""
157+
content2 = """
158+
import { helper } from './main';
159+
const x = helper();
160+
"""
161+
expected1 = """
162+
export { helper } from './utils';
163+
export function localFunction() { return true; }
164+
"""
165+
166+
with get_codebase_session(tmpdir=tmpdir, programming_language=ProgrammingLanguage.TYPESCRIPT, files={
167+
"main.ts": content1,
168+
"other.ts": content2
169+
}) as codebase:
170+
main_file = codebase.get_file("main.ts")
171+
main_file.remove_unused_exports()
103172
assert main_file.content.strip() == expected1.strip()
173+
174+
175+
def test_remove_unused_exports_with_moved_and_reexported_symbol(tmpdir):
176+
content1 = """
177+
export function helper() {
178+
return true;
179+
}
180+
"""
181+
content2 = """
182+
import { helper } from './utils';
183+
export { helper }; # This re-export should be preserved as it's used
184+
185+
const x = helper();
186+
"""
187+
content3 = """
188+
import { helper } from './main';
189+
190+
function useHelper() {
191+
return helper();
192+
}
193+
"""
194+
195+
with get_codebase_session(tmpdir=tmpdir, programming_language=ProgrammingLanguage.TYPESCRIPT, files={
196+
"utils.ts": content1,
197+
"main.ts": content2,
198+
"consumer.ts": content3
199+
}) as codebase:
200+
utils_file = codebase.get_file("utils.ts")
201+
main_file = codebase.get_file("main.ts")
202+
203+
# Move helper to main.ts
204+
helper = utils_file.get_function("helper")
205+
helper.move_to_file(main_file)
206+
207+
# Remove unused exports
208+
utils_file.remove_unused_exports()
209+
main_file.remove_unused_exports()
210+
211+
# The re-export in main.ts should be preserved since it's used by consumer.ts
212+
assert "export { helper }" in main_file.content
213+
# The original export in utils.ts should be gone
214+
assert "export function helper" not in utils_file.content

0 commit comments

Comments
 (0)