Skip to content

Commit 322b3d9

Browse files
committed
Updated dry-run mode to show the file diff instead of per-command diff.
1 parent c5ac30e commit 322b3d9

File tree

1 file changed

+115
-57
lines changed
  • awsclilinter/awsclilinter

1 file changed

+115
-57
lines changed

awsclilinter/awsclilinter/cli.py

Lines changed: 115 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
import difflib
33
import sys
44
from pathlib import Path
5-
from typing import List, Optional, Tuple
5+
from typing import List, Optional, Tuple, Any
66

7-
from ast_grep_py.ast_grep_py import SgRoot
7+
from ast_grep_py import SgRoot
8+
from ast_grep_py.ast_grep_py import SgRoot, SgNode
89

910
from awsclilinter import linter
1011
from awsclilinter.linter import parse
@@ -27,14 +28,14 @@
2728
CONTEXT_SIZE = 3
2829

2930

30-
def color_text(text, color_code):
31+
def _color_text(text, color_code):
3132
"""Colorize text for terminal output only if a TTY is available."""
3233
if sys.stdout.isatty():
3334
return f"{color_code}{text}{RESET}"
3435
return text
3536

3637

37-
def prompt_user_choice_interactive_mode(auto_fixable: bool = True) -> str:
38+
def _prompt_user_choice_interactive_mode(auto_fixable: bool = True) -> str:
3839
"""Get user input for interactive mode."""
3940
while True:
4041
if auto_fixable:
@@ -56,7 +57,7 @@ def prompt_user_choice_interactive_mode(auto_fixable: bool = True) -> str:
5657
print("Invalid choice. Please enter n, s, or q.")
5758

5859

59-
def display_finding(finding: LintFinding, index: int, script_content: str):
60+
def _display_finding(finding: LintFinding, index: int, script_content: str):
6061
"""Display a finding to the user with context."""
6162
if finding.auto_fixable:
6263
# Apply the edit to get the fixed content
@@ -79,13 +80,13 @@ def display_finding(finding: LintFinding, index: int, script_content: str):
7980
elif line_num == 2:
8081
# The 3rd line is the context control line.
8182
print("\n")
82-
print(color_text(line, CYAN))
83+
print(_color_text(line, CYAN))
8384
elif line.startswith("-"):
8485
# Removed line
85-
print(color_text(line, RED))
86+
print(_color_text(line, RED))
8687
elif line.startswith("+"):
8788
# Added line
88-
print(color_text(line, GREEN))
89+
print(_color_text(line, GREEN))
8990
else:
9091
# Context (unchanged) lines always start with whitespace.
9192
print(line)
@@ -97,42 +98,31 @@ def display_finding(finding: LintFinding, index: int, script_content: str):
9798
context_start = max(0, start_line - CONTEXT_SIZE)
9899
context_end = min(len(src_lines), end_line + CONTEXT_SIZE + 1)
99100

100-
manual_tag = color_text("[MANUAL REVIEW REQUIRED]", YELLOW)
101+
manual_tag = _color_text("[MANUAL REVIEW REQUIRED]", YELLOW)
101102
print(f"\n[{index}] {finding.rule_name} {manual_tag}")
102103
print(f"{finding.description}\n")
103104

104-
print(color_text(f"Lines {context_start + 1}-{context_end + 1}", CYAN))
105+
print(_color_text(f"Lines {context_start + 1}-{context_end + 1}", CYAN))
105106
for i in range(context_start, context_end):
106107
line = src_lines[i]
107108
if start_line <= i <= end_line:
108-
print(f"{color_text(line, YELLOW)}")
109+
print(f"{_color_text(line, YELLOW)}")
109110
else:
110111
print(f"{line}")
111112

