Skip to content

Commit 92a18ed

Browse files
misrasaurabh1claude
andcommitted
fix: strip file extensions from JS/TS import paths in generated tests
LLMs often add .js extensions to TypeScript import paths (e.g., `import { func } from '../module.js'`), but TypeScript's module resolution doesn't require explicit extensions. This causes "Cannot find module" errors when Jest tries to resolve these imports. This change adds `strip_js_extensions()` to remove .js/.ts/.tsx/.jsx/.mjs/.mts extensions from relative import paths in generated tests. The function handles: - ES module imports: import { x } from '../path/file.js' - CommonJS requires: require('../path/file.js') - Jest mocks: jest.mock('../path/file.js'), jest.doMock(), etc. External package imports (lodash, react, etc.) and alias imports (@/components) are preserved. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 13e35ca commit 92a18ed

4 files changed

Lines changed: 162 additions & 5 deletions

File tree

codeflash/languages/javascript/instrument.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,43 @@
1818
from codeflash.discovery.functions_to_optimize import FunctionToOptimize
1919

2020

21+
# Patterns to strip file extensions from import paths
22+
# LLMs sometimes add .js extensions to TypeScript imports, which breaks module resolution
23+
_JS_EXTENSION_PATTERN = re.compile(
24+
r"""(from\s+['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"])"""
25+
)
26+
_REQUIRE_EXTENSION_PATTERN = re.compile(
27+
r"""(require\s*\(\s*['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"]\s*\))"""
28+
)
29+
_JEST_MOCK_EXTENSION_PATTERN = re.compile(
30+
r"""(jest\.(?:mock|doMock|unmock|requireActual|requireMock)\s*\(\s*['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"])"""
31+
)
32+
33+
34+
def strip_js_extensions(source: str) -> str:
35+
"""Strip .js/.ts/.tsx/.jsx extensions from relative import paths.
36+
37+
TypeScript and Jest's module resolution automatically resolve file extensions,
38+
so adding them explicitly can cause "Cannot find module" errors when the LLM
39+
adds incorrect extensions (e.g., .js to a .ts file).
40+
41+
This function removes extensions from:
42+
- ES module imports: import { x } from '../path/file.js'
43+
- CommonJS requires: require('../path/file.js')
44+
- Jest mocks: jest.mock('../path/file.js')
45+
46+
Args:
47+
source: The test source code.
48+
49+
Returns:
50+
Source code with extensions stripped from relative import paths.
51+
52+
"""
53+
source = _JS_EXTENSION_PATTERN.sub(r"\1\2\4", source)
54+
source = _REQUIRE_EXTENSION_PATTERN.sub(r"\1\2\4", source)
55+
return _JEST_MOCK_EXTENSION_PATTERN.sub(r"\1\2\4", source)
56+
57+
2158
class TestingMode:
2259
"""Testing mode constants."""
2360

codeflash/languages/javascript/module_system.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,10 +184,18 @@ def _get_relative_import_path(target_path: Path, source_path: Path) -> str:
184184

185185

186186
def add_js_extension(module_path: str) -> str:
187-
"""Add .js extension to relative module paths for ESM compatibility."""
188-
if module_path.startswith(("./", "../")):
189-
if not module_path.endswith(".js") and not module_path.endswith(".mjs"):
190-
return module_path + ".js"
187+
"""Process module path for ESM compatibility.
188+
189+
NOTE: This function intentionally does NOT add extensions because:
190+
1. TypeScript projects resolve modules without explicit extensions
191+
2. Adding .js to .ts imports causes "Cannot find module" errors
192+
3. Modern bundlers (webpack, vite, etc.) handle extension resolution automatically
193+
194+
The function name is preserved for backward compatibility but the behavior
195+
has been changed to NOT add extensions.
196+
"""
197+
# Previously this function added .js extensions, but this caused module resolution
198+
# errors in TypeScript projects. We now preserve paths without adding extensions.
191199
return module_path
192200

193201

