Skip to content

Commit c53c20a

Browse files
committed
Merge remote-tracking branch 'origin/omni-java' into omni-java
2 parents 090e775 + e8c5227 commit c53c20a

4 files changed

Lines changed: 329 additions & 14 deletions

File tree

CLAUDE.md

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,19 +57,11 @@ codeflash/
5757
└── result/ # Result types and handling
5858
```
5959

60-
### Key Patterns
61-
62-
**Either/Result pattern for errors:**
63-
```python
64-
from codeflash.either import is_successful, Success, Failure
65-
result = some_operation()
66-
if is_successful(result):
67-
value = result.unwrap()
68-
else:
69-
error = result.failure()
70-
```
60+
### Key Rules to follow
7161

72-
**Use libcst, not ast** - Always use `libcst` for code parsing/modification to preserve formatting.
62+
- Use libcst, not ast - For Python, always use `libcst` for code parsing/modification to preserve formatting.
63+
- Code context extraction and replacement tests must always assert for full string equality, no substring matching.
64+
- Any new feature or bug fix that can be tested automatically must have test cases.
7365

7466
## Code Style
7567

codeflash/cli_cmds/cli.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,11 +212,22 @@ def process_pyproject_config(args: Namespace) -> Namespace:
212212
is_js_ts_project = pyproject_config.get("language") in ("javascript", "typescript")
213213
if args.tests_root is None:
214214
if is_js_ts_project:
215-
# Try common JS test directories, or default to module_root
215+
# Try common JS test directories at project root first
216216
for test_dir in ["test", "tests", "__tests__"]:
217217
if Path(test_dir).is_dir():
218218
args.tests_root = test_dir
219219
break
220+
# If not found at project root, try inside module_root (e.g., src/test, src/__tests__)
221+
if args.tests_root is None and args.module_root:
222+
module_root_path = Path(args.module_root)
223+
for test_dir in ["test", "tests", "__tests__"]:
224+
test_path = module_root_path / test_dir
225+
if test_path.is_dir():
226+
args.tests_root = str(test_path)
227+
break
228+
# Final fallback: default to module_root
229+
# Note: This may cause issues if tests are colocated with source files
230+
# In such cases, the user should explicitly configure testsRoot in package.json
220231
if args.tests_root is None:
221232
args.tests_root = args.module_root
222233
else:

codeflash/languages/javascript/instrument.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ class StandaloneCallMatch:
5151
has_trailing_semicolon: bool
5252

5353

54+
codeflash_import_pattern = re.compile(
55+
r"(import\s+codeflash\s+from\s+['\"]codeflash['\"])|(const\s+codeflash\s*=\s*require\(['\"]codeflash['\"]\))"
56+
)
57+
58+
5459
class StandaloneCallTransformer:
5560
"""Transforms standalone func(...) calls in JavaScript test code.
5661
@@ -730,7 +735,7 @@ def _instrument_js_test_code(
730735
"""
731736
# Add codeflash helper import if not already present
732737
# Support both npm package (codeflash) and legacy local file (codeflash-jest-helper)
733-
has_codeflash_import = "codeflash" in code
738+
has_codeflash_import = codeflash_import_pattern.search(code)
734739
if not has_codeflash_import:
735740
# Detect module system: ESM uses "import ... from", CommonJS uses "require()"
736741
is_esm = bool(re.search(r"^\s*import\s+.+\s+from\s+['\"]", code, re.MULTILINE))
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
"""Tests for TypeScript code extraction and validation.
2+
3+
These tests verify that TypeScript code is correctly extracted by the codeflash
4+
language support and that the extracted code is syntactically valid.
5+
6+
This test suite was created to help diagnose issues where the API server
7+
reported "Invalid TypeScript: Invalid syntax" errors for code that tree-sitter
8+
validates as correct.
9+
"""
10+
11+
import tempfile
12+
from pathlib import Path
13+
14+
import pytest
15+
16+
from codeflash.languages.base import FunctionInfo, Language, ParentInfo
17+
from codeflash.languages.javascript.support import TypeScriptSupport
18+
19+
20+
@pytest.fixture
21+
def ts_support():
22+
"""Create a TypeScriptSupport instance."""
23+
return TypeScriptSupport()
24+
25+
26+
class TestTypeScriptValidation:
27+
"""Tests for TypeScript syntax validation."""
28+
29+
def test_simple_function_is_valid(self, ts_support):
30+
"""Test that a simple TypeScript function validates correctly."""
31+
code = """
32+
function hello(name: string): string {
33+
return "Hello " + name;
34+
}
35+
"""
36+
assert ts_support.validate_syntax(code) is True
37+
38+
def test_async_function_is_valid(self, ts_support):
39+
"""Test that an async function validates correctly."""
40+
code = """
41+
async function fetchData(url: string): Promise<any> {
42+
const response = await fetch(url);
43+
return response.json();
44+
}
45+
"""
46+
assert ts_support.validate_syntax(code) is True
47+
48+
def test_template_literal_is_valid(self, ts_support):
49+
"""Test that template literals with expressions validate correctly."""
50+
code = """
51+
async function execCommand(expression: string, uri: string) {
52+
const command = `--eval=${expression}`;
53+
return await runCommand([
54+
"mongosh",
55+
uri,
56+
command,
57+
]);
58+
}
59+
"""
60+
assert ts_support.validate_syntax(code) is True
61+
62+
def test_function_with_try_catch_is_valid(self, ts_support):
63+
"""Test that try-catch blocks validate correctly."""
64+
code = """
65+
async function figureOutContentsPath(root: string): Promise<string> {
66+
const subfolders = await fs.readdir(root, { withFileTypes: true });
67+
68+
try {
69+
await fs.access(path.join(root, "manifest.json"));
70+
return root;
71+
} catch (error) {
72+
// Ignore
73+
}
74+
75+
for (const subfolder of subfolders) {
76+
if (subfolder.isDirectory()) {
77+
try {
78+
await fs.access(path.join(root, subfolder.name, "manifest.json"));
79+
return path.join(root, subfolder.name);
80+
} catch (error) {
81+
// Ignore
82+
}
83+
}
84+
}
85+
86+
throw new Error("Could not find contents.");
87+
}
88+
"""
89+
assert ts_support.validate_syntax(code) is True
90+
91+
def test_class_method_is_valid(self, ts_support):
92+
"""Test that class methods validate correctly."""
93+
code = """
94+
class MyClass {
95+
private value: number;
96+
97+
constructor(value: number) {
98+
this.value = value;
99+
}
100+
101+
async processData(input: string): Promise<number> {
102+
const result = parseInt(input, 10);
103+
return result * this.value;
104+
}
105+
}
106+
"""
107+
assert ts_support.validate_syntax(code) is True
108+
109+
def test_invalid_syntax_is_detected(self, ts_support):
110+
"""Test that invalid syntax is correctly detected."""
111+
code = "function broken( { return; }"
112+
assert ts_support.validate_syntax(code) is False
113+
114+
115+
class TestTypeScriptCodeExtraction:
116+
"""Tests for TypeScript code context extraction."""
117+
118+
def test_extract_simple_function(self, ts_support):
119+
"""Test extracting code context for a simple function."""
120+
with tempfile.NamedTemporaryFile(suffix=".ts", mode="w", delete=False) as f:
121+
f.write("""
122+
function add(a: number, b: number): number {
123+
return a + b;
124+
}
125+
""")
126+
f.flush()
127+
file_path = Path(f.name)
128+
129+
functions = ts_support.discover_functions(file_path)
130+
assert len(functions) == 1
131+
assert functions[0].name == "add"
132+
133+
# Extract code context
134+
code_context = ts_support.extract_code_context(
135+
functions[0], file_path.parent, file_path.parent
136+
)
137+
138+
# Verify extracted code is valid
139+
assert ts_support.validate_syntax(code_context.target_code) is True
140+
assert "function add" in code_context.target_code
141+
142+
def test_extract_async_function_with_template_literal(self, ts_support):
143+
"""Test extracting async function with template literals."""
144+
with tempfile.NamedTemporaryFile(suffix=".ts", mode="w", delete=False) as f:
145+
f.write("""
146+
import * as utils from "./utils";
147+
148+
const command_args = process.argv.slice(3);
149+
150+
async function execMongoEval(queryExpression, appsmithMongoURI) {
151+
queryExpression = queryExpression.trim();
152+
153+
if (command_args.includes("--pretty")) {
154+
queryExpression += ".pretty()";
155+
}
156+
157+
return await utils.execCommand([
158+
"mongosh",
159+
appsmithMongoURI,
160+
`--eval=${queryExpression}`,
161+
]);
162+
}
163+
""")
164+
f.flush()
165+
file_path = Path(f.name)
166+
167+
functions = ts_support.discover_functions(file_path)
168+
assert len(functions) == 1
169+
assert functions[0].name == "execMongoEval"
170+
171+
# Extract code context
172+
code_context = ts_support.extract_code_context(
173+
functions[0], file_path.parent, file_path.parent
174+
)
175+
176+
# Verify extracted code is valid
177+
assert ts_support.validate_syntax(code_context.target_code) is True
178+
assert "execMongoEval" in code_context.target_code
179+
# Template literal should be preserved
180+
assert "`--eval=${queryExpression}`" in code_context.target_code
181+
182+
def test_extract_function_with_complex_try_catch(self, ts_support):
183+
"""Test extracting function with nested try-catch blocks."""
184+
with tempfile.NamedTemporaryFile(suffix=".ts", mode="w", delete=False) as f:
185+
f.write("""
186+
import fsPromises from "fs/promises";
187+
import path from "path";
188+
189+
async function figureOutContentsPath(root: string): Promise<string> {
190+
const subfolders = await fsPromises.readdir(root, { withFileTypes: true });
191+
192+
try {
193+
await fsPromises.access(path.join(root, "manifest.json"));
194+
return root;
195+
} catch (error) {
196+
// Ignore
197+
}
198+
199+
for (const subfolder of subfolders) {
200+
if (subfolder.isDirectory()) {
201+
try {
202+
await fsPromises.access(
203+
path.join(root, subfolder.name, "manifest.json"),
204+
);
205+
return path.join(root, subfolder.name);
206+
} catch (error) {
207+
// Ignore
208+
}
209+
}
210+
}
211+
212+
throw new Error("Could not find the contents.");
213+
}
214+
""")
215+
f.flush()
216+
file_path = Path(f.name)
217+
218+
functions = ts_support.discover_functions(file_path)
219+
assert len(functions) == 1
220+
assert functions[0].name == "figureOutContentsPath"
221+
222+
# Extract code context
223+
code_context = ts_support.extract_code_context(
224+
functions[0], file_path.parent, file_path.parent
225+
)
226+
227+
# Verify extracted code is valid
228+
assert ts_support.validate_syntax(code_context.target_code) is True
229+
assert "figureOutContentsPath" in code_context.target_code
230+
# Should contain nested try-catch
231+
assert "try {" in code_context.target_code
232+
assert "catch (error)" in code_context.target_code
233+
234+
def test_extracted_code_includes_imports(self, ts_support):
235+
"""Test that imports are included in code context."""
236+
with tempfile.NamedTemporaryFile(suffix=".ts", mode="w", delete=False) as f:
237+
f.write("""
238+
import fs from "fs";
239+
import path from "path";
240+
241+
function readConfig(filename: string): string {
242+
const fullPath = path.join(__dirname, filename);
243+
return fs.readFileSync(fullPath, "utf8");
244+
}
245+
""")
246+
f.flush()
247+
file_path = Path(f.name)
248+
249+
functions = ts_support.discover_functions(file_path)
250+
assert len(functions) == 1
251+
252+
code_context = ts_support.extract_code_context(
253+
functions[0], file_path.parent, file_path.parent
254+
)
255+
256+
# Check that imports are captured
257+
assert len(code_context.imports) > 0
258+
assert any("fs" in imp for imp in code_context.imports)
259+
260+
def test_extracted_code_includes_global_variables(self, ts_support):
261+
"""Test that referenced global variables are included."""
262+
with tempfile.NamedTemporaryFile(suffix=".ts", mode="w", delete=False) as f:
263+
f.write("""
264+
const CONFIG = { timeout: 5000 };
265+
const MAX_RETRIES = 3;
266+
267+
async function fetchWithRetry(url: string): Promise<any> {
268+
for (let i = 0; i < MAX_RETRIES; i++) {
269+
try {
270+
const response = await fetch(url, { signal: AbortSignal.timeout(CONFIG.timeout) });
271+
return response.json();
272+
} catch (e) {
273+
if (i === MAX_RETRIES - 1) throw e;
274+
}
275+
}
276+
}
277+
""")
278+
f.flush()
279+
file_path = Path(f.name)
280+
281+
functions = ts_support.discover_functions(file_path)
282+
assert len(functions) == 1
283+
284+
code_context = ts_support.extract_code_context(
285+
functions[0], file_path.parent, file_path.parent
286+
)
287+
288+
# Verify extracted code is valid
289+
assert ts_support.validate_syntax(code_context.target_code) is True
290+
291+
292+
class TestTypeScriptLanguageProperties:
293+
"""Tests for TypeScript language support properties."""
294+
295+
def test_language_is_typescript(self, ts_support):
296+
"""Test that language property returns TypeScript."""
297+
assert ts_support.language == Language.TYPESCRIPT
298+
299+
def test_file_extensions_include_ts(self, ts_support):
300+
"""Test that TypeScript file extensions are supported."""
301+
extensions = ts_support.file_extensions
302+
assert ".ts" in extensions
303+
assert ".tsx" in extensions
304+
305+
def test_test_file_suffix(self, ts_support):
306+
"""Test that test file suffix is .test.ts."""
307+
assert ts_support.get_test_file_suffix() == ".test.ts"

0 commit comments

Comments
 (0)