yuho 5.0.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.
- yuho/__init__.py +16 -0
- yuho/ast/__init__.py +196 -0
- yuho/ast/builder.py +926 -0
- yuho/ast/constant_folder.py +280 -0
- yuho/ast/dead_code.py +199 -0
- yuho/ast/exhaustiveness.py +503 -0
- yuho/ast/nodes.py +907 -0
- yuho/ast/overlap.py +291 -0
- yuho/ast/reachability.py +293 -0
- yuho/ast/scope_analysis.py +490 -0
- yuho/ast/transformer.py +490 -0
- yuho/ast/type_check.py +471 -0
- yuho/ast/type_inference.py +425 -0
- yuho/ast/visitor.py +239 -0
- yuho/cli/__init__.py +14 -0
- yuho/cli/commands/__init__.py +1 -0
- yuho/cli/commands/api.py +431 -0
- yuho/cli/commands/ast_viz.py +334 -0
- yuho/cli/commands/check.py +218 -0
- yuho/cli/commands/config.py +311 -0
- yuho/cli/commands/contribute.py +122 -0
- yuho/cli/commands/diff.py +487 -0
- yuho/cli/commands/explain.py +240 -0
- yuho/cli/commands/fmt.py +253 -0
- yuho/cli/commands/generate.py +316 -0
- yuho/cli/commands/graph.py +410 -0
- yuho/cli/commands/init.py +120 -0
- yuho/cli/commands/library.py +656 -0
- yuho/cli/commands/lint.py +503 -0
- yuho/cli/commands/lsp.py +36 -0
- yuho/cli/commands/preview.py +377 -0
- yuho/cli/commands/repl.py +444 -0
- yuho/cli/commands/serve.py +44 -0
- yuho/cli/commands/test.py +528 -0
- yuho/cli/commands/transpile.py +121 -0
- yuho/cli/commands/wizard.py +370 -0
- yuho/cli/completions.py +182 -0
- yuho/cli/error_formatter.py +193 -0
- yuho/cli/main.py +1064 -0
- yuho/config/__init__.py +46 -0
- yuho/config/loader.py +235 -0
- yuho/config/mask.py +194 -0
- yuho/config/schema.py +147 -0
- yuho/library/__init__.py +84 -0
- yuho/library/index.py +328 -0
- yuho/library/install.py +699 -0
- yuho/library/lockfile.py +330 -0
- yuho/library/package.py +421 -0
- yuho/library/resolver.py +791 -0
- yuho/library/signature.py +335 -0
- yuho/llm/__init__.py +45 -0
- yuho/llm/config.py +75 -0
- yuho/llm/factory.py +123 -0
- yuho/llm/prompts.py +146 -0
- yuho/llm/providers.py +383 -0
- yuho/llm/utils.py +470 -0
- yuho/lsp/__init__.py +14 -0
- yuho/lsp/code_action_handler.py +518 -0
- yuho/lsp/completion_handler.py +85 -0
- yuho/lsp/diagnostics.py +100 -0
- yuho/lsp/hover_handler.py +130 -0
- yuho/lsp/server.py +1425 -0
- yuho/mcp/__init__.py +10 -0
- yuho/mcp/server.py +1452 -0
- yuho/parser/__init__.py +8 -0
- yuho/parser/source_location.py +108 -0
- yuho/parser/wrapper.py +311 -0
- yuho/testing/__init__.py +48 -0
- yuho/testing/coverage.py +274 -0
- yuho/testing/fixtures.py +263 -0
- yuho/transpile/__init__.py +52 -0
- yuho/transpile/alloy_transpiler.py +546 -0
- yuho/transpile/base.py +100 -0
- yuho/transpile/blocks_transpiler.py +338 -0
- yuho/transpile/english_transpiler.py +470 -0
- yuho/transpile/graphql_transpiler.py +404 -0
- yuho/transpile/json_transpiler.py +217 -0
- yuho/transpile/jsonld_transpiler.py +250 -0
- yuho/transpile/latex_preamble.py +161 -0
- yuho/transpile/latex_transpiler.py +406 -0
- yuho/transpile/latex_utils.py +206 -0
- yuho/transpile/mermaid_transpiler.py +357 -0
- yuho/transpile/registry.py +275 -0
- yuho/verify/__init__.py +43 -0
- yuho/verify/alloy.py +352 -0
- yuho/verify/combined.py +218 -0
- yuho/verify/z3_solver.py +1155 -0
- yuho-5.0.0.dist-info/METADATA +186 -0
- yuho-5.0.0.dist-info/RECORD +91 -0
- yuho-5.0.0.dist-info/WHEEL +4 -0
- yuho-5.0.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lint command - style checking and best practices.
|
|
3
|
+
|
|
4
|
+
Analyzes Yuho files for:
|
|
5
|
+
- Style violations (naming conventions, formatting)
|
|
6
|
+
- Best practice violations (missing elements, incomplete statutes)
|
|
7
|
+
- Potential issues (undefined references, unused definitions)
|
|
8
|
+
- Documentation quality (missing titles, empty descriptions)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional, List, Set, Dict
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from enum import Enum, auto
|
|
17
|
+
|
|
18
|
+
import click
|
|
19
|
+
|
|
20
|
+
from yuho.parser import Parser
|
|
21
|
+
from yuho.ast import ASTBuilder
|
|
22
|
+
from yuho.ast.nodes import (
|
|
23
|
+
ModuleNode, StatuteNode, ElementNode, PenaltyNode,
|
|
24
|
+
DefinitionEntry, IllustrationNode, StructDefNode,
|
|
25
|
+
FunctionDefNode, StringLit
|
|
26
|
+
)
|
|
27
|
+
from yuho.cli.error_formatter import Colors, colorize
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Severity(Enum):
|
|
31
|
+
"""Lint issue severity levels."""
|
|
32
|
+
ERROR = auto() # Must fix
|
|
33
|
+
WARNING = auto() # Should fix
|
|
34
|
+
INFO = auto() # Style suggestion
|
|
35
|
+
HINT = auto() # Optional improvement
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class LintIssue:
|
|
40
|
+
"""A single lint issue."""
|
|
41
|
+
rule: str
|
|
42
|
+
severity: Severity
|
|
43
|
+
message: str
|
|
44
|
+
line: Optional[int] = None
|
|
45
|
+
column: Optional[int] = None
|
|
46
|
+
suggestion: Optional[str] = None
|
|
47
|
+
|
|
48
|
+
def __str__(self) -> str:
|
|
49
|
+
loc = f":{self.line}" if self.line else ""
|
|
50
|
+
if self.column:
|
|
51
|
+
loc += f":{self.column}"
|
|
52
|
+
return f"[{self.rule}] {self.message}{loc}"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class LintRule:
|
|
56
|
+
"""Base class for lint rules."""
|
|
57
|
+
|
|
58
|
+
id: str = "base"
|
|
59
|
+
severity: Severity = Severity.WARNING
|
|
60
|
+
description: str = "Base lint rule"
|
|
61
|
+
|
|
62
|
+
def check(self, ast: ModuleNode, source: str) -> List[LintIssue]:
|
|
63
|
+
"""Run the lint check. Override in subclasses."""
|
|
64
|
+
return []
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class MissingStatuteTitleRule(LintRule):
|
|
68
|
+
"""Check for statutes without titles."""
|
|
69
|
+
|
|
70
|
+
id = "missing-title"
|
|
71
|
+
severity = Severity.WARNING
|
|
72
|
+
description = "Statute should have a descriptive title"
|
|
73
|
+
|
|
74
|
+
def check(self, ast: ModuleNode, source: str) -> List[LintIssue]:
|
|
75
|
+
issues = []
|
|
76
|
+
for statute in ast.statutes:
|
|
77
|
+
if not statute.title or not statute.title.value.strip():
|
|
78
|
+
loc = statute.source_location
|
|
79
|
+
issues.append(LintIssue(
|
|
80
|
+
rule=self.id,
|
|
81
|
+
severity=self.severity,
|
|
82
|
+
message=f"Statute {statute.section_number} is missing a title",
|
|
83
|
+
line=loc.line if loc else None,
|
|
84
|
+
suggestion="Add a descriptive title for the statute"
|
|
85
|
+
))
|
|
86
|
+
return issues
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class MissingElementsRule(LintRule):
|
|
90
|
+
"""Check for statutes without elements."""
|
|
91
|
+
|
|
92
|
+
id = "missing-elements"
|
|
93
|
+
severity = Severity.WARNING
|
|
94
|
+
description = "Statute should define offense elements"
|
|
95
|
+
|
|
96
|
+
def check(self, ast: ModuleNode, source: str) -> List[LintIssue]:
|
|
97
|
+
issues = []
|
|
98
|
+
for statute in ast.statutes:
|
|
99
|
+
if not statute.elements:
|
|
100
|
+
loc = statute.source_location
|
|
101
|
+
issues.append(LintIssue(
|
|
102
|
+
rule=self.id,
|
|
103
|
+
severity=self.severity,
|
|
104
|
+
message=f"Statute {statute.section_number} has no elements defined",
|
|
105
|
+
line=loc.line if loc else None,
|
|
106
|
+
suggestion="Define actus_reus and mens_rea elements"
|
|
107
|
+
))
|
|
108
|
+
return issues
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class MissingActusReusRule(LintRule):
|
|
112
|
+
"""Check for statutes without actus reus element."""
|
|
113
|
+
|
|
114
|
+
id = "missing-actus-reus"
|
|
115
|
+
severity = Severity.INFO
|
|
116
|
+
description = "Statute should have an actus reus element"
|
|
117
|
+
|
|
118
|
+
def check(self, ast: ModuleNode, source: str) -> List[LintIssue]:
|
|
119
|
+
issues = []
|
|
120
|
+
for statute in ast.statutes:
|
|
121
|
+
has_actus = any(e.element_type == "actus_reus" for e in statute.elements)
|
|
122
|
+
if statute.elements and not has_actus:
|
|
123
|
+
loc = statute.source_location
|
|
124
|
+
issues.append(LintIssue(
|
|
125
|
+
rule=self.id,
|
|
126
|
+
severity=self.severity,
|
|
127
|
+
message=f"Statute {statute.section_number} has no actus_reus element",
|
|
128
|
+
line=loc.line if loc else None,
|
|
129
|
+
))
|
|
130
|
+
return issues
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class MissingMensReaRule(LintRule):
|
|
134
|
+
"""Check for statutes without mens rea element."""
|
|
135
|
+
|
|
136
|
+
id = "missing-mens-rea"
|
|
137
|
+
severity = Severity.INFO
|
|
138
|
+
description = "Statute should have a mens rea element"
|
|
139
|
+
|
|
140
|
+
def check(self, ast: ModuleNode, source: str) -> List[LintIssue]:
|
|
141
|
+
issues = []
|
|
142
|
+
for statute in ast.statutes:
|
|
143
|
+
has_mens = any(e.element_type == "mens_rea" for e in statute.elements)
|
|
144
|
+
if statute.elements and not has_mens:
|
|
145
|
+
loc = statute.source_location
|
|
146
|
+
issues.append(LintIssue(
|
|
147
|
+
rule=self.id,
|
|
148
|
+
severity=self.severity,
|
|
149
|
+
message=f"Statute {statute.section_number} has no mens_rea element",
|
|
150
|
+
line=loc.line if loc else None,
|
|
151
|
+
))
|
|
152
|
+
return issues
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class MissingPenaltyRule(LintRule):
|
|
156
|
+
"""Check for statutes without penalty clause."""
|
|
157
|
+
|
|
158
|
+
id = "missing-penalty"
|
|
159
|
+
severity = Severity.INFO
|
|
160
|
+
description = "Statute should specify a penalty"
|
|
161
|
+
|
|
162
|
+
def check(self, ast: ModuleNode, source: str) -> List[LintIssue]:
|
|
163
|
+
issues = []
|
|
164
|
+
for statute in ast.statutes:
|
|
165
|
+
if not statute.penalty:
|
|
166
|
+
loc = statute.source_location
|
|
167
|
+
issues.append(LintIssue(
|
|
168
|
+
rule=self.id,
|
|
169
|
+
severity=self.severity,
|
|
170
|
+
message=f"Statute {statute.section_number} has no penalty defined",
|
|
171
|
+
line=loc.line if loc else None,
|
|
172
|
+
))
|
|
173
|
+
return issues
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class EmptyIllustrationRule(LintRule):
|
|
177
|
+
"""Check for empty illustrations."""
|
|
178
|
+
|
|
179
|
+
id = "empty-illustration"
|
|
180
|
+
severity = Severity.WARNING
|
|
181
|
+
description = "Illustrations should have meaningful content"
|
|
182
|
+
|
|
183
|
+
def check(self, ast: ModuleNode, source: str) -> List[LintIssue]:
|
|
184
|
+
issues = []
|
|
185
|
+
for statute in ast.statutes:
|
|
186
|
+
for illus in statute.illustrations:
|
|
187
|
+
if not illus.description.value.strip():
|
|
188
|
+
loc = illus.source_location
|
|
189
|
+
issues.append(LintIssue(
|
|
190
|
+
rule=self.id,
|
|
191
|
+
severity=self.severity,
|
|
192
|
+
message=f"Empty illustration in statute {statute.section_number}",
|
|
193
|
+
line=loc.line if loc else None,
|
|
194
|
+
))
|
|
195
|
+
return issues
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class UnusedDefinitionRule(LintRule):
|
|
199
|
+
"""Check for definitions that are never referenced."""
|
|
200
|
+
|
|
201
|
+
id = "unused-definition"
|
|
202
|
+
severity = Severity.HINT
|
|
203
|
+
description = "Definition is never referenced"
|
|
204
|
+
|
|
205
|
+
def check(self, ast: ModuleNode, source: str) -> List[LintIssue]:
|
|
206
|
+
issues = []
|
|
207
|
+
|
|
208
|
+
for statute in ast.statutes:
|
|
209
|
+
defined_terms = {d.term.lower() for d in statute.definitions}
|
|
210
|
+
|
|
211
|
+
# Collect all text content to search for term usage
|
|
212
|
+
text_content = ""
|
|
213
|
+
for elem in statute.elements:
|
|
214
|
+
if isinstance(elem.description, StringLit):
|
|
215
|
+
text_content += " " + elem.description.value
|
|
216
|
+
for illus in statute.illustrations:
|
|
217
|
+
text_content += " " + illus.description.value
|
|
218
|
+
|
|
219
|
+
text_lower = text_content.lower()
|
|
220
|
+
|
|
221
|
+
for defn in statute.definitions:
|
|
222
|
+
term = defn.term
|
|
223
|
+
# Check if term is used in element descriptions or illustrations
|
|
224
|
+
if term.lower() not in text_lower:
|
|
225
|
+
loc = defn.source_location
|
|
226
|
+
issues.append(LintIssue(
|
|
227
|
+
rule=self.id,
|
|
228
|
+
severity=self.severity,
|
|
229
|
+
message=f"Definition '{term}' is never referenced in statute {statute.section_number}",
|
|
230
|
+
line=loc.line if loc else None,
|
|
231
|
+
))
|
|
232
|
+
|
|
233
|
+
return issues
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class NamingConventionRule(LintRule):
|
|
237
|
+
"""Check naming conventions."""
|
|
238
|
+
|
|
239
|
+
id = "naming-convention"
|
|
240
|
+
severity = Severity.INFO
|
|
241
|
+
description = "Follow naming conventions"
|
|
242
|
+
|
|
243
|
+
def check(self, ast: ModuleNode, source: str) -> List[LintIssue]:
|
|
244
|
+
issues = []
|
|
245
|
+
|
|
246
|
+
# Check struct names (should be PascalCase)
|
|
247
|
+
for struct in ast.type_defs:
|
|
248
|
+
if not re.match(r'^[A-Z][a-zA-Z0-9]*$', struct.name):
|
|
249
|
+
loc = struct.source_location
|
|
250
|
+
issues.append(LintIssue(
|
|
251
|
+
rule=self.id,
|
|
252
|
+
severity=self.severity,
|
|
253
|
+
message=f"Type '{struct.name}' should use PascalCase",
|
|
254
|
+
line=loc.line if loc else None,
|
|
255
|
+
suggestion=f"Rename to '{struct.name.title().replace('_', '')}'"
|
|
256
|
+
))
|
|
257
|
+
|
|
258
|
+
# Check function names (should be snake_case)
|
|
259
|
+
for func in ast.function_defs:
|
|
260
|
+
if not re.match(r'^[a-z][a-z0-9_]*$', func.name):
|
|
261
|
+
loc = func.source_location
|
|
262
|
+
issues.append(LintIssue(
|
|
263
|
+
rule=self.id,
|
|
264
|
+
severity=self.severity,
|
|
265
|
+
message=f"Function '{func.name}' should use snake_case",
|
|
266
|
+
line=loc.line if loc else None,
|
|
267
|
+
))
|
|
268
|
+
|
|
269
|
+
return issues
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class SectionNumberFormatRule(LintRule):
|
|
273
|
+
"""Check section number format."""
|
|
274
|
+
|
|
275
|
+
id = "section-format"
|
|
276
|
+
severity = Severity.WARNING
|
|
277
|
+
description = "Section numbers should follow standard format"
|
|
278
|
+
|
|
279
|
+
def check(self, ast: ModuleNode, source: str) -> List[LintIssue]:
|
|
280
|
+
issues = []
|
|
281
|
+
|
|
282
|
+
for statute in ast.statutes:
|
|
283
|
+
# Allow digits with optional letter suffix (e.g., "299", "300A")
|
|
284
|
+
if not re.match(r'^\d+[A-Za-z]?$', statute.section_number):
|
|
285
|
+
loc = statute.source_location
|
|
286
|
+
issues.append(LintIssue(
|
|
287
|
+
rule=self.id,
|
|
288
|
+
severity=self.severity,
|
|
289
|
+
message=f"Non-standard section number format: '{statute.section_number}'",
|
|
290
|
+
line=loc.line if loc else None,
|
|
291
|
+
suggestion="Use numeric format with optional letter suffix (e.g., '299', '300A')"
|
|
292
|
+
))
|
|
293
|
+
|
|
294
|
+
return issues
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class DuplicateSectionRule(LintRule):
|
|
298
|
+
"""Check for duplicate section numbers."""
|
|
299
|
+
|
|
300
|
+
id = "duplicate-section"
|
|
301
|
+
severity = Severity.ERROR
|
|
302
|
+
description = "Section numbers must be unique"
|
|
303
|
+
|
|
304
|
+
def check(self, ast: ModuleNode, source: str) -> List[LintIssue]:
|
|
305
|
+
issues = []
|
|
306
|
+
seen: Dict[str, int] = {}
|
|
307
|
+
|
|
308
|
+
for statute in ast.statutes:
|
|
309
|
+
section = statute.section_number
|
|
310
|
+
if section in seen:
|
|
311
|
+
loc = statute.source_location
|
|
312
|
+
issues.append(LintIssue(
|
|
313
|
+
rule=self.id,
|
|
314
|
+
severity=self.severity,
|
|
315
|
+
message=f"Duplicate section number: '{section}'",
|
|
316
|
+
line=loc.line if loc else None,
|
|
317
|
+
))
|
|
318
|
+
seen[section] = seen.get(section, 0) + 1
|
|
319
|
+
|
|
320
|
+
return issues
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# All available lint rules
|
|
324
|
+
ALL_RULES: List[LintRule] = [
|
|
325
|
+
MissingStatuteTitleRule(),
|
|
326
|
+
MissingElementsRule(),
|
|
327
|
+
MissingActusReusRule(),
|
|
328
|
+
MissingMensReaRule(),
|
|
329
|
+
MissingPenaltyRule(),
|
|
330
|
+
EmptyIllustrationRule(),
|
|
331
|
+
UnusedDefinitionRule(),
|
|
332
|
+
NamingConventionRule(),
|
|
333
|
+
SectionNumberFormatRule(),
|
|
334
|
+
DuplicateSectionRule(),
|
|
335
|
+
]
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def format_issues(
|
|
339
|
+
issues: List[LintIssue],
|
|
340
|
+
filename: str,
|
|
341
|
+
color: bool = True
|
|
342
|
+
) -> str:
|
|
343
|
+
"""Format lint issues for display."""
|
|
344
|
+
if not issues:
|
|
345
|
+
return ""
|
|
346
|
+
|
|
347
|
+
lines = []
|
|
348
|
+
|
|
349
|
+
# Group by severity
|
|
350
|
+
errors = [i for i in issues if i.severity == Severity.ERROR]
|
|
351
|
+
warnings = [i for i in issues if i.severity == Severity.WARNING]
|
|
352
|
+
infos = [i for i in issues if i.severity == Severity.INFO]
|
|
353
|
+
hints = [i for i in issues if i.severity == Severity.HINT]
|
|
354
|
+
|
|
355
|
+
def c(text: str, col: str) -> str:
|
|
356
|
+
return colorize(text, col) if color else text
|
|
357
|
+
|
|
358
|
+
def format_issue(issue: LintIssue, prefix: str, col: str) -> str:
|
|
359
|
+
loc = f":{issue.line}" if issue.line else ""
|
|
360
|
+
msg = f"{filename}{loc}: {c(prefix, col)} [{issue.rule}] {issue.message}"
|
|
361
|
+
if issue.suggestion:
|
|
362
|
+
msg += f"\n → {issue.suggestion}"
|
|
363
|
+
return msg
|
|
364
|
+
|
|
365
|
+
for issue in errors:
|
|
366
|
+
lines.append(format_issue(issue, "error", Colors.RED))
|
|
367
|
+
|
|
368
|
+
for issue in warnings:
|
|
369
|
+
lines.append(format_issue(issue, "warning", Colors.YELLOW))
|
|
370
|
+
|
|
371
|
+
for issue in infos:
|
|
372
|
+
lines.append(format_issue(issue, "info", Colors.CYAN))
|
|
373
|
+
|
|
374
|
+
for issue in hints:
|
|
375
|
+
lines.append(format_issue(issue, "hint", Colors.DIM))
|
|
376
|
+
|
|
377
|
+
return "\n".join(lines)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def run_lint(
|
|
381
|
+
files: List[str],
|
|
382
|
+
rules: Optional[List[str]] = None,
|
|
383
|
+
exclude_rules: Optional[List[str]] = None,
|
|
384
|
+
json_output: bool = False,
|
|
385
|
+
verbose: bool = False,
|
|
386
|
+
color: bool = True,
|
|
387
|
+
fix: bool = False,
|
|
388
|
+
) -> None:
|
|
389
|
+
"""
|
|
390
|
+
Run lint checks on Yuho files.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
files: List of file paths to lint
|
|
394
|
+
rules: Specific rules to run (None = all)
|
|
395
|
+
exclude_rules: Rules to exclude
|
|
396
|
+
json_output: Output as JSON
|
|
397
|
+
verbose: Enable verbose output
|
|
398
|
+
color: Use colored output
|
|
399
|
+
fix: Attempt to auto-fix issues (not yet implemented)
|
|
400
|
+
"""
|
|
401
|
+
import json as json_module
|
|
402
|
+
|
|
403
|
+
# Select rules to run
|
|
404
|
+
active_rules = ALL_RULES
|
|
405
|
+
|
|
406
|
+
if rules:
|
|
407
|
+
active_rules = [r for r in ALL_RULES if r.id in rules]
|
|
408
|
+
|
|
409
|
+
if exclude_rules:
|
|
410
|
+
active_rules = [r for r in active_rules if r.id not in exclude_rules]
|
|
411
|
+
|
|
412
|
+
if verbose:
|
|
413
|
+
click.echo(f"Running {len(active_rules)} lint rules")
|
|
414
|
+
|
|
415
|
+
all_issues: Dict[str, List[LintIssue]] = {}
|
|
416
|
+
parser = Parser()
|
|
417
|
+
|
|
418
|
+
for file_path in files:
|
|
419
|
+
path = Path(file_path)
|
|
420
|
+
|
|
421
|
+
if not path.exists():
|
|
422
|
+
click.echo(colorize(f"error: File not found: {file_path}", Colors.RED), err=True)
|
|
423
|
+
continue
|
|
424
|
+
|
|
425
|
+
# Parse file
|
|
426
|
+
try:
|
|
427
|
+
source = path.read_text(encoding="utf-8")
|
|
428
|
+
result = parser.parse_file(path)
|
|
429
|
+
|
|
430
|
+
if result.errors:
|
|
431
|
+
# Skip files with parse errors
|
|
432
|
+
if verbose:
|
|
433
|
+
click.echo(f"Skipping {path} (parse errors)")
|
|
434
|
+
continue
|
|
435
|
+
|
|
436
|
+
builder = ASTBuilder()
|
|
437
|
+
ast = builder.build(result.tree)
|
|
438
|
+
|
|
439
|
+
# Run all rules
|
|
440
|
+
file_issues: List[LintIssue] = []
|
|
441
|
+
for rule in active_rules:
|
|
442
|
+
file_issues.extend(rule.check(ast, source))
|
|
443
|
+
|
|
444
|
+
if file_issues:
|
|
445
|
+
all_issues[str(path)] = file_issues
|
|
446
|
+
|
|
447
|
+
except Exception as e:
|
|
448
|
+
if verbose:
|
|
449
|
+
click.echo(f"Error processing {path}: {e}", err=True)
|
|
450
|
+
|
|
451
|
+
# Output results
|
|
452
|
+
if json_output:
|
|
453
|
+
output = {
|
|
454
|
+
"files": len(files),
|
|
455
|
+
"issues": {
|
|
456
|
+
path: [
|
|
457
|
+
{
|
|
458
|
+
"rule": i.rule,
|
|
459
|
+
"severity": i.severity.name.lower(),
|
|
460
|
+
"message": i.message,
|
|
461
|
+
"line": i.line,
|
|
462
|
+
"suggestion": i.suggestion,
|
|
463
|
+
}
|
|
464
|
+
for i in issues
|
|
465
|
+
]
|
|
466
|
+
for path, issues in all_issues.items()
|
|
467
|
+
},
|
|
468
|
+
"summary": {
|
|
469
|
+
"errors": sum(1 for issues in all_issues.values() for i in issues if i.severity == Severity.ERROR),
|
|
470
|
+
"warnings": sum(1 for issues in all_issues.values() for i in issues if i.severity == Severity.WARNING),
|
|
471
|
+
"infos": sum(1 for issues in all_issues.values() for i in issues if i.severity == Severity.INFO),
|
|
472
|
+
"hints": sum(1 for issues in all_issues.values() for i in issues if i.severity == Severity.HINT),
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
print(json_module.dumps(output, indent=2))
|
|
476
|
+
else:
|
|
477
|
+
if not all_issues:
|
|
478
|
+
click.echo(colorize("✓ No issues found", Colors.GREEN))
|
|
479
|
+
return
|
|
480
|
+
|
|
481
|
+
for filename, issues in all_issues.items():
|
|
482
|
+
output = format_issues(issues, filename, color=color)
|
|
483
|
+
click.echo(output)
|
|
484
|
+
click.echo()
|
|
485
|
+
|
|
486
|
+
# Summary
|
|
487
|
+
total_errors = sum(1 for issues in all_issues.values() for i in issues if i.severity == Severity.ERROR)
|
|
488
|
+
total_warnings = sum(1 for issues in all_issues.values() for i in issues if i.severity == Severity.WARNING)
|
|
489
|
+
total_other = sum(1 for issues in all_issues.values() for i in issues if i.severity not in (Severity.ERROR, Severity.WARNING))
|
|
490
|
+
|
|
491
|
+
summary_parts = []
|
|
492
|
+
if total_errors:
|
|
493
|
+
summary_parts.append(colorize(f"{total_errors} error(s)", Colors.RED) if color else f"{total_errors} error(s)")
|
|
494
|
+
if total_warnings:
|
|
495
|
+
summary_parts.append(colorize(f"{total_warnings} warning(s)", Colors.YELLOW) if color else f"{total_warnings} warning(s)")
|
|
496
|
+
if total_other:
|
|
497
|
+
summary_parts.append(f"{total_other} info/hint(s)")
|
|
498
|
+
|
|
499
|
+
click.echo(f"Found {', '.join(summary_parts)}")
|
|
500
|
+
|
|
501
|
+
# Exit with error if there are errors
|
|
502
|
+
if any(i.severity == Severity.ERROR for issues in all_issues.values() for i in issues):
|
|
503
|
+
sys.exit(1)
|
yuho/cli/commands/lsp.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LSP command - start Language Server Protocol server.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from yuho.cli.error_formatter import Colors, colorize
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run_lsp(tcp: Optional[int] = None, verbose: bool = False) -> None:
|
|
14
|
+
"""
|
|
15
|
+
Start the Language Server Protocol server.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
tcp: TCP port to listen on (default: stdio)
|
|
19
|
+
verbose: Enable verbose output
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
from yuho.lsp import YuhoLanguageServer
|
|
23
|
+
except ImportError:
|
|
24
|
+
click.echo(colorize("error: LSP module not available. Install with: pip install yuho[lsp]", Colors.RED), err=True)
|
|
25
|
+
sys.exit(1)
|
|
26
|
+
|
|
27
|
+
server = YuhoLanguageServer()
|
|
28
|
+
|
|
29
|
+
if tcp:
|
|
30
|
+
if verbose:
|
|
31
|
+
click.echo(f"Starting LSP server on TCP port {tcp}...")
|
|
32
|
+
server.start_tcp("127.0.0.1", tcp)
|
|
33
|
+
else:
|
|
34
|
+
if verbose:
|
|
35
|
+
click.echo("Starting LSP server on stdio...", err=True)
|
|
36
|
+
server.start_io()
|