112-
warning_msg = color_text(
113+
warning_msg = _color_text(
113114
f"⚠️ This issue requires manual intervention. "
114115
f"Suggested action: {finding.suggested_manual_fix}",
115116
YELLOW,
116117
)
117118
print(f"\n{warning_msg}")
118119

119120

120-
def auto_fix_mode(
121+
def _lint_and_fix_script(
121122
rules: List[LintRule],
122-
script_content: str,
123-
output_path: Path,
124-
):
125-
"""Handler for auto-fix mode. Lints the input script based on the input rules list. If
126-
any findings were detected that can be automatically fixed, applies the automatic
127-
fixes to the script and writes it to the output path.
128-
129-
Args:
130-
rules: List of findings and their rules.
131-
script_content: Current script represented as an AST.
132-
output_path: The path to write the updated script if any findings were detected.
133-
"""
134-
current_ast = parse(script_content)
135-
123+
script_ast: SgRoot,
124+
) -> tuple[SgRoot, int, list[tuple[LintFinding, LintRule]]]:
125+
current_ast = script_ast
136126
findings_found = 0
137127
num_auto_fixable_findings = 0
138128
non_auto_fixable = []
@@ -154,6 +144,27 @@ def auto_fix_mode(
154144
continue
155145

156146
current_ast = parse(linter.apply_fixes(current_ast, auto_fixable_findings))
147+
return current_ast, num_auto_fixable_findings, non_auto_fixable
148+
149+
150+
def auto_fix_mode(
151+
rules: List[LintRule],
152+
script_content: str,
153+
output_path: Path,
154+
):
155+
"""Handler for auto-fix mode. Lints the input script based on the input rules list. If
156+
any findings were detected that can be automatically fixed, applies the automatic
157+
fixes to the script and writes it to the output path.
158+
159+
Args:
160+
rules: List of linting rules to run against the input script.
161+
script_content: Current script represented as an AST.
162+
output_path: The path to write the updated script if any findings were detected.
163+
"""
164+
current_ast, num_auto_fixable_findings, non_auto_fixable = _lint_and_fix_script(
165+
rules,
166+
parse(script_content)
167+
)
157168

158169
if num_auto_fixable_findings:
159170
output_path.write_text(current_ast.root().text())
@@ -164,12 +175,83 @@ def auto_fix_mode(
164175

165176
# If there were findings that need manual review, display them last.
166177
if non_auto_fixable:
167-
warning_header = color_text(
178+
warning_header = _color_text(
168179
f"⚠️ {len(non_auto_fixable)} issue(s) require manual review:", YELLOW
169180
)
170181
print(f"\n{warning_header}\n")
171182
for i, (finding, _) in enumerate(non_auto_fixable, 1):
172-
display_finding(finding, i, script_content)
183+
_display_finding(finding, i, script_content)
184+
185+
186+
def dry_run_mode(
187+
rules: List[LintRule],
188+
script_content: str,
189+
script_path: Path,
190+
):
191+
"""Handler for dry-run mode. Lints the input script based on the input rules list and
192+
prints the diff to stdout.
193+
194+
Args:
195+
rules: List of linting rules to run against the input script.
196+
script_content: The input script.
197+
script_path: Path to the script being linted.
198+
"""
199+
current_ast, num_auto_fixable_findings, non_auto_fixable = _lint_and_fix_script(
200+
rules,
201+
parse(script_content)
202+
)
203+
204+
if not num_auto_fixable_findings and not non_auto_fixable:
205+
print("No issues found.")
206+
return
207+
208+
print(f"\nFound {num_auto_fixable_findings + len(non_auto_fixable)} issue(s):")
209+
if num_auto_fixable_findings and non_auto_fixable:
210+
print(f" - {num_auto_fixable_findings} can be automatically fixed")
211+
print(f" - {len(non_auto_fixable)} require manual review")
212+
print()
213+
214+
diff = difflib.unified_diff(
215+
script_content.splitlines(),
216+
current_ast.root().text().splitlines(),
217+
n=CONTEXT_SIZE,
218+
lineterm="",
219+
)
220+
for line_num, line in enumerate(diff):
221+
if line_num == 0:
222+
# First line is always '--- '
223+
print(f"{line}a/{script_path}")
224+
elif line_num == 1:
225+
# Second line is always '+++ '
226+
print(f"{line}b/{script_path}")
227+
elif line.startswith("@"):
228+
# Context control line.
229+
print(f"\n{_color_text(line, CYAN)}")
230+
elif line.startswith("-"):
231+
# Removed line
232+
print(_color_text(line, RED))
233+
elif line.startswith("+"):
234+
# Added line
235+
print(_color_text(line, GREEN))
236+
else:
237+
# Context (unchanged) lines always start with whitespace.
238+
print(line)
239+
240+
if non_auto_fixable:
241+
warning_header = _color_text(
242+
f"⚠️ {len(non_auto_fixable)} issue(s) require manual review:", YELLOW
243+
)
244+
print(f"\n{warning_header}\n")
245+
for i, (finding, _) in enumerate(non_auto_fixable, 1):
246+
_display_finding(finding, i, script_content)
247+
248+
if num_auto_fixable_findings:
249+
print(
250+
"\n\nRun with `--fix` to apply automatic fixes to the script, "
251+
"or `--output PATH` to write the modified script to a specific path."
252+
)
253+
else:
254+
print("\n\nAll issues require manual review.")
173255

174256

175257
def interactive_mode_for_rule(
@@ -194,11 +276,11 @@ def interactive_mode_for_rule(
194276
last_choice: Optional[str] = None
195277

196278
for i, finding in enumerate(findings):
197-
display_finding(finding, finding_offset + i + 1, ast.root().text())
279+
_display_finding(finding, finding_offset + i + 1, ast.root().text())
198280

199281
if not finding.auto_fixable:
200282
# Non-fixable finding - only allow next, save, or quit
201-
last_choice = prompt_user_choice_interactive_mode(auto_fixable=False)
283+
last_choice = _prompt_user_choice_interactive_mode(auto_fixable=False)
202284
if last_choice == "q":
203285
print("Quit without saving.")
204286
sys.exit(0)
@@ -210,7 +292,7 @@ def interactive_mode_for_rule(
210292
# 'n' means continue to next finding
211293
continue
212294

213-
last_choice = prompt_user_choice_interactive_mode(auto_fixable=True)
295+
last_choice = _prompt_user_choice_interactive_mode(auto_fixable=True)
214296

215297
if last_choice == "y":
216298
accepted_findings.append(finding)
@@ -341,31 +423,7 @@ def main():
341423
elif args.fix or args.output:
342424
auto_fix_mode(rules, script_content, Path(args.output) if args.output else script_path)
343425
else:
344-
current_ast = parse(script_content)
345-
findings_with_rules = linter.lint(current_ast, rules)
346-
347-
if not findings_with_rules:
348-
print("No issues found.")
349-
return
350-
351-
fixable = [(f, r) for f, r in findings_with_rules if f.auto_fixable]
352-
non_fixable = [(f, r) for f, r in findings_with_rules if not f.auto_fixable]
353-
354-
print(f"\nFound {len(findings_with_rules)} issue(s):")
355-
if fixable and non_fixable:
356-
print(f" - {len(fixable)} can be automatically fixed")
357-
print(f" - {len(non_fixable)} require manual review")
358-
print()
359-
360-
for i, (finding, _) in enumerate(findings_with_rules, 1):
361-
display_finding(finding, i, script_content)
362-
363-
if fixable:
364-
print("\n\nRun with --fix to apply automatic fixes")
365-
if non_fixable:
366-
print("Non-fixable issues will be shown for manual review")
367-
else:
368-
print("\n\nAll issues require manual review")
426+
dry_run_mode(rules, script_content, script_path)
369427

370428

371429
if __name__ == "__main__":

0 commit comments

Comments
 (0)