codeflash/verification/verifier.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ def generate_tests(
6767
from codeflash.languages.javascript.instrument import (
6868
TestingMode,
6969
instrument_generated_js_test,
70+
strip_js_extensions,
7071
validate_and_fix_import_style,
7172
)
7273
from codeflash.languages.javascript.module_system import ensure_module_system_compatibility
@@ -75,7 +76,10 @@ def generate_tests(
7576
func_name = function_to_optimize.function_name
7677
qualified_name = function_to_optimize.qualified_name
7778

78-
# First validate and fix import styles
79+
# Strip incorrect file extensions from import paths (LLMs sometimes add .js to .ts imports)
80+
generated_test_source = strip_js_extensions(generated_test_source)
81+
82+
# Validate and fix import styles (default vs named exports)
7983
generated_test_source = validate_and_fix_import_style(generated_test_source, source_file, func_name)
8084

8185
# Convert module system if needed (e.g., CommonJS -> ESM for ESM projects)

tests/test_languages/test_javascript_instrumentation.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,3 +652,111 @@ def test_this_method_call_exact_output(self):
652652
expected = " return codeflash.capture('Class.fibonacci', '1', this.fibonacci.bind(this), n - 1);"
653653
assert transformed == expected, f"Expected:\n{expected}\nGot:\n{transformed}"
654654
assert counter == 1
655+
656+
657+
class TestStripJsExtensions:
658+
"""Tests for stripping file extensions from import paths."""
659+
660+
def test_strip_js_extension_from_esm_import(self):
661+
"""Test stripping .js from ES module imports."""
662+
from codeflash.languages.javascript.instrument import strip_js_extensions
663+
664+
code = "import { getDifferences } from '../src/utils/DynamicBindingUtils.js';"
665+
expected = "import { getDifferences } from '../src/utils/DynamicBindingUtils';"
666+
667+
result = strip_js_extensions(code)
668+
assert result == expected
669+
670+
def test_strip_ts_extension_from_esm_import(self):
671+
"""Test stripping .ts from ES module imports."""
672+
from codeflash.languages.javascript.instrument import strip_js_extensions
673+
674+
code = "import { func } from './module.ts';"
675+
expected = "import { func } from './module';"
676+
677+
result = strip_js_extensions(code)
678+
assert result == expected
679+
680+
def test_strip_extension_from_require(self):
681+
"""Test stripping extensions from require() calls."""
682+
from codeflash.languages.javascript.instrument import strip_js_extensions
683+
684+
code = "const { func } = require('../utils/helper.js');"
685+
expected = "const { func } = require('../utils/helper');"
686+
687+
result = strip_js_extensions(code)
688+
assert result == expected
689+
690+
def test_strip_extension_from_jest_mock(self):
691+
"""Test stripping extensions from jest.mock() calls."""
692+
from codeflash.languages.javascript.instrument import strip_js_extensions
693+
694+
code = "jest.mock('../src/utils/DynamicBindingUtils.js');"
695+
expected = "jest.mock('../src/utils/DynamicBindingUtils');"
696+
697+
result = strip_js_extensions(code)
698+
assert result == expected
699+
700+
def test_strip_extension_from_jest_doMock(self):
701+
"""Test stripping extensions from jest.doMock() calls."""
702+
from codeflash.languages.javascript.instrument import strip_js_extensions
703+
704+
code = "jest.doMock('./helper.ts');"
705+
expected = "jest.doMock('./helper');"
706+
707+
result = strip_js_extensions(code)
708+
assert result == expected
709+
710+
def test_preserve_external_package_imports(self):
711+
"""Test that external package imports are not modified."""
712+
from codeflash.languages.javascript.instrument import strip_js_extensions
713+
714+
code = "import lodash from 'lodash';"
715+
716+
result = strip_js_extensions(code)
717+
assert result == code # Should be unchanged
718+
719+
def test_preserve_absolute_imports(self):
720+
"""Test that absolute/alias imports are not modified."""
721+
from codeflash.languages.javascript.instrument import strip_js_extensions
722+
723+
code = "import { Component } from '@/components/Button';"
724+
725+
result = strip_js_extensions(code)
726+
assert result == code # Should be unchanged
727+
728+
def test_strip_multiple_extensions_in_file(self):
729+
"""Test stripping multiple extensions in a single file."""
730+
from codeflash.languages.javascript.instrument import strip_js_extensions
731+
732+
code = """
733+
import { func1 } from '../utils/helper.js';
734+
import { func2 } from './local.ts';
735+
const { func3 } = require('../lib/util.tsx');
736+
jest.mock('../mocks/mock.jsx');
737+
"""
738+
expected = """
739+
import { func1 } from '../utils/helper';
740+
import { func2 } from './local';
741+
const { func3 } = require('../lib/util');
742+
jest.mock('../mocks/mock');
743+
"""
744+
745+
result = strip_js_extensions(code)
746+
assert result == expected
747+
748+
def test_strip_mjs_mts_extensions(self):
749+
"""Test stripping .mjs and .mts extensions."""
750+
from codeflash.languages.javascript.instrument import strip_js_extensions
751+
752+
code = """
753+
import { a } from './module.mjs';
754+
import { b } from '../util.mts';
755+
"""
756+
expected = """
757+
import { a } from './module';
758+
import { b } from '../util';
759+
"""
760+
761+
result = strip_js_extensions(code)
762+
assert result == expected

0 commit comments

Comments
 (0)