scitex-linter 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scitex_linter/__init__.py +3 -0
- scitex_linter/_mcp/__init__.py +1 -0
- scitex_linter/_mcp/tools/__init__.py +8 -0
- scitex_linter/_mcp/tools/lint.py +67 -0
- scitex_linter/_server.py +23 -0
- scitex_linter/checker.py +469 -0
- scitex_linter/cli.py +449 -0
- scitex_linter/flake8_plugin.py +34 -0
- scitex_linter/formatter.py +95 -0
- scitex_linter/rules.py +384 -0
- scitex_linter/runner.py +72 -0
- scitex_linter-0.1.0.dist-info/METADATA +277 -0
- scitex_linter-0.1.0.dist-info/RECORD +17 -0
- scitex_linter-0.1.0.dist-info/WHEEL +5 -0
- scitex_linter-0.1.0.dist-info/entry_points.txt +5 -0
- scitex_linter-0.1.0.dist-info/licenses/LICENSE +661 -0
- scitex_linter-0.1.0.dist-info/top_level.txt +1 -0
scitex_linter/cli.py
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
"""CLI entry point for scitex-linter.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
scitex-linter lint <path> [--json] [--severity] [--category] [--no-color]
|
|
5
|
+
scitex-linter python <script.py> [--strict] [-- script_args...]
|
|
6
|
+
scitex-linter list-rules [--json] [--category] [--severity]
|
|
7
|
+
scitex-linter mcp start
|
|
8
|
+
scitex-linter mcp list-tools
|
|
9
|
+
scitex-linter --help-recursive
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from . import __version__
|
|
18
|
+
from .checker import lint_file
|
|
19
|
+
from .formatter import format_issue, format_summary, to_json
|
|
20
|
+
from .rules import ALL_RULES, SEVERITY_ORDER
|
|
21
|
+
|
|
22
|
+
# =========================================================================
|
|
23
|
+
# File collection helper
|
|
24
|
+
# =========================================================================
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _collect_files(path: Path, recursive: bool = True) -> list:
|
|
28
|
+
"""Collect Python files from a path."""
|
|
29
|
+
if path.is_file():
|
|
30
|
+
return [path]
|
|
31
|
+
if path.is_dir():
|
|
32
|
+
pattern = "**/*.py" if recursive else "*.py"
|
|
33
|
+
skip = {"__pycache__", ".git", "node_modules", ".tox", "venv", ".venv"}
|
|
34
|
+
return sorted(
|
|
35
|
+
p for p in path.glob(pattern) if not any(s in p.parts for s in skip)
|
|
36
|
+
)
|
|
37
|
+
return []
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# =========================================================================
|
|
41
|
+
# Subcommand: lint
|
|
42
|
+
# =========================================================================
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _register_lint(subparsers) -> None:
|
|
46
|
+
p = subparsers.add_parser(
|
|
47
|
+
"lint",
|
|
48
|
+
help="Lint Python files for SciTeX pattern compliance",
|
|
49
|
+
description="Lint Python files for SciTeX pattern compliance.",
|
|
50
|
+
)
|
|
51
|
+
p.add_argument("path", help="Python file or directory to lint")
|
|
52
|
+
p.add_argument("--json", action="store_true", dest="as_json", help="Output as JSON")
|
|
53
|
+
p.add_argument("--no-color", action="store_true", help="Disable colored output")
|
|
54
|
+
p.add_argument(
|
|
55
|
+
"--severity",
|
|
56
|
+
choices=["error", "warning", "info"],
|
|
57
|
+
default="info",
|
|
58
|
+
help="Minimum severity to report (default: info)",
|
|
59
|
+
)
|
|
60
|
+
p.add_argument(
|
|
61
|
+
"--category",
|
|
62
|
+
help="Filter by category (comma-separated: structure,import,io,plot,stats)",
|
|
63
|
+
)
|
|
64
|
+
p.set_defaults(func=_cmd_lint)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _cmd_lint(args) -> int:
|
|
68
|
+
use_color = not args.no_color and sys.stdout.isatty()
|
|
69
|
+
min_sev = SEVERITY_ORDER[args.severity]
|
|
70
|
+
categories = set(args.category.split(",")) if args.category else None
|
|
71
|
+
|
|
72
|
+
target = Path(args.path)
|
|
73
|
+
if not target.exists():
|
|
74
|
+
print(f"Error: {args.path} not found", file=sys.stderr)
|
|
75
|
+
return 2
|
|
76
|
+
|
|
77
|
+
files = _collect_files(target)
|
|
78
|
+
if not files:
|
|
79
|
+
print(f"No Python files found in {args.path}", file=sys.stderr)
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
all_results = {}
|
|
83
|
+
for f in files:
|
|
84
|
+
issues = lint_file(str(f))
|
|
85
|
+
issues = [
|
|
86
|
+
i
|
|
87
|
+
for i in issues
|
|
88
|
+
if SEVERITY_ORDER[i.rule.severity] >= min_sev
|
|
89
|
+
and (categories is None or i.rule.category in categories)
|
|
90
|
+
]
|
|
91
|
+
if issues:
|
|
92
|
+
all_results[str(f)] = issues
|
|
93
|
+
|
|
94
|
+
# JSON output
|
|
95
|
+
if args.as_json:
|
|
96
|
+
combined = {fp: to_json(issues, fp) for fp, issues in all_results.items()}
|
|
97
|
+
print(json.dumps(combined, indent=2))
|
|
98
|
+
has_errors = any(
|
|
99
|
+
any(i.rule.severity == "error" for i in issues)
|
|
100
|
+
for issues in all_results.values()
|
|
101
|
+
)
|
|
102
|
+
return 2 if has_errors else (1 if all_results else 0)
|
|
103
|
+
|
|
104
|
+
# Terminal output
|
|
105
|
+
if not all_results:
|
|
106
|
+
msg = "All files clean"
|
|
107
|
+
if use_color:
|
|
108
|
+
print(f"\033[92m{msg}\033[0m")
|
|
109
|
+
else:
|
|
110
|
+
print(msg)
|
|
111
|
+
return 0
|
|
112
|
+
|
|
113
|
+
has_errors = False
|
|
114
|
+
for filepath, issues in all_results.items():
|
|
115
|
+
for issue in issues:
|
|
116
|
+
print(format_issue(issue, filepath, color=use_color))
|
|
117
|
+
if issue.rule.severity == "error":
|
|
118
|
+
has_errors = True
|
|
119
|
+
print(format_summary(issues, filepath, color=use_color))
|
|
120
|
+
print()
|
|
121
|
+
|
|
122
|
+
return 2 if has_errors else 1
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# =========================================================================
|
|
126
|
+
# Subcommand: python (lint then execute)
|
|
127
|
+
# =========================================================================
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _register_python(subparsers) -> None:
|
|
131
|
+
p = subparsers.add_parser(
|
|
132
|
+
"python",
|
|
133
|
+
help="Lint then execute a Python script",
|
|
134
|
+
description=(
|
|
135
|
+
"Lint a Python script, then execute it.\n"
|
|
136
|
+
"Use -- to separate script arguments: scitex-linter python script.py -- --arg1"
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
p.add_argument("script", help="Python script to run")
|
|
140
|
+
p.add_argument("--strict", action="store_true", help="Abort on lint errors")
|
|
141
|
+
p.set_defaults(func=_cmd_python)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _cmd_python(args) -> int:
|
|
145
|
+
from .runner import run_script
|
|
146
|
+
|
|
147
|
+
# Extract script args: everything after -- in sys.argv (or test argv)
|
|
148
|
+
# argparse already consumed known flags; remaining unknown args go to script
|
|
149
|
+
script_args = getattr(args, "_script_args", [])
|
|
150
|
+
return run_script(args.script, strict=args.strict, script_args=script_args)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# =========================================================================
|
|
154
|
+
# Subcommand: list-rules
|
|
155
|
+
# =========================================================================
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _register_list_rules(subparsers) -> None:
|
|
159
|
+
p = subparsers.add_parser(
|
|
160
|
+
"list-rules",
|
|
161
|
+
help="List all lint rules",
|
|
162
|
+
description="List all available SciTeX lint rules.",
|
|
163
|
+
)
|
|
164
|
+
p.add_argument("--json", action="store_true", dest="as_json", help="Output as JSON")
|
|
165
|
+
p.add_argument(
|
|
166
|
+
"--category",
|
|
167
|
+
help="Filter by category (comma-separated: structure,import,io,plot,stats)",
|
|
168
|
+
)
|
|
169
|
+
p.add_argument(
|
|
170
|
+
"--severity",
|
|
171
|
+
choices=["error", "warning", "info"],
|
|
172
|
+
help="Filter by severity",
|
|
173
|
+
)
|
|
174
|
+
p.set_defaults(func=_cmd_list_rules)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _cmd_list_rules(args) -> int:
|
|
178
|
+
categories = set(args.category.split(",")) if args.category else None
|
|
179
|
+
rules_list = list(ALL_RULES.values())
|
|
180
|
+
|
|
181
|
+
if categories:
|
|
182
|
+
rules_list = [r for r in rules_list if r.category in categories]
|
|
183
|
+
if args.severity:
|
|
184
|
+
rules_list = [r for r in rules_list if r.severity == args.severity]
|
|
185
|
+
|
|
186
|
+
if args.as_json:
|
|
187
|
+
data = [
|
|
188
|
+
{
|
|
189
|
+
"id": r.id,
|
|
190
|
+
"severity": r.severity,
|
|
191
|
+
"category": r.category,
|
|
192
|
+
"message": r.message,
|
|
193
|
+
"suggestion": r.suggestion,
|
|
194
|
+
}
|
|
195
|
+
for r in rules_list
|
|
196
|
+
]
|
|
197
|
+
print(json.dumps(data, indent=2))
|
|
198
|
+
return 0
|
|
199
|
+
|
|
200
|
+
use_color = sys.stdout.isatty()
|
|
201
|
+
sev_color = {"error": "\033[91m", "warning": "\033[93m", "info": "\033[94m"}
|
|
202
|
+
reset = "\033[0m"
|
|
203
|
+
|
|
204
|
+
for r in rules_list:
|
|
205
|
+
if use_color:
|
|
206
|
+
c = sev_color.get(r.severity, "")
|
|
207
|
+
print(f" {c}{r.id}{reset} [{r.severity}] {r.message}")
|
|
208
|
+
else:
|
|
209
|
+
print(f" {r.id} [{r.severity}] {r.message}")
|
|
210
|
+
|
|
211
|
+
print(f"\n {len(rules_list)} rules")
|
|
212
|
+
return 0
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# =========================================================================
|
|
216
|
+
# Subcommand: mcp
|
|
217
|
+
# =========================================================================
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _register_mcp(subparsers) -> None:
|
|
221
|
+
p = subparsers.add_parser(
|
|
222
|
+
"mcp",
|
|
223
|
+
help="MCP server commands",
|
|
224
|
+
description="Manage the scitex-linter MCP server.",
|
|
225
|
+
)
|
|
226
|
+
mcp_sub = p.add_subparsers(dest="mcp_command")
|
|
227
|
+
|
|
228
|
+
start_p = mcp_sub.add_parser("start", help="Start the MCP server (stdio)")
|
|
229
|
+
start_p.add_argument(
|
|
230
|
+
"--transport",
|
|
231
|
+
choices=["stdio", "sse"],
|
|
232
|
+
default="stdio",
|
|
233
|
+
help="Transport mode (default: stdio)",
|
|
234
|
+
)
|
|
235
|
+
start_p.set_defaults(func=_cmd_mcp_start)
|
|
236
|
+
|
|
237
|
+
list_p = mcp_sub.add_parser("list-tools", help="List available MCP tools")
|
|
238
|
+
list_p.set_defaults(func=_cmd_mcp_list_tools)
|
|
239
|
+
|
|
240
|
+
doctor_p = mcp_sub.add_parser("doctor", help="Check MCP server health")
|
|
241
|
+
doctor_p.set_defaults(func=_cmd_mcp_doctor)
|
|
242
|
+
|
|
243
|
+
install_p = mcp_sub.add_parser(
|
|
244
|
+
"installation", help="Show Claude Desktop configuration"
|
|
245
|
+
)
|
|
246
|
+
install_p.set_defaults(func=_cmd_mcp_installation)
|
|
247
|
+
|
|
248
|
+
p.set_defaults(func=lambda args: _cmd_mcp_help(p, args))
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _cmd_mcp_help(parser, args) -> int:
|
|
252
|
+
if not hasattr(args, "mcp_command") or args.mcp_command is None:
|
|
253
|
+
parser.print_help()
|
|
254
|
+
return 0
|
|
255
|
+
return 0
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _cmd_mcp_start(args) -> int:
|
|
259
|
+
try:
|
|
260
|
+
from ._server import run_server
|
|
261
|
+
|
|
262
|
+
run_server(transport=args.transport)
|
|
263
|
+
return 0
|
|
264
|
+
except ImportError:
|
|
265
|
+
print(
|
|
266
|
+
"fastmcp is required for MCP server. "
|
|
267
|
+
"Install with: pip install scitex-linter[mcp]",
|
|
268
|
+
file=sys.stderr,
|
|
269
|
+
)
|
|
270
|
+
return 1
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _cmd_mcp_list_tools(args) -> int:
|
|
274
|
+
tools = [
|
|
275
|
+
("linter_lint", "Lint a Python file for SciTeX pattern compliance"),
|
|
276
|
+
("linter_list_rules", "List all available lint rules"),
|
|
277
|
+
("linter_check_source", "Lint Python source code string"),
|
|
278
|
+
]
|
|
279
|
+
for name, desc in tools:
|
|
280
|
+
print(f" {name:30s} {desc}")
|
|
281
|
+
print(f"\n {len(tools)} tools")
|
|
282
|
+
return 0
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _cmd_mcp_doctor(args) -> int:
|
|
286
|
+
import shutil
|
|
287
|
+
|
|
288
|
+
print(f"scitex-linter {__version__}\n")
|
|
289
|
+
print("Health Check")
|
|
290
|
+
print("=" * 40)
|
|
291
|
+
|
|
292
|
+
checks = []
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
import fastmcp
|
|
296
|
+
|
|
297
|
+
checks.append(("fastmcp", True, fastmcp.__version__))
|
|
298
|
+
except ImportError:
|
|
299
|
+
checks.append(("fastmcp", False, "not installed"))
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
from ._mcp.tools import register_all_tools # noqa: F401
|
|
303
|
+
|
|
304
|
+
checks.append(("MCP tools", True, "3 tools"))
|
|
305
|
+
except Exception as e:
|
|
306
|
+
checks.append(("MCP tools", False, str(e)))
|
|
307
|
+
|
|
308
|
+
cli_path = shutil.which("scitex-linter")
|
|
309
|
+
if cli_path:
|
|
310
|
+
checks.append(("CLI", True, cli_path))
|
|
311
|
+
else:
|
|
312
|
+
checks.append(("CLI", False, "not in PATH"))
|
|
313
|
+
|
|
314
|
+
rule_count = len(ALL_RULES)
|
|
315
|
+
checks.append(("Rules", True, f"{rule_count} rules"))
|
|
316
|
+
|
|
317
|
+
all_ok = True
|
|
318
|
+
for name, ok, info in checks:
|
|
319
|
+
status = "\u2713" if ok else "\u2717"
|
|
320
|
+
if not ok:
|
|
321
|
+
all_ok = False
|
|
322
|
+
print(f" {status} {name}: {info}")
|
|
323
|
+
|
|
324
|
+
print()
|
|
325
|
+
if all_ok:
|
|
326
|
+
print("All checks passed!")
|
|
327
|
+
else:
|
|
328
|
+
print("Some checks failed. Run 'pip install scitex-linter[mcp]' to fix.")
|
|
329
|
+
|
|
330
|
+
return 0 if all_ok else 1
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _cmd_mcp_installation(args) -> int:
|
|
334
|
+
import shutil
|
|
335
|
+
|
|
336
|
+
print(f"scitex-linter {__version__}\n")
|
|
337
|
+
print("Add this to your Claude Desktop config file:\n")
|
|
338
|
+
print(" macOS: ~/Library/Application Support/Claude/claude_desktop_config.json")
|
|
339
|
+
print(" Linux: ~/.config/Claude/claude_desktop_config.json\n")
|
|
340
|
+
|
|
341
|
+
cli_path = shutil.which("scitex-linter")
|
|
342
|
+
if cli_path:
|
|
343
|
+
print(f"Your installation path: {cli_path}\n")
|
|
344
|
+
|
|
345
|
+
config = (
|
|
346
|
+
"{\n"
|
|
347
|
+
' "mcpServers": {\n'
|
|
348
|
+
' "scitex-linter": {\n'
|
|
349
|
+
f' "command": "{cli_path or "scitex-linter"}",\n'
|
|
350
|
+
' "args": ["mcp", "start"]\n'
|
|
351
|
+
" }\n"
|
|
352
|
+
" }\n"
|
|
353
|
+
"}"
|
|
354
|
+
)
|
|
355
|
+
print(config)
|
|
356
|
+
return 0
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# =========================================================================
|
|
360
|
+
# --help-recursive
|
|
361
|
+
# =========================================================================
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _print_help_recursive(parser, subparsers_actions) -> None:
|
|
365
|
+
"""Print help for all commands recursively."""
|
|
366
|
+
cyan = "\033[96m" if sys.stdout.isatty() else ""
|
|
367
|
+
bold = "\033[1m" if sys.stdout.isatty() else ""
|
|
368
|
+
reset = "\033[0m" if sys.stdout.isatty() else ""
|
|
369
|
+
|
|
370
|
+
bar = "\u2501" * 3
|
|
371
|
+
print(f"\n{cyan}{bar} scitex-linter {bar}{reset}\n")
|
|
372
|
+
parser.print_help()
|
|
373
|
+
|
|
374
|
+
for action in subparsers_actions:
|
|
375
|
+
for choice, subparser in action.choices.items():
|
|
376
|
+
print(f"\n{cyan}{bar} scitex-linter {choice} {bar}{reset}\n")
|
|
377
|
+
subparser.print_help()
|
|
378
|
+
|
|
379
|
+
# Nested subparsers (e.g., mcp -> start, list-tools)
|
|
380
|
+
if subparser._subparsers is not None:
|
|
381
|
+
for sub_action in subparser._subparsers._group_actions:
|
|
382
|
+
if not hasattr(sub_action, "choices") or not sub_action.choices:
|
|
383
|
+
continue
|
|
384
|
+
for sub_choice, sub_subparser in sub_action.choices.items():
|
|
385
|
+
print(
|
|
386
|
+
f"\n{cyan}{bar} scitex-linter {choice} {sub_choice} {bar}{reset}\n"
|
|
387
|
+
)
|
|
388
|
+
sub_subparser.print_help()
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
# =========================================================================
|
|
392
|
+
# Main entry point
|
|
393
|
+
# =========================================================================
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def main(argv: list = None) -> int:
|
|
397
|
+
parser = argparse.ArgumentParser(
|
|
398
|
+
prog="scitex-linter",
|
|
399
|
+
description="SciTeX Linter \u2014 enforce reproducible research patterns",
|
|
400
|
+
)
|
|
401
|
+
parser.add_argument(
|
|
402
|
+
"-V", "--version", action="version", version=f"%(prog)s {__version__}"
|
|
403
|
+
)
|
|
404
|
+
parser.add_argument(
|
|
405
|
+
"--help-recursive",
|
|
406
|
+
action="store_true",
|
|
407
|
+
help="Show help for all commands",
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
411
|
+
|
|
412
|
+
_register_lint(subparsers)
|
|
413
|
+
_register_python(subparsers)
|
|
414
|
+
_register_list_rules(subparsers)
|
|
415
|
+
_register_mcp(subparsers)
|
|
416
|
+
|
|
417
|
+
# Split on -- to capture script args for the 'python' subcommand
|
|
418
|
+
raw = argv if argv is not None else sys.argv[1:]
|
|
419
|
+
script_args = []
|
|
420
|
+
if "--" in raw:
|
|
421
|
+
idx = raw.index("--")
|
|
422
|
+
script_args = raw[idx + 1 :]
|
|
423
|
+
raw = raw[:idx]
|
|
424
|
+
|
|
425
|
+
args = parser.parse_args(raw)
|
|
426
|
+
|
|
427
|
+
# Attach script_args for the run subcommand
|
|
428
|
+
args._script_args = script_args
|
|
429
|
+
|
|
430
|
+
if args.help_recursive:
|
|
431
|
+
subparsers_actions = [
|
|
432
|
+
a for a in parser._subparsers._group_actions if hasattr(a, "choices")
|
|
433
|
+
]
|
|
434
|
+
_print_help_recursive(parser, subparsers_actions)
|
|
435
|
+
return 0
|
|
436
|
+
|
|
437
|
+
if args.command is None:
|
|
438
|
+
parser.print_help()
|
|
439
|
+
return 0
|
|
440
|
+
|
|
441
|
+
if hasattr(args, "func"):
|
|
442
|
+
return args.func(args)
|
|
443
|
+
|
|
444
|
+
parser.print_help()
|
|
445
|
+
return 0
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
if __name__ == "__main__":
|
|
449
|
+
sys.exit(main())
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Flake8 plugin for SciTeX linter.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
pip install scitex-linter
|
|
5
|
+
flake8 --select STX script.py
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
|
|
10
|
+
from .checker import SciTeXChecker
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SciTeXFlake8Checker:
|
|
14
|
+
"""Flake8 checker wrapping the SciTeX AST visitor."""
|
|
15
|
+
|
|
16
|
+
name = "scitex-linter"
|
|
17
|
+
version = "0.1.0"
|
|
18
|
+
|
|
19
|
+
def __init__(self, tree: ast.AST, filename: str = "<stdin>", lines: list = None):
|
|
20
|
+
self._tree = tree
|
|
21
|
+
self._filename = filename
|
|
22
|
+
self._lines = lines or []
|
|
23
|
+
|
|
24
|
+
def run(self):
|
|
25
|
+
"""Yield (line, col, message, type) tuples for flake8."""
|
|
26
|
+
source_lines = [line.rstrip("\n") for line in self._lines]
|
|
27
|
+
checker = SciTeXChecker(source_lines, filepath=self._filename)
|
|
28
|
+
checker.visit(self._tree)
|
|
29
|
+
|
|
30
|
+
for issue in checker.get_issues():
|
|
31
|
+
# flake8 format: (line, col, "CODE message", type)
|
|
32
|
+
code = issue.rule.id.replace("-", "") # STX-S001 -> STXS001
|
|
33
|
+
msg = f"{code} {issue.rule.message}"
|
|
34
|
+
yield (issue.line, issue.col, msg, type(self))
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Output formatting for terminal and JSON."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from .checker import Issue
|
|
5
|
+
|
|
6
|
+
# ANSI colors
|
|
7
|
+
_RED = "\033[91m"
|
|
8
|
+
_YELLOW = "\033[93m"
|
|
9
|
+
_BLUE = "\033[94m"
|
|
10
|
+
_GREEN = "\033[92m"
|
|
11
|
+
_GRAY = "\033[90m"
|
|
12
|
+
_BOLD = "\033[1m"
|
|
13
|
+
_RESET = "\033[0m"
|
|
14
|
+
|
|
15
|
+
_SEV_COLOR = {"error": _RED, "warning": _YELLOW, "info": _BLUE}
|
|
16
|
+
_SEV_ICON = {"error": "E", "warning": "W", "info": "I"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def format_issue(issue: Issue, filepath: str, color: bool = True) -> str:
|
|
20
|
+
if not color:
|
|
21
|
+
return _format_plain(issue, filepath)
|
|
22
|
+
|
|
23
|
+
sev = issue.rule.severity
|
|
24
|
+
c = _SEV_COLOR.get(sev, "")
|
|
25
|
+
icon = _SEV_ICON.get(sev, "?")
|
|
26
|
+
lines = [
|
|
27
|
+
f" {c}{icon}{_RESET} {_BOLD}{filepath}:{issue.line}:{issue.col}{_RESET}"
|
|
28
|
+
f" {c}{issue.rule.id}{_RESET}",
|
|
29
|
+
]
|
|
30
|
+
if issue.source_line:
|
|
31
|
+
lines.append(f" {_GRAY}{issue.source_line}{_RESET}")
|
|
32
|
+
lines.append(f" {c}{issue.rule.message}{_RESET}")
|
|
33
|
+
lines.append(f" {_GREEN}{issue.rule.suggestion}{_RESET}")
|
|
34
|
+
return "\n".join(lines)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _format_plain(issue: Issue, filepath: str) -> str:
|
|
38
|
+
icon = _SEV_ICON.get(issue.rule.severity, "?")
|
|
39
|
+
lines = [
|
|
40
|
+
f" {icon} {filepath}:{issue.line}:{issue.col} {issue.rule.id}",
|
|
41
|
+
]
|
|
42
|
+
if issue.source_line:
|
|
43
|
+
lines.append(f" {issue.source_line}")
|
|
44
|
+
lines.append(f" {issue.rule.message}")
|
|
45
|
+
lines.append(f" {issue.rule.suggestion}")
|
|
46
|
+
return "\n".join(lines)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def format_summary(issues: list, filepath: str, color: bool = True) -> str:
|
|
50
|
+
if not issues:
|
|
51
|
+
if color:
|
|
52
|
+
return f"{_GREEN}OK{_RESET} {filepath}"
|
|
53
|
+
return f"OK {filepath}"
|
|
54
|
+
|
|
55
|
+
errors = sum(1 for i in issues if i.rule.severity == "error")
|
|
56
|
+
warnings = sum(1 for i in issues if i.rule.severity == "warning")
|
|
57
|
+
infos = sum(1 for i in issues if i.rule.severity == "info")
|
|
58
|
+
|
|
59
|
+
parts = []
|
|
60
|
+
if errors:
|
|
61
|
+
label = f"{errors} error{'s' if errors != 1 else ''}"
|
|
62
|
+
parts.append(f"{_RED}{label}{_RESET}" if color else label)
|
|
63
|
+
if warnings:
|
|
64
|
+
label = f"{warnings} warning{'s' if warnings != 1 else ''}"
|
|
65
|
+
parts.append(f"{_YELLOW}{label}{_RESET}" if color else label)
|
|
66
|
+
if infos:
|
|
67
|
+
label = f"{infos} info"
|
|
68
|
+
parts.append(f"{_BLUE}{label}{_RESET}" if color else label)
|
|
69
|
+
|
|
70
|
+
fp = f"{_BOLD}{filepath}{_RESET}" if color else filepath
|
|
71
|
+
return f" {', '.join(parts)} in {fp}"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def to_json(issues: list, filepath: str) -> dict:
|
|
75
|
+
return {
|
|
76
|
+
"file": filepath,
|
|
77
|
+
"issues": [
|
|
78
|
+
{
|
|
79
|
+
"rule_id": i.rule.id,
|
|
80
|
+
"severity": i.rule.severity,
|
|
81
|
+
"category": i.rule.category,
|
|
82
|
+
"line": i.line,
|
|
83
|
+
"col": i.col,
|
|
84
|
+
"message": i.rule.message,
|
|
85
|
+
"suggestion": i.rule.suggestion,
|
|
86
|
+
"source_line": i.source_line,
|
|
87
|
+
}
|
|
88
|
+
for i in issues
|
|
89
|
+
],
|
|
90
|
+
"summary": {
|
|
91
|
+
"errors": sum(1 for i in issues if i.rule.severity == "error"),
|
|
92
|
+
"warnings": sum(1 for i in issues if i.rule.severity == "warning"),
|
|
93
|
+
"infos": sum(1 for i in issues if i.rule.severity == "info"),
|
|
94
|
+
},
|
|
95
|
+
}
|