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/Jest module
resolution doesn't require explicit extensions. This causes
"Cannot find module" errors.
This change adds `strip_js_extensions()` function that removes
.js/.ts/.tsx/.jsx/.mjs/.mts extensions from relative import paths
in generated tests. The function handles:
- ES module imports: import { x } from '../path.js'
- CommonJS requires: require('../path.js')
- Jest mocks: jest.mock('../path.js'), jest.doMock(), etc.
External package imports (lodash, react, etc.) are preserved.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
6a13aa688a
commit
70360436bd
2 changed files with 122 additions and 0 deletions
|
|
@ -122,6 +122,43 @@ def _is_valid_js_identifier(name: str) -> bool:
|
|||
return bool(JS_IDENTIFIER_PATTERN.match(name)) and name not in reserved_words
|
||||
|
||||
|
||||
# Patterns to strip file extensions from import paths
|
||||
# LLMs sometimes add .js extensions to TypeScript imports, which breaks module resolution
|
||||
_JS_EXTENSION_PATTERN = re.compile(
|
||||
r"""(from\s+['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"])"""
|
||||
)
|
||||
_REQUIRE_EXTENSION_PATTERN = re.compile(
|
||||
r"""(require\s*\(\s*['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"]\s*\))"""
|
||||
)
|
||||
_JEST_MOCK_EXTENSION_PATTERN = re.compile(
|
||||
r"""(jest\.(?:mock|doMock|unmock|requireActual|requireMock)\s*\(\s*['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"])"""
|
||||
)
|
||||
|
||||
|
||||
def strip_js_extensions(source: str) -> str:
|
||||
"""Strip .js/.ts/.tsx/.jsx extensions from relative import paths.
|
||||
|
||||
TypeScript and Jest's module resolution automatically resolve file extensions,
|
||||
so adding them explicitly can cause "Cannot find module" errors when the LLM
|
||||
adds incorrect extensions (e.g., .js to a .ts file).
|
||||
|
||||
This function removes extensions from:
|
||||
- ES module imports: import { x } from '../path/file.js'
|
||||
- CommonJS requires: require('../path/file.js')
|
||||
- Jest mocks: jest.mock('../path/file.js')
|
||||
|
||||
Args:
|
||||
source: The test source code.
|
||||
|
||||
Returns:
|
||||
Source code with extensions stripped from relative import paths.
|
||||
|
||||
"""
|
||||
source = _JS_EXTENSION_PATTERN.sub(r"\1\2\4", source)
|
||||
source = _REQUIRE_EXTENSION_PATTERN.sub(r"\1\2\4", source)
|
||||
return _JEST_MOCK_EXTENSION_PATTERN.sub(r"\1\2\4", source)
|
||||
|
||||
|
||||
def _generate_import_statement(function_name: str, module_path: str) -> tuple[str, str]:
|
||||
"""Generate appropriate import statement and accessor for JavaScript/TypeScript.
|
||||
|
||||
|
|
@ -480,6 +517,9 @@ async def testgen_javascript(
|
|||
language=data.language,
|
||||
)
|
||||
|
||||
# Strip incorrect file extensions from import paths (LLMs sometimes add .js to .ts imports)
|
||||
generated_test_source = strip_js_extensions(generated_test_source)
|
||||
|
||||
ph(request.user, "aiservice-testgen-tests-generated", properties={"language": language})
|
||||
|
||||
if hasattr(request, "should_log_features") and request.should_log_features:
|
||||
|
|
|
|||
|
|
@ -285,3 +285,85 @@ class TestJavaScriptTestGenPromptContent:
|
|||
system_content = messages[0]["content"]
|
||||
# Should warn against mocking
|
||||
assert "mock" in system_content.lower() or "Mock" in system_content
|
||||
|
||||
|
||||
class TestStripJsExtensions:
|
||||
"""Tests for stripping file extensions from import paths.
|
||||
|
||||
These tests copy the regex patterns and function directly to avoid Django dependencies.
|
||||
"""
|
||||
|
||||
# Copy of patterns from aiservice/languages/js_ts/testgen.py
|
||||
_JS_EXTENSION_PATTERN = re.compile(
|
||||
r"""(from\s+['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"])"""
|
||||
)
|
||||
_REQUIRE_EXTENSION_PATTERN = re.compile(
|
||||
r"""(require\s*\(\s*['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"]\s*\))"""
|
||||
)
|
||||
_JEST_MOCK_EXTENSION_PATTERN = re.compile(
|
||||
r"""(jest\.(?:mock|doMock|unmock|requireActual|requireMock)\s*\(\s*['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"])"""
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def strip_js_extensions(source: str) -> str:
|
||||
"""Strip .js/.ts/.tsx/.jsx extensions from relative import paths."""
|
||||
source = TestStripJsExtensions._JS_EXTENSION_PATTERN.sub(r"\1\2\4", source)
|
||||
source = TestStripJsExtensions._REQUIRE_EXTENSION_PATTERN.sub(r"\1\2\4", source)
|
||||
return TestStripJsExtensions._JEST_MOCK_EXTENSION_PATTERN.sub(r"\1\2\4", source)
|
||||
|
||||
def test_strip_js_extension_from_esm_import(self) -> None:
|
||||
"""Test stripping .js from ES module imports."""
|
||||
code = "import { getDifferences } from '../src/utils/DynamicBindingUtils.js';"
|
||||
expected = "import { getDifferences } from '../src/utils/DynamicBindingUtils';"
|
||||
|
||||
result = self.strip_js_extensions(code)
|
||||
assert result == expected
|
||||
|
||||
def test_strip_ts_extension_from_esm_import(self) -> None:
|
||||
"""Test stripping .ts from ES module imports."""
|
||||
code = "import { func } from './module.ts';"
|
||||
expected = "import { func } from './module';"
|
||||
|
||||
result = self.strip_js_extensions(code)
|
||||
assert result == expected
|
||||
|
||||
def test_strip_extension_from_require(self) -> None:
|
||||
"""Test stripping extensions from require() calls."""
|
||||
code = "const { func } = require('../utils/helper.js');"
|
||||
expected = "const { func } = require('../utils/helper');"
|
||||
|
||||
result = self.strip_js_extensions(code)
|
||||
assert result == expected
|
||||
|
||||
def test_strip_extension_from_jest_mock(self) -> None:
|
||||
"""Test stripping extensions from jest.mock() calls."""
|
||||
code = "jest.mock('../src/utils/DynamicBindingUtils.js');"
|
||||
expected = "jest.mock('../src/utils/DynamicBindingUtils');"
|
||||
|
||||
result = self.strip_js_extensions(code)
|
||||
assert result == expected
|
||||
|
||||
def test_preserve_external_package_imports(self) -> None:
|
||||
"""Test that external package imports are not modified."""
|
||||
code = "import lodash from 'lodash';"
|
||||
|
||||
result = self.strip_js_extensions(code)
|
||||
assert result == code # Should be unchanged
|
||||
|
||||
def test_strip_multiple_extensions_in_file(self) -> None:
|
||||
"""Test stripping multiple extensions in a single file."""
|
||||
code = """
|
||||
import { func1 } from '../utils/helper.js';
|
||||
import { func2 } from './local.ts';
|
||||
const { func3 } = require('../lib/util.tsx');
|
||||
jest.mock('../mocks/mock.jsx');
|
||||
"""
|
||||
expected = """
|
||||
import { func1 } from '../utils/helper';
|
||||
import { func2 } from './local';
|
||||
const { func3 } = require('../lib/util');
|
||||
jest.mock('../mocks/mock');
|
||||
"""
|
||||
|
||||
result = self.strip_js_extensions(code)
|
||||
assert result == expected
|
||||
|
|
|
|||
Loading…
Reference in a new issue