|
| 1 | +"""JavaScript/TypeScript code replacement helpers.""" |
| 2 | + |
| 3 | +from __future__ import annotations |
| 4 | + |
| 5 | +from typing import TYPE_CHECKING |
| 6 | + |
| 7 | +from codeflash.cli_cmds.console import logger |
| 8 | + |
| 9 | +if TYPE_CHECKING: |
| 10 | + from pathlib import Path |
| 11 | + |
| 12 | + from codeflash.languages.base import Language |
| 13 | + from codeflash.languages.javascript.treesitter import TreeSitterAnalyzer |
| 14 | + |
| 15 | + |
| 16 | +# Author: ali <mohammed18200118@gmail.com> |
| 17 | +def _add_global_declarations_for_language( |
| 18 | + optimized_code: str, original_source: str, module_abspath: Path, language: Language |
| 19 | +) -> str: |
| 20 | + """Add new global declarations from optimized code to original source. |
| 21 | +
|
| 22 | + Finds module-level declarations (const, let, var, class, type, interface, enum) |
| 23 | + in the optimized code that don't exist in the original source and adds them. |
| 24 | +
|
| 25 | + New declarations are inserted after any existing declarations they depend on. |
| 26 | + For example, if optimized code has `const _has = FOO.bar.bind(FOO)`, and `FOO` |
| 27 | + is already declared in the original source, `_has` will be inserted after `FOO`. |
| 28 | +
|
| 29 | + Args: |
| 30 | + optimized_code: The optimized code that may contain new declarations. |
| 31 | + original_source: The original source code. |
| 32 | + module_abspath: Path to the module file (for parser selection). |
| 33 | + language: The language of the code. |
| 34 | +
|
| 35 | + Returns: |
| 36 | + Original source with new declarations added in dependency order. |
| 37 | +
|
| 38 | + """ |
| 39 | + from codeflash.languages.base import Language |
| 40 | + |
| 41 | + if language not in (Language.JAVASCRIPT, Language.TYPESCRIPT): |
| 42 | + return original_source |
| 43 | + |
| 44 | + try: |
| 45 | + from codeflash.languages.javascript.treesitter import get_analyzer_for_file |
| 46 | + |
| 47 | + analyzer = get_analyzer_for_file(module_abspath) |
| 48 | + |
| 49 | + original_declarations = analyzer.find_module_level_declarations(original_source) |
| 50 | + optimized_declarations = analyzer.find_module_level_declarations(optimized_code) |
| 51 | + |
| 52 | + if not optimized_declarations: |
| 53 | + return original_source |
| 54 | + |
| 55 | + existing_names = _get_existing_names(original_declarations, analyzer, original_source) |
| 56 | + new_declarations = _filter_new_declarations(optimized_declarations, existing_names) |
| 57 | + |
| 58 | + if not new_declarations: |
| 59 | + return original_source |
| 60 | + |
| 61 | + # Build a map of existing declaration names to their end lines (1-indexed) |
| 62 | + existing_decl_end_lines = {decl.name: decl.end_line for decl in original_declarations} |
| 63 | + |
| 64 | + # Insert each new declaration after its dependencies |
| 65 | + result = original_source |
| 66 | + for decl in new_declarations: |
| 67 | + result = _insert_declaration_after_dependencies( |
| 68 | + result, decl, existing_decl_end_lines, analyzer, module_abspath |
| 69 | + ) |
| 70 | + # Update the map with the newly inserted declaration for subsequent insertions |
| 71 | + # Re-parse to get accurate line numbers after insertion |
| 72 | + updated_declarations = analyzer.find_module_level_declarations(result) |
| 73 | + existing_decl_end_lines = {d.name: d.end_line for d in updated_declarations} |
| 74 | + |
| 75 | + return result |
| 76 | + |
| 77 | + except Exception as e: |
| 78 | + logger.debug(f"Error adding global declarations: {e}") |
| 79 | + return original_source |
| 80 | + |
| 81 | + |
| 82 | +# Author: ali <mohammed18200118@gmail.com> |
| 83 | +def _get_existing_names(original_declarations: list, analyzer: TreeSitterAnalyzer, original_source: str) -> set[str]: |
| 84 | + """Get all names that already exist in the original source (declarations + imports).""" |
| 85 | + existing_names = {decl.name for decl in original_declarations} |
| 86 | + |
| 87 | + original_imports = analyzer.find_imports(original_source) |
| 88 | + for imp in original_imports: |
| 89 | + if imp.default_import: |
| 90 | + existing_names.add(imp.default_import) |
| 91 | + for name, alias in imp.named_imports: |
| 92 | + existing_names.add(alias if alias else name) |
| 93 | + if imp.namespace_import: |
| 94 | + existing_names.add(imp.namespace_import) |
| 95 | + |
| 96 | + return existing_names |
| 97 | + |
| 98 | + |
| 99 | +# Author: ali <mohammed18200118@gmail.com> |
| 100 | +def _filter_new_declarations(optimized_declarations: list, existing_names: set[str]) -> list: |
| 101 | + """Filter declarations to only those that don't exist in the original source.""" |
| 102 | + new_declarations = [] |
| 103 | + seen_sources: set[str] = set() |
| 104 | + |
| 105 | + # Sort by line number to maintain order from optimized code |
| 106 | + sorted_declarations = sorted(optimized_declarations, key=lambda d: d.start_line) |
| 107 | + |
| 108 | + for decl in sorted_declarations: |
| 109 | + if decl.name not in existing_names and decl.source_code not in seen_sources: |
| 110 | + new_declarations.append(decl) |
| 111 | + seen_sources.add(decl.source_code) |
| 112 | + |
| 113 | + return new_declarations |
| 114 | + |
| 115 | + |
| 116 | +# Author: ali <mohammed18200118@gmail.com> |
| 117 | +def _insert_declaration_after_dependencies( |
| 118 | + source: str, |
| 119 | + declaration, |
| 120 | + existing_decl_end_lines: dict[str, int], |
| 121 | + analyzer: TreeSitterAnalyzer, |
| 122 | + module_abspath: Path, |
| 123 | +) -> str: |
| 124 | + """Insert a declaration after the last existing declaration it depends on. |
| 125 | +
|
| 126 | + Args: |
| 127 | + source: Current source code. |
| 128 | + declaration: The declaration to insert. |
| 129 | + existing_decl_end_lines: Map of existing declaration names to their end lines. |
| 130 | + analyzer: TreeSitter analyzer. |
| 131 | + module_abspath: Path to the module file. |
| 132 | +
|
| 133 | + Returns: |
| 134 | + Source code with the declaration inserted at the correct position. |
| 135 | +
|
| 136 | + """ |
| 137 | + # Find identifiers referenced in this declaration |
| 138 | + referenced_names = analyzer.find_referenced_identifiers(declaration.source_code) |
| 139 | + |
| 140 | + # Find the latest end line among all referenced declarations |
| 141 | + insertion_line = _find_insertion_line_for_declaration(source, referenced_names, existing_decl_end_lines, analyzer) |
| 142 | + |
| 143 | + lines = source.splitlines(keepends=True) |
| 144 | + |
| 145 | + # Ensure proper spacing |
| 146 | + decl_code = declaration.source_code |
| 147 | + if not decl_code.endswith("\n"): |
| 148 | + decl_code += "\n" |
| 149 | + |
| 150 | + # Add blank line before if inserting after content |
| 151 | + if insertion_line > 0 and lines[insertion_line - 1].strip(): |
| 152 | + decl_code = "\n" + decl_code |
| 153 | + |
| 154 | + before = lines[:insertion_line] |
| 155 | + after = lines[insertion_line:] |
| 156 | + |
| 157 | + return "".join([*before, decl_code, *after]) |
| 158 | + |
| 159 | + |
| 160 | +# Author: ali <mohammed18200118@gmail.com> |
| 161 | +def _find_insertion_line_for_declaration( |
| 162 | + source: str, referenced_names: set[str], existing_decl_end_lines: dict[str, int], analyzer: TreeSitterAnalyzer |
| 163 | +) -> int: |
| 164 | + """Find the line where a declaration should be inserted based on its dependencies. |
| 165 | +
|
| 166 | + Args: |
| 167 | + source: Source code. |
| 168 | + referenced_names: Names referenced by the declaration. |
| 169 | + existing_decl_end_lines: Map of declaration names to their end lines (1-indexed). |
| 170 | + analyzer: TreeSitter analyzer. |
| 171 | +
|
| 172 | + Returns: |
| 173 | + Line index (0-based) where the declaration should be inserted. |
| 174 | +
|
| 175 | + """ |
| 176 | + # Find the maximum end line among referenced declarations |
| 177 | + max_dependency_line = 0 |
| 178 | + for name in referenced_names: |
| 179 | + if name in existing_decl_end_lines: |
| 180 | + max_dependency_line = max(max_dependency_line, existing_decl_end_lines[name]) |
| 181 | + |
| 182 | + if max_dependency_line > 0: |
| 183 | + # Insert after the last dependency (end_line is 1-indexed, we need 0-indexed) |
| 184 | + return max_dependency_line |
| 185 | + |
| 186 | + # No dependencies found - insert after imports |
| 187 | + lines = source.splitlines(keepends=True) |
| 188 | + return _find_line_after_imports(lines, analyzer, source) |
| 189 | + |
| 190 | + |
| 191 | +# Author: ali <mohammed18200118@gmail.com> |
| 192 | +def _find_line_after_imports(lines: list[str], analyzer: TreeSitterAnalyzer, source: str) -> int: |
| 193 | + """Find the line index after all imports. |
| 194 | +
|
| 195 | + Args: |
| 196 | + lines: Source lines. |
| 197 | + analyzer: TreeSitter analyzer. |
| 198 | + source: Full source code. |
| 199 | +
|
| 200 | + Returns: |
| 201 | + Line index (0-based) for insertion after imports. |
| 202 | +
|
| 203 | + """ |
| 204 | + try: |
| 205 | + imports = analyzer.find_imports(source) |
| 206 | + if imports: |
| 207 | + return max(imp.end_line for imp in imports) |
| 208 | + except Exception as exc: |
| 209 | + logger.debug(f"Exception in _find_line_after_imports: {exc}") |
| 210 | + |
| 211 | + # Default: insert at beginning (after shebang/directive comments) |
| 212 | + for i, line in enumerate(lines): |
| 213 | + stripped = line.strip() |
| 214 | + if stripped and not stripped.startswith("//") and not stripped.startswith("#!"): |
| 215 | + return i |
| 216 | + |
| 217 | + return 0 |
0 commit comments