Skip to content

Commit 2060b2b

Browse files
Fix linting and formatting inconsistencies (#12)
* Fix linting and formatting inconsistencies due to differences in whether the line break after the script is taken into account * Refactor to cleanup duplicated code and simplify * Remove unnecessary method * Use PEP 604 syntax instead of using Optional * Make tests more specific and add some edge cases
1 parent 85e55f0 commit 2060b2b

5 files changed

Lines changed: 338 additions & 96 deletions

File tree

ruff_cgx/formatter.py

Lines changed: 43 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,59 @@
22
from pathlib import Path
33

44
from .template_formatter import format_template
5-
from .utils import get_script_range, parse_cgx_file, run_ruff_format
5+
from .utils import (
6+
extract_script_content,
7+
parse_cgx_file,
8+
run_ruff_format,
9+
)
610

711
logger = logging.getLogger(__name__)
812

913

10-
def format_script(script_node, source_lines):
14+
def format_script(script_node, source_lines, check=False):
1115
"""
12-
Format script section (CLI version with printing).
16+
Format script section using ruff.
1317
14-
Returns formatted source and the original location of the node.
18+
Args:
19+
script_node: script node from collagraph's parser
20+
source_lines: contents of source file as list of lines
21+
check: If True, only check without modifying (for ruff format --check)
22+
23+
Returns:
24+
Tuple of (formatted_lines, (start_line, end_line))
1525
"""
16-
start, end = get_script_range(script_node)
17-
source = "".join(source_lines[start:end])
26+
if script_node.end is None:
27+
raise RuntimeError("Invalid script node: no end")
28+
29+
# Extract pure Python content
30+
script_content = extract_script_content(script_node)
31+
if not script_content:
32+
# No content to format - return original lines unchanged
33+
start = script_node.location[0] - 1 # Convert to 0-indexed
34+
end = script_node.end[0] - 1 # End tag line
35+
return source_lines[start:end], (start, end)
1836

1937
# Format using ruff
20-
formatted_source = run_ruff_format(source, check=False)
38+
formatted_source = run_ruff_format(script_content.python_code, check=check)
2139

22-
# Convert back to lines for consistency
40+
# Convert to lines
2341
formatted_lines = formatted_source.splitlines(keepends=True)
2442

25-
return formatted_lines, (start, end)
43+
# If Python was on same line as tag, prepend the tag on its own line
44+
if not script_content.starts_on_new_line:
45+
formatted_lines = ["<script>\n", *formatted_lines]
46+
47+
# If closing tag was inline with Python code, we need to:
48+
# 1. Append closing tag to formatted output
49+
# 2. Extend replacement range to include that line
50+
if script_content.closing_tag_inline:
51+
formatted_lines.append("</script>\n")
52+
replacement_end = script_content.end_line + 1
53+
else:
54+
replacement_end = script_content.end_line
55+
56+
# Return formatted content with range that will be replaced
57+
return formatted_lines, (script_content.start_line, replacement_end)
2658

2759

2860
def format_file(path, check=False, write=True):
@@ -123,10 +155,8 @@ def format_cgx_content(content: str, uri: str = "") -> str:
123155
logger.warning(f"Missing script node in {uri}")
124156
return content
125157

126-
# Format script section (using updated signature for LSP)
127-
script_content, script_location = format_script_content(
128-
parsed.script_node, lines
129-
)
158+
# Format script section
159+
script_content, script_location = format_script(parsed.script_node, lines)
130160

131161
# Format all template nodes
132162
formatted_template_nodes = [
@@ -162,30 +192,3 @@ def format_cgx_content(content: str, uri: str = "") -> str:
162192
logger.error(f"Error formatting {uri}: {e}", exc_info=True)
163193
# Return original content on error
164194
return content
165-
166-
167-
def format_script_content(script_node, source_lines):
168-
"""
169-
Format the script section of a CGX file using ruff (for LSP use).
170-
171-
Args:
172-
script_node: script node from collagraph's parser
173-
source_lines: contents of source file as list of lines
174-
175-
Returns:
176-
Formatted source and the original location of the node.
177-
"""
178-
if script_node.end is None:
179-
raise RuntimeError("Invalid script node: no end")
180-
181-
start, end = get_script_range(script_node)
182-
source = "".join(source_lines[start:end])
183-
184-
# Format using ruff
185-
formatted_source = run_ruff_format(source)
186-
187-
# Convert to lines for consistency
188-
formatted_lines = formatted_source.splitlines(keepends=True)
189-
190-
# Return the formatted content and its location
191-
return formatted_lines, (start, end)

ruff_cgx/linter.py

Lines changed: 44 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,49 @@
55

66
from .utils import (
77
create_virtual_render_content,
8-
get_script_range,
8+
extract_script_content,
99
parse_cgx_file,
1010
run_ruff_check,
1111
)
1212

1313

14+
def _prepare_content_for_linting(content: str) -> str | None:
15+
"""
16+
Prepare CGX content for linting by extracting Python and creating virtual content.
17+
18+
Args:
19+
content: The CGX file content as a string
20+
21+
Returns:
22+
The prepared Python code ready for ruff, or None if no script section
23+
"""
24+
# Parse the CGX file
25+
parsed = parse_cgx_file(content)
26+
if not parsed.script_node:
27+
return None
28+
29+
# Extract pure Python content
30+
script_content = extract_script_content(parsed.script_node)
31+
if not script_content:
32+
return None
33+
34+
# Prepend with comment lines to preserve line numbers for diagnostics
35+
prefix_lines = ["#\n"] * script_content.start_line
36+
python_lines = script_content.python_code.splitlines(keepends=True)
37+
modified_lines = prefix_lines + python_lines
38+
39+
# Add newline to lines that don't have it
40+
modified_lines = [
41+
line if line.endswith("\n") else f"{line}\n" for line in modified_lines
42+
]
43+
44+
# Create virtual content with render method
45+
# This allows ruff to see template variable usage
46+
virtual_content = create_virtual_render_content(content, modified_lines)
47+
48+
return virtual_content
49+
50+
1451
def lint_file(path, **_):
1552
"""
1653
Lint a CGX file using ruff (CLI version).
@@ -26,26 +63,11 @@ def lint_file(path, **_):
2663
path = Path(path)
2764
content = path.read_text(encoding="utf-8")
2865

29-
# Parse CGX file
30-
parsed = parse_cgx_file(content)
31-
if not parsed.script_node:
66+
# Prepare content for linting
67+
virtual_content = _prepare_content_for_linting(content)
68+
if virtual_content is None:
3269
return 1
3370

34-
# Get script range
35-
start, end = get_script_range(parsed.script_node)
36-
script_range = range(start, end)
37-
38-
# Read source lines
39-
lines = content.splitlines(keepends=True)
40-
41-
# Comment out all non-script lines
42-
template_commented = [
43-
line if idx in script_range else "#\n" for idx, line in enumerate(lines)
44-
]
45-
46-
# Create virtual content with render method
47-
virtual_content = create_virtual_render_content(content, template_commented)
48-
4971
# Run ruff check with full output (for CLI)
5072
result, temp_path = run_ruff_check(virtual_content, output_format="full")
5173

@@ -80,36 +102,11 @@ def lint_cgx_content(content: str) -> List[Diagnostic]:
80102
Returns:
81103
List of diagnostics
82104
"""
83-
# Parse the CGX file using Collagraph's parser
84-
parsed = parse_cgx_file(content)
85-
86-
# Check if there's a script section
87-
if not parsed.script_node:
88-
# No script section, nothing to lint
105+
# Prepare content for linting
106+
virtual_content = _prepare_content_for_linting(content)
107+
if virtual_content is None:
89108
return []
90109

91-
# Get the line range of the script section
92-
start_line, end_line = get_script_range(parsed.script_node)
93-
94-
# Create a modified version where non-script lines are commented out
95-
source_lines = content.splitlines(keepends=True)
96-
97-
# Add newline to lines that don't have it (to make sure last line has it?)
98-
source_lines = [
99-
line if line.endswith("\n") else f"{line}\n" for line in source_lines
100-
]
101-
102-
script_range = range(start_line, end_line)
103-
104-
# Comment out all non-script lines to preserve line numbers
105-
modified_lines = [
106-
line if idx in script_range else "#\n" for idx, line in enumerate(source_lines)
107-
]
108-
109-
# Try to construct AST and append virtual render method
110-
# This allows ruff to see template variable usage
111-
virtual_content = create_virtual_render_content(content, modified_lines)
112-
113110
# Run ruff on the virtual file
114111
diagnostics = _run_ruff(virtual_content)
115112

ruff_cgx/utils.py

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
from contextlib import contextmanager
99
from dataclasses import dataclass
1010
from pathlib import Path
11-
from typing import List, Optional
11+
from typing import List
1212

1313
from collagraph.sfc.compiler import construct_ast
1414
from collagraph.sfc.parser import CGXParser, Element
1515

1616
# Module-level configuration for ruff command
17-
_ruff_command: Optional[str] = None
17+
_ruff_command: str | None = None
1818

1919

2020
def set_ruff_command(command: str) -> None:
@@ -68,10 +68,21 @@ class ParsedCGX:
6868
"""Result of parsing a CGX file."""
6969

7070
parser: CGXParser
71-
script_node: Optional[Element]
71+
script_node: Element | None
7272
template_nodes: List[Element]
7373

7474

75+
@dataclass
76+
class ScriptContent:
77+
"""Result of extracting Python content from a script node."""
78+
79+
python_code: str # Pure Python content (without leading newline)
80+
start_line: int # 0-indexed line where Python actually starts
81+
end_line: int # 0-indexed line where </script> tag is
82+
starts_on_new_line: bool # Whether Python was on a new line after <script>
83+
closing_tag_inline: bool # Whether </script> is on same line as last Python code
84+
85+
7586
def parse_cgx_file(content: str) -> ParsedCGX:
7687
"""
7788
Parse a CGX file and extract script and template nodes.
@@ -99,19 +110,59 @@ def parse_cgx_file(content: str) -> ParsedCGX:
99110
)
100111

101112

102-
def get_script_range(script_node: Element) -> tuple[int, int]:
113+
def extract_script_content(script_node: Element) -> ScriptContent | None:
103114
"""
104-
Get the line range of a script node.
115+
Extract pure Python content from a script node.
116+
117+
This handles cases where Python code is on the same line as the <script> tag
118+
by extracting the content from the TextElement child and determining the actual
119+
line boundaries.
105120
106121
Args:
107122
script_node: The script node from CGXParser
108123
109124
Returns:
110-
Tuple of (start_line, end_line) where end_line is exclusive
125+
ScriptContent with pure Python code and location info, or None if no content
111126
"""
112-
start = script_node.location[0]
113-
end = script_node.end[0] - 1 # -1 because end points to closing tag
114-
return start, end
127+
if not script_node.children:
128+
return None
129+
130+
script_child = script_node.children[0]
131+
python_content = script_child.content
132+
133+
# Get the line where the <script> tag starts and where it ends
134+
script_tag_line = script_node.location[0] - 1 # Convert to 0-indexed
135+
end_line = script_node.end[0] - 1 # End tag line (0-indexed)
136+
137+
# Determine if Python starts on a new line after <script>
138+
starts_on_new_line = python_content.startswith("\n")
139+
140+
# Determine if closing tag is on the same line as Python code
141+
# If column > 0, there's content before the closing tag
142+
closing_tag_inline = script_node.end[1] > 0
143+
144+
# Strip leading newline if present
145+
if starts_on_new_line:
146+
python_content = python_content[1:]
147+
start_line = script_tag_line + 1
148+
else:
149+
start_line = script_tag_line
150+
151+
# Strip leading/trailing whitespace from the Python content
152+
# (parser may include spaces when tags are inline)
153+
python_content = python_content.strip()
154+
155+
# Ensure content ends with a newline for proper formatting
156+
if python_content and not python_content.endswith("\n"):
157+
python_content += "\n"
158+
159+
return ScriptContent(
160+
python_code=python_content,
161+
start_line=start_line,
162+
end_line=end_line,
163+
starts_on_new_line=starts_on_new_line,
164+
closing_tag_inline=closing_tag_inline,
165+
)
115166

116167

117168
def create_virtual_render_content(

0 commit comments

Comments
 (0)