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.
Files changed (91) hide show
  1. yuho/__init__.py +16 -0
  2. yuho/ast/__init__.py +196 -0
  3. yuho/ast/builder.py +926 -0
  4. yuho/ast/constant_folder.py +280 -0
  5. yuho/ast/dead_code.py +199 -0
  6. yuho/ast/exhaustiveness.py +503 -0
  7. yuho/ast/nodes.py +907 -0
  8. yuho/ast/overlap.py +291 -0
  9. yuho/ast/reachability.py +293 -0
  10. yuho/ast/scope_analysis.py +490 -0
  11. yuho/ast/transformer.py +490 -0
  12. yuho/ast/type_check.py +471 -0
  13. yuho/ast/type_inference.py +425 -0
  14. yuho/ast/visitor.py +239 -0
  15. yuho/cli/__init__.py +14 -0
  16. yuho/cli/commands/__init__.py +1 -0
  17. yuho/cli/commands/api.py +431 -0
  18. yuho/cli/commands/ast_viz.py +334 -0
  19. yuho/cli/commands/check.py +218 -0
  20. yuho/cli/commands/config.py +311 -0
  21. yuho/cli/commands/contribute.py +122 -0
  22. yuho/cli/commands/diff.py +487 -0
  23. yuho/cli/commands/explain.py +240 -0
  24. yuho/cli/commands/fmt.py +253 -0
  25. yuho/cli/commands/generate.py +316 -0
  26. yuho/cli/commands/graph.py +410 -0
  27. yuho/cli/commands/init.py +120 -0
  28. yuho/cli/commands/library.py +656 -0
  29. yuho/cli/commands/lint.py +503 -0
  30. yuho/cli/commands/lsp.py +36 -0
  31. yuho/cli/commands/preview.py +377 -0
  32. yuho/cli/commands/repl.py +444 -0
  33. yuho/cli/commands/serve.py +44 -0
  34. yuho/cli/commands/test.py +528 -0
  35. yuho/cli/commands/transpile.py +121 -0
  36. yuho/cli/commands/wizard.py +370 -0
  37. yuho/cli/completions.py +182 -0
  38. yuho/cli/error_formatter.py +193 -0
  39. yuho/cli/main.py +1064 -0
  40. yuho/config/__init__.py +46 -0
  41. yuho/config/loader.py +235 -0
  42. yuho/config/mask.py +194 -0
  43. yuho/config/schema.py +147 -0
  44. yuho/library/__init__.py +84 -0
  45. yuho/library/index.py +328 -0
  46. yuho/library/install.py +699 -0
  47. yuho/library/lockfile.py +330 -0
  48. yuho/library/package.py +421 -0
  49. yuho/library/resolver.py +791 -0
  50. yuho/library/signature.py +335 -0
  51. yuho/llm/__init__.py +45 -0
  52. yuho/llm/config.py +75 -0
  53. yuho/llm/factory.py +123 -0
  54. yuho/llm/prompts.py +146 -0
  55. yuho/llm/providers.py +383 -0
  56. yuho/llm/utils.py +470 -0
  57. yuho/lsp/__init__.py +14 -0
  58. yuho/lsp/code_action_handler.py +518 -0
  59. yuho/lsp/completion_handler.py +85 -0
  60. yuho/lsp/diagnostics.py +100 -0
  61. yuho/lsp/hover_handler.py +130 -0
  62. yuho/lsp/server.py +1425 -0
  63. yuho/mcp/__init__.py +10 -0
  64. yuho/mcp/server.py +1452 -0
  65. yuho/parser/__init__.py +8 -0
  66. yuho/parser/source_location.py +108 -0
  67. yuho/parser/wrapper.py +311 -0
  68. yuho/testing/__init__.py +48 -0
  69. yuho/testing/coverage.py +274 -0
  70. yuho/testing/fixtures.py +263 -0
  71. yuho/transpile/__init__.py +52 -0
  72. yuho/transpile/alloy_transpiler.py +546 -0
  73. yuho/transpile/base.py +100 -0
  74. yuho/transpile/blocks_transpiler.py +338 -0
  75. yuho/transpile/english_transpiler.py +470 -0
  76. yuho/transpile/graphql_transpiler.py +404 -0
  77. yuho/transpile/json_transpiler.py +217 -0
  78. yuho/transpile/jsonld_transpiler.py +250 -0
  79. yuho/transpile/latex_preamble.py +161 -0
  80. yuho/transpile/latex_transpiler.py +406 -0
  81. yuho/transpile/latex_utils.py +206 -0
  82. yuho/transpile/mermaid_transpiler.py +357 -0
  83. yuho/transpile/registry.py +275 -0
  84. yuho/verify/__init__.py +43 -0
  85. yuho/verify/alloy.py +352 -0
  86. yuho/verify/combined.py +218 -0
  87. yuho/verify/z3_solver.py +1155 -0
  88. yuho-5.0.0.dist-info/METADATA +186 -0
  89. yuho-5.0.0.dist-info/RECORD +91 -0
  90. yuho-5.0.0.dist-info/WHEEL +4 -0
  91. 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)
@@ -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()