machine-dialect 0.1.0a1__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.
- machine_dialect/__main__.py +667 -0
- machine_dialect/agent/__init__.py +5 -0
- machine_dialect/agent/agent.py +360 -0
- machine_dialect/ast/__init__.py +95 -0
- machine_dialect/ast/ast_node.py +35 -0
- machine_dialect/ast/call_expression.py +82 -0
- machine_dialect/ast/dict_extraction.py +60 -0
- machine_dialect/ast/expressions.py +439 -0
- machine_dialect/ast/literals.py +309 -0
- machine_dialect/ast/program.py +35 -0
- machine_dialect/ast/statements.py +1433 -0
- machine_dialect/ast/tests/test_ast_string_representation.py +62 -0
- machine_dialect/ast/tests/test_boolean_literal.py +29 -0
- machine_dialect/ast/tests/test_collection_hir.py +138 -0
- machine_dialect/ast/tests/test_define_statement.py +142 -0
- machine_dialect/ast/tests/test_desugar.py +541 -0
- machine_dialect/ast/tests/test_foreach_desugar.py +245 -0
- machine_dialect/cfg/__init__.py +6 -0
- machine_dialect/cfg/config.py +156 -0
- machine_dialect/cfg/examples.py +221 -0
- machine_dialect/cfg/generate_with_ai.py +187 -0
- machine_dialect/cfg/openai_generation.py +200 -0
- machine_dialect/cfg/parser.py +94 -0
- machine_dialect/cfg/tests/__init__.py +1 -0
- machine_dialect/cfg/tests/test_cfg_parser.py +252 -0
- machine_dialect/cfg/tests/test_config.py +188 -0
- machine_dialect/cfg/tests/test_examples.py +391 -0
- machine_dialect/cfg/tests/test_generate_with_ai.py +354 -0
- machine_dialect/cfg/tests/test_openai_generation.py +256 -0
- machine_dialect/codegen/__init__.py +5 -0
- machine_dialect/codegen/bytecode_module.py +89 -0
- machine_dialect/codegen/bytecode_serializer.py +300 -0
- machine_dialect/codegen/opcodes.py +101 -0
- machine_dialect/codegen/register_codegen.py +1996 -0
- machine_dialect/codegen/symtab.py +208 -0
- machine_dialect/codegen/tests/__init__.py +1 -0
- machine_dialect/codegen/tests/test_array_operations_codegen.py +295 -0
- machine_dialect/codegen/tests/test_bytecode_serializer.py +185 -0
- machine_dialect/codegen/tests/test_register_codegen_ssa.py +324 -0
- machine_dialect/codegen/tests/test_symtab.py +418 -0
- machine_dialect/codegen/vm_serializer.py +621 -0
- machine_dialect/compiler/__init__.py +18 -0
- machine_dialect/compiler/compiler.py +197 -0
- machine_dialect/compiler/config.py +149 -0
- machine_dialect/compiler/context.py +149 -0
- machine_dialect/compiler/phases/__init__.py +19 -0
- machine_dialect/compiler/phases/bytecode_optimization.py +90 -0
- machine_dialect/compiler/phases/codegen.py +40 -0
- machine_dialect/compiler/phases/hir_generation.py +39 -0
- machine_dialect/compiler/phases/mir_generation.py +86 -0
- machine_dialect/compiler/phases/optimization.py +110 -0
- machine_dialect/compiler/phases/parsing.py +39 -0
- machine_dialect/compiler/pipeline.py +143 -0
- machine_dialect/compiler/tests/__init__.py +1 -0
- machine_dialect/compiler/tests/test_compiler.py +568 -0
- machine_dialect/compiler/vm_runner.py +173 -0
- machine_dialect/errors/__init__.py +32 -0
- machine_dialect/errors/exceptions.py +369 -0
- machine_dialect/errors/messages.py +82 -0
- machine_dialect/errors/tests/__init__.py +0 -0
- machine_dialect/errors/tests/test_expected_token_errors.py +188 -0
- machine_dialect/errors/tests/test_name_errors.py +118 -0
- machine_dialect/helpers/__init__.py +0 -0
- machine_dialect/helpers/stopwords.py +225 -0
- machine_dialect/helpers/validators.py +30 -0
- machine_dialect/lexer/__init__.py +9 -0
- machine_dialect/lexer/constants.py +23 -0
- machine_dialect/lexer/lexer.py +907 -0
- machine_dialect/lexer/tests/__init__.py +0 -0
- machine_dialect/lexer/tests/helpers.py +86 -0
- machine_dialect/lexer/tests/test_apostrophe_identifiers.py +122 -0
- machine_dialect/lexer/tests/test_backtick_identifiers.py +140 -0
- machine_dialect/lexer/tests/test_boolean_literals.py +108 -0
- machine_dialect/lexer/tests/test_case_insensitive_keywords.py +188 -0
- machine_dialect/lexer/tests/test_comments.py +200 -0
- machine_dialect/lexer/tests/test_double_asterisk_keywords.py +127 -0
- machine_dialect/lexer/tests/test_lexer_position.py +113 -0
- machine_dialect/lexer/tests/test_list_tokens.py +282 -0
- machine_dialect/lexer/tests/test_stopwords.py +80 -0
- machine_dialect/lexer/tests/test_strict_equality.py +129 -0
- machine_dialect/lexer/tests/test_token.py +41 -0
- machine_dialect/lexer/tests/test_tokenization.py +294 -0
- machine_dialect/lexer/tests/test_underscore_literals.py +343 -0
- machine_dialect/lexer/tests/test_url_literals.py +169 -0
- machine_dialect/lexer/tokens.py +487 -0
- machine_dialect/linter/__init__.py +10 -0
- machine_dialect/linter/__main__.py +144 -0
- machine_dialect/linter/linter.py +154 -0
- machine_dialect/linter/rules/__init__.py +8 -0
- machine_dialect/linter/rules/base.py +112 -0
- machine_dialect/linter/rules/statement_termination.py +99 -0
- machine_dialect/linter/tests/__init__.py +1 -0
- machine_dialect/linter/tests/mdrules/__init__.py +0 -0
- machine_dialect/linter/tests/mdrules/test_md101_statement_termination.py +181 -0
- machine_dialect/linter/tests/test_linter.py +81 -0
- machine_dialect/linter/tests/test_rules.py +110 -0
- machine_dialect/linter/tests/test_violations.py +71 -0
- machine_dialect/linter/violations.py +51 -0
- machine_dialect/mir/__init__.py +69 -0
- machine_dialect/mir/analyses/__init__.py +20 -0
- machine_dialect/mir/analyses/alias_analysis.py +315 -0
- machine_dialect/mir/analyses/dominance_analysis.py +49 -0
- machine_dialect/mir/analyses/escape_analysis.py +286 -0
- machine_dialect/mir/analyses/loop_analysis.py +272 -0
- machine_dialect/mir/analyses/tests/test_type_analysis.py +736 -0
- machine_dialect/mir/analyses/type_analysis.py +448 -0
- machine_dialect/mir/analyses/use_def_chains.py +232 -0
- machine_dialect/mir/basic_block.py +385 -0
- machine_dialect/mir/dataflow.py +445 -0
- machine_dialect/mir/debug_info.py +208 -0
- machine_dialect/mir/hir_to_mir.py +1738 -0
- machine_dialect/mir/mir_dumper.py +366 -0
- machine_dialect/mir/mir_function.py +167 -0
- machine_dialect/mir/mir_instructions.py +1877 -0
- machine_dialect/mir/mir_interpreter.py +556 -0
- machine_dialect/mir/mir_module.py +225 -0
- machine_dialect/mir/mir_printer.py +480 -0
- machine_dialect/mir/mir_transformer.py +410 -0
- machine_dialect/mir/mir_types.py +367 -0
- machine_dialect/mir/mir_validation.py +455 -0
- machine_dialect/mir/mir_values.py +268 -0
- machine_dialect/mir/optimization_config.py +233 -0
- machine_dialect/mir/optimization_pass.py +251 -0
- machine_dialect/mir/optimization_pipeline.py +355 -0
- machine_dialect/mir/optimizations/__init__.py +84 -0
- machine_dialect/mir/optimizations/algebraic_simplification.py +733 -0
- machine_dialect/mir/optimizations/branch_prediction.py +372 -0
- machine_dialect/mir/optimizations/constant_propagation.py +634 -0
- machine_dialect/mir/optimizations/cse.py +398 -0
- machine_dialect/mir/optimizations/dce.py +288 -0
- machine_dialect/mir/optimizations/inlining.py +551 -0
- machine_dialect/mir/optimizations/jump_threading.py +487 -0
- machine_dialect/mir/optimizations/licm.py +405 -0
- machine_dialect/mir/optimizations/loop_unrolling.py +366 -0
- machine_dialect/mir/optimizations/strength_reduction.py +422 -0
- machine_dialect/mir/optimizations/tail_call.py +207 -0
- machine_dialect/mir/optimizations/tests/test_loop_unrolling.py +483 -0
- machine_dialect/mir/optimizations/type_narrowing.py +397 -0
- machine_dialect/mir/optimizations/type_specialization.py +447 -0
- machine_dialect/mir/optimizations/type_specific.py +906 -0
- machine_dialect/mir/optimize_mir.py +89 -0
- machine_dialect/mir/pass_manager.py +391 -0
- machine_dialect/mir/profiling/__init__.py +26 -0
- machine_dialect/mir/profiling/profile_collector.py +318 -0
- machine_dialect/mir/profiling/profile_data.py +372 -0
- machine_dialect/mir/profiling/profile_reader.py +272 -0
- machine_dialect/mir/profiling/profile_writer.py +226 -0
- machine_dialect/mir/register_allocation.py +302 -0
- machine_dialect/mir/reporting/__init__.py +17 -0
- machine_dialect/mir/reporting/optimization_reporter.py +314 -0
- machine_dialect/mir/reporting/report_formatter.py +289 -0
- machine_dialect/mir/ssa_construction.py +342 -0
- machine_dialect/mir/tests/__init__.py +1 -0
- machine_dialect/mir/tests/test_algebraic_associativity.py +204 -0
- machine_dialect/mir/tests/test_algebraic_complex_patterns.py +221 -0
- machine_dialect/mir/tests/test_algebraic_division.py +126 -0
- machine_dialect/mir/tests/test_algebraic_simplification.py +863 -0
- machine_dialect/mir/tests/test_basic_block.py +425 -0
- machine_dialect/mir/tests/test_branch_prediction.py +459 -0
- machine_dialect/mir/tests/test_call_lowering.py +168 -0
- machine_dialect/mir/tests/test_collection_lowering.py +604 -0
- machine_dialect/mir/tests/test_cross_block_constant_propagation.py +255 -0
- machine_dialect/mir/tests/test_custom_passes.py +166 -0
- machine_dialect/mir/tests/test_debug_info.py +285 -0
- machine_dialect/mir/tests/test_dict_extraction_lowering.py +192 -0
- machine_dialect/mir/tests/test_dictionary_lowering.py +299 -0
- machine_dialect/mir/tests/test_double_negation.py +231 -0
- machine_dialect/mir/tests/test_escape_analysis.py +233 -0
- machine_dialect/mir/tests/test_hir_to_mir.py +465 -0
- machine_dialect/mir/tests/test_hir_to_mir_complete.py +389 -0
- machine_dialect/mir/tests/test_hir_to_mir_simple.py +130 -0
- machine_dialect/mir/tests/test_inlining.py +435 -0
- machine_dialect/mir/tests/test_licm.py +472 -0
- machine_dialect/mir/tests/test_mir_dumper.py +313 -0
- machine_dialect/mir/tests/test_mir_instructions.py +445 -0
- machine_dialect/mir/tests/test_mir_module.py +860 -0
- machine_dialect/mir/tests/test_mir_printer.py +387 -0
- machine_dialect/mir/tests/test_mir_types.py +123 -0
- machine_dialect/mir/tests/test_mir_types_enhanced.py +132 -0
- machine_dialect/mir/tests/test_mir_validation.py +378 -0
- machine_dialect/mir/tests/test_mir_values.py +168 -0
- machine_dialect/mir/tests/test_one_based_indexing.py +202 -0
- machine_dialect/mir/tests/test_optimization_helpers.py +60 -0
- machine_dialect/mir/tests/test_optimization_pipeline.py +554 -0
- machine_dialect/mir/tests/test_optimization_reporter.py +318 -0
- machine_dialect/mir/tests/test_pass_manager.py +294 -0
- machine_dialect/mir/tests/test_pass_registration.py +64 -0
- machine_dialect/mir/tests/test_profiling.py +356 -0
- machine_dialect/mir/tests/test_register_allocation.py +307 -0
- machine_dialect/mir/tests/test_report_formatters.py +372 -0
- machine_dialect/mir/tests/test_ssa_construction.py +433 -0
- machine_dialect/mir/tests/test_tail_call.py +236 -0
- machine_dialect/mir/tests/test_type_annotated_instructions.py +192 -0
- machine_dialect/mir/tests/test_type_narrowing.py +277 -0
- machine_dialect/mir/tests/test_type_specialization.py +421 -0
- machine_dialect/mir/tests/test_type_specific_optimization.py +545 -0
- machine_dialect/mir/tests/test_type_specific_optimization_advanced.py +382 -0
- machine_dialect/mir/type_inference.py +368 -0
- machine_dialect/parser/__init__.py +12 -0
- machine_dialect/parser/enums.py +45 -0
- machine_dialect/parser/parser.py +3655 -0
- machine_dialect/parser/protocols.py +11 -0
- machine_dialect/parser/symbol_table.py +169 -0
- machine_dialect/parser/tests/__init__.py +0 -0
- machine_dialect/parser/tests/helper_functions.py +193 -0
- machine_dialect/parser/tests/test_action_statements.py +334 -0
- machine_dialect/parser/tests/test_boolean_literal_expressions.py +152 -0
- machine_dialect/parser/tests/test_call_statements.py +154 -0
- machine_dialect/parser/tests/test_call_statements_errors.py +187 -0
- machine_dialect/parser/tests/test_collection_mutations.py +264 -0
- machine_dialect/parser/tests/test_conditional_expressions.py +343 -0
- machine_dialect/parser/tests/test_define_integration.py +468 -0
- machine_dialect/parser/tests/test_define_statements.py +311 -0
- machine_dialect/parser/tests/test_dict_extraction.py +115 -0
- machine_dialect/parser/tests/test_empty_literal.py +155 -0
- machine_dialect/parser/tests/test_float_literal_expressions.py +163 -0
- machine_dialect/parser/tests/test_identifier_expressions.py +57 -0
- machine_dialect/parser/tests/test_if_empty_block.py +61 -0
- machine_dialect/parser/tests/test_if_statements.py +299 -0
- machine_dialect/parser/tests/test_illegal_tokens.py +86 -0
- machine_dialect/parser/tests/test_infix_expressions.py +680 -0
- machine_dialect/parser/tests/test_integer_literal_expressions.py +137 -0
- machine_dialect/parser/tests/test_interaction_statements.py +269 -0
- machine_dialect/parser/tests/test_list_literals.py +277 -0
- machine_dialect/parser/tests/test_no_none_in_ast.py +94 -0
- machine_dialect/parser/tests/test_panic_mode_recovery.py +171 -0
- machine_dialect/parser/tests/test_parse_errors.py +114 -0
- machine_dialect/parser/tests/test_possessive_syntax.py +182 -0
- machine_dialect/parser/tests/test_prefix_expressions.py +415 -0
- machine_dialect/parser/tests/test_program.py +13 -0
- machine_dialect/parser/tests/test_return_statements.py +89 -0
- machine_dialect/parser/tests/test_set_statements.py +152 -0
- machine_dialect/parser/tests/test_strict_equality.py +258 -0
- machine_dialect/parser/tests/test_symbol_table.py +217 -0
- machine_dialect/parser/tests/test_url_literal_expressions.py +209 -0
- machine_dialect/parser/tests/test_utility_statements.py +423 -0
- machine_dialect/parser/token_buffer.py +159 -0
- machine_dialect/repl/__init__.py +3 -0
- machine_dialect/repl/repl.py +426 -0
- machine_dialect/repl/tests/__init__.py +0 -0
- machine_dialect/repl/tests/test_repl.py +606 -0
- machine_dialect/semantic/__init__.py +12 -0
- machine_dialect/semantic/analyzer.py +906 -0
- machine_dialect/semantic/error_messages.py +189 -0
- machine_dialect/semantic/tests/__init__.py +1 -0
- machine_dialect/semantic/tests/test_analyzer.py +364 -0
- machine_dialect/semantic/tests/test_error_messages.py +104 -0
- machine_dialect/tests/edge_cases/__init__.py +10 -0
- machine_dialect/tests/edge_cases/test_boundary_access.py +256 -0
- machine_dialect/tests/edge_cases/test_empty_collections.py +166 -0
- machine_dialect/tests/edge_cases/test_invalid_operations.py +243 -0
- machine_dialect/tests/edge_cases/test_named_list_edge_cases.py +295 -0
- machine_dialect/tests/edge_cases/test_nested_structures.py +313 -0
- machine_dialect/tests/edge_cases/test_type_mixing.py +277 -0
- machine_dialect/tests/integration/test_array_operations_emulation.py +248 -0
- machine_dialect/tests/integration/test_list_compilation.py +395 -0
- machine_dialect/tests/integration/test_lists_and_dictionaries.py +322 -0
- machine_dialect/type_checking/__init__.py +21 -0
- machine_dialect/type_checking/tests/__init__.py +1 -0
- machine_dialect/type_checking/tests/test_type_system.py +230 -0
- machine_dialect/type_checking/type_system.py +270 -0
- machine_dialect-0.1.0a1.dist-info/METADATA +128 -0
- machine_dialect-0.1.0a1.dist-info/RECORD +268 -0
- machine_dialect-0.1.0a1.dist-info/WHEEL +5 -0
- machine_dialect-0.1.0a1.dist-info/entry_points.txt +3 -0
- machine_dialect-0.1.0a1.dist-info/licenses/LICENSE +201 -0
- machine_dialect-0.1.0a1.dist-info/top_level.txt +2 -0
- machine_dialect_vm/__init__.pyi +15 -0
@@ -0,0 +1,144 @@
|
|
1
|
+
"""Command-line interface for the Machine Dialect™ linter.
|
2
|
+
|
3
|
+
This module provides the CLI for running the linter on Machine Dialect™ files.
|
4
|
+
"""
|
5
|
+
|
6
|
+
import argparse
|
7
|
+
import json
|
8
|
+
import sys
|
9
|
+
from pathlib import Path
|
10
|
+
from typing import Any
|
11
|
+
|
12
|
+
from machine_dialect.linter import Linter, Violation
|
13
|
+
|
14
|
+
|
15
|
+
def load_config(config_path: Path | None) -> dict[str, Any]:
|
16
|
+
"""Load linter configuration from a file.
|
17
|
+
|
18
|
+
Args:
|
19
|
+
config_path: Path to the configuration file.
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
Configuration dictionary.
|
23
|
+
"""
|
24
|
+
if not config_path or not config_path.exists():
|
25
|
+
return {}
|
26
|
+
|
27
|
+
with open(config_path) as f:
|
28
|
+
if config_path.suffix == ".json":
|
29
|
+
return json.load(f) # type: ignore[no-any-return]
|
30
|
+
else:
|
31
|
+
# For now, only support JSON
|
32
|
+
print(f"Warning: Unsupported config format {config_path.suffix}, using defaults")
|
33
|
+
return {}
|
34
|
+
|
35
|
+
|
36
|
+
def format_violation(violation: Violation, filename: str) -> str:
|
37
|
+
"""Format a violation for display.
|
38
|
+
|
39
|
+
Args:
|
40
|
+
violation: The violation to format.
|
41
|
+
filename: The filename where the violation occurred.
|
42
|
+
|
43
|
+
Returns:
|
44
|
+
A formatted string for display.
|
45
|
+
"""
|
46
|
+
return f"{filename}:{violation}"
|
47
|
+
|
48
|
+
|
49
|
+
def main() -> None:
|
50
|
+
"""Main entry point for the linter CLI."""
|
51
|
+
parser = argparse.ArgumentParser(description="Lint Machine Dialect™ code for style and errors")
|
52
|
+
|
53
|
+
parser.add_argument(
|
54
|
+
"files",
|
55
|
+
nargs="*", # Changed from "+" to "*" to make it optional
|
56
|
+
type=Path,
|
57
|
+
help="Machine Dialect™ files to lint",
|
58
|
+
)
|
59
|
+
|
60
|
+
parser.add_argument(
|
61
|
+
"-c",
|
62
|
+
"--config",
|
63
|
+
type=Path,
|
64
|
+
help="Path to configuration file",
|
65
|
+
)
|
66
|
+
|
67
|
+
parser.add_argument(
|
68
|
+
"-q",
|
69
|
+
"--quiet",
|
70
|
+
action="store_true",
|
71
|
+
help="Only show errors, not warnings",
|
72
|
+
)
|
73
|
+
|
74
|
+
parser.add_argument(
|
75
|
+
"--list-rules",
|
76
|
+
action="store_true",
|
77
|
+
help="List all available rules and exit",
|
78
|
+
)
|
79
|
+
|
80
|
+
args = parser.parse_args()
|
81
|
+
|
82
|
+
# Handle --list-rules
|
83
|
+
if args.list_rules:
|
84
|
+
linter = Linter()
|
85
|
+
print("Available linting rules:")
|
86
|
+
for rule in linter.rules:
|
87
|
+
print(f" {rule.rule_id}: {rule.description}")
|
88
|
+
sys.exit(0)
|
89
|
+
|
90
|
+
# Check if files were provided
|
91
|
+
if not args.files:
|
92
|
+
parser.error("No files specified to lint")
|
93
|
+
|
94
|
+
# Load configuration
|
95
|
+
config = load_config(args.config)
|
96
|
+
linter = Linter(config)
|
97
|
+
|
98
|
+
# Lint files
|
99
|
+
total_violations = 0
|
100
|
+
has_errors = False
|
101
|
+
|
102
|
+
for filepath in args.files:
|
103
|
+
if not filepath.exists():
|
104
|
+
print(f"Error: File not found: {filepath}", file=sys.stderr)
|
105
|
+
has_errors = True
|
106
|
+
continue
|
107
|
+
|
108
|
+
try:
|
109
|
+
violations = linter.lint_file(str(filepath))
|
110
|
+
|
111
|
+
# Filter violations if --quiet
|
112
|
+
if args.quiet:
|
113
|
+
from machine_dialect.linter.violations import ViolationSeverity
|
114
|
+
|
115
|
+
violations = [v for v in violations if v.severity == ViolationSeverity.ERROR]
|
116
|
+
|
117
|
+
# Display violations
|
118
|
+
for violation in violations:
|
119
|
+
print(format_violation(violation, str(filepath)))
|
120
|
+
if violation.fix_suggestion:
|
121
|
+
print(f" Suggestion: {violation.fix_suggestion}")
|
122
|
+
|
123
|
+
total_violations += len(violations)
|
124
|
+
|
125
|
+
# Check for errors
|
126
|
+
from machine_dialect.linter.violations import ViolationSeverity
|
127
|
+
|
128
|
+
if any(v.severity == ViolationSeverity.ERROR for v in violations):
|
129
|
+
has_errors = True
|
130
|
+
|
131
|
+
except Exception as e:
|
132
|
+
print(f"Error linting {filepath}: {e}", file=sys.stderr)
|
133
|
+
has_errors = True
|
134
|
+
|
135
|
+
# Summary
|
136
|
+
if not args.quiet and total_violations > 0:
|
137
|
+
print(f"\nFound {total_violations} issue(s)")
|
138
|
+
|
139
|
+
# Exit with error code if there were errors
|
140
|
+
sys.exit(1 if has_errors or total_violations > 0 else 0)
|
141
|
+
|
142
|
+
|
143
|
+
if __name__ == "__main__":
|
144
|
+
main()
|
@@ -0,0 +1,154 @@
|
|
1
|
+
"""Main linter class for Machine Dialect™.
|
2
|
+
|
3
|
+
This module provides the main Linter class that orchestrates the
|
4
|
+
linting process by running rules against an AST.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from typing import Any
|
8
|
+
|
9
|
+
from machine_dialect.ast import (
|
10
|
+
ASTNode,
|
11
|
+
ExpressionStatement,
|
12
|
+
PrefixExpression,
|
13
|
+
Program,
|
14
|
+
ReturnStatement,
|
15
|
+
SetStatement,
|
16
|
+
)
|
17
|
+
from machine_dialect.linter.rules import Rule
|
18
|
+
from machine_dialect.linter.rules.base import Context
|
19
|
+
from machine_dialect.linter.violations import Violation, ViolationSeverity
|
20
|
+
from machine_dialect.parser import Parser
|
21
|
+
|
22
|
+
|
23
|
+
class Linter:
|
24
|
+
"""Main linter class that runs rules against Machine Dialect™ code.
|
25
|
+
|
26
|
+
The linter uses a visitor pattern to traverse the AST and apply
|
27
|
+
registered rules to each node.
|
28
|
+
"""
|
29
|
+
|
30
|
+
def __init__(self, config: dict[str, Any] | None = None) -> None:
|
31
|
+
"""Initialize the linter with optional configuration.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
config: Configuration dictionary for the linter.
|
35
|
+
"""
|
36
|
+
self.config = config or {}
|
37
|
+
self.rules: list[Rule] = []
|
38
|
+
self._register_default_rules()
|
39
|
+
|
40
|
+
def _register_default_rules(self) -> None:
|
41
|
+
"""Register the default set of linting rules."""
|
42
|
+
# Import rules here to avoid circular imports
|
43
|
+
from machine_dialect.linter.rules.statement_termination import StatementTerminationRule
|
44
|
+
|
45
|
+
# Add default rules
|
46
|
+
self.add_rule(StatementTerminationRule())
|
47
|
+
|
48
|
+
def add_rule(self, rule: Rule) -> None:
|
49
|
+
"""Add a linting rule to the linter.
|
50
|
+
|
51
|
+
Args:
|
52
|
+
rule: The rule instance to add.
|
53
|
+
"""
|
54
|
+
if rule.is_enabled(self.config):
|
55
|
+
self.rules.append(rule)
|
56
|
+
|
57
|
+
def lint(self, source_code: str, filename: str = "<stdin>") -> list[Violation]:
|
58
|
+
"""Lint the given source code.
|
59
|
+
|
60
|
+
Args:
|
61
|
+
source_code: The Machine Dialect™ source code to lint.
|
62
|
+
filename: The filename for error reporting.
|
63
|
+
|
64
|
+
Returns:
|
65
|
+
A list of violations found in the code.
|
66
|
+
"""
|
67
|
+
# Parse the source code
|
68
|
+
parser = Parser()
|
69
|
+
program = parser.parse(source_code)
|
70
|
+
|
71
|
+
# Include parse errors as violations
|
72
|
+
violations: list[Violation] = []
|
73
|
+
for error in parser.errors:
|
74
|
+
violations.append(
|
75
|
+
Violation(
|
76
|
+
rule_id="parse-error",
|
77
|
+
message=str(error),
|
78
|
+
severity=ViolationSeverity.ERROR,
|
79
|
+
line=error._line,
|
80
|
+
column=error._column,
|
81
|
+
)
|
82
|
+
)
|
83
|
+
|
84
|
+
# If there are parse errors, don't run other rules
|
85
|
+
if violations:
|
86
|
+
return violations
|
87
|
+
|
88
|
+
# Create context
|
89
|
+
context = Context(filename, source_code)
|
90
|
+
|
91
|
+
# Visit the AST and collect violations
|
92
|
+
violations.extend(self._visit_node(program, context))
|
93
|
+
|
94
|
+
return violations
|
95
|
+
|
96
|
+
def _visit_node(self, node: ASTNode | None, context: Context) -> list[Violation]:
|
97
|
+
"""Visit an AST node and its children, applying rules.
|
98
|
+
|
99
|
+
Args:
|
100
|
+
node: The AST node to visit.
|
101
|
+
context: The linting context.
|
102
|
+
|
103
|
+
Returns:
|
104
|
+
A list of violations found.
|
105
|
+
"""
|
106
|
+
if node is None:
|
107
|
+
return []
|
108
|
+
|
109
|
+
violations: list[Violation] = []
|
110
|
+
|
111
|
+
# Apply all rules to this node
|
112
|
+
for rule in self.rules:
|
113
|
+
violations.extend(rule.check(node, context))
|
114
|
+
|
115
|
+
# Visit children based on node type
|
116
|
+
context.push_parent(node)
|
117
|
+
|
118
|
+
if isinstance(node, Program):
|
119
|
+
for statement in node.statements:
|
120
|
+
violations.extend(self._visit_node(statement, context))
|
121
|
+
|
122
|
+
elif isinstance(node, SetStatement):
|
123
|
+
violations.extend(self._visit_node(node.name, context))
|
124
|
+
violations.extend(self._visit_node(node.value, context))
|
125
|
+
|
126
|
+
elif isinstance(node, ReturnStatement):
|
127
|
+
# ReturnStatement doesn't have a value attribute yet (TODO)
|
128
|
+
# violations.extend(self._visit_node(node.value, context))
|
129
|
+
pass
|
130
|
+
|
131
|
+
elif isinstance(node, ExpressionStatement):
|
132
|
+
violations.extend(self._visit_node(node.expression, context))
|
133
|
+
|
134
|
+
elif isinstance(node, PrefixExpression):
|
135
|
+
violations.extend(self._visit_node(node.right, context))
|
136
|
+
|
137
|
+
# Literals and identifiers have no children to visit
|
138
|
+
|
139
|
+
context.pop_parent()
|
140
|
+
return violations
|
141
|
+
|
142
|
+
def lint_file(self, filepath: str) -> list[Violation]:
|
143
|
+
"""Lint a file by reading its contents and running the linter.
|
144
|
+
|
145
|
+
Args:
|
146
|
+
filepath: Path to the file to lint.
|
147
|
+
|
148
|
+
Returns:
|
149
|
+
A list of violations found in the file.
|
150
|
+
"""
|
151
|
+
with open(filepath, encoding="utf-8") as f:
|
152
|
+
source_code = f.read()
|
153
|
+
|
154
|
+
return self.lint(source_code, filepath)
|
@@ -0,0 +1,112 @@
|
|
1
|
+
"""Base rule class for Machine Dialect™ linting rules.
|
2
|
+
|
3
|
+
This module defines the abstract base class that all linting rules
|
4
|
+
must inherit from.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from abc import ABC, abstractmethod
|
8
|
+
from typing import Any
|
9
|
+
|
10
|
+
from machine_dialect.ast import ASTNode
|
11
|
+
from machine_dialect.linter.violations import Violation
|
12
|
+
|
13
|
+
|
14
|
+
class Context:
|
15
|
+
"""Context information passed to rules during linting.
|
16
|
+
|
17
|
+
Attributes:
|
18
|
+
filename: The name of the file being linted.
|
19
|
+
source_lines: The source code split into lines.
|
20
|
+
parent_stack: Stack of parent nodes for context.
|
21
|
+
"""
|
22
|
+
|
23
|
+
def __init__(self, filename: str, source_code: str) -> None:
|
24
|
+
"""Initialize the linting context.
|
25
|
+
|
26
|
+
Args:
|
27
|
+
filename: The name of the file being linted.
|
28
|
+
source_code: The complete source code.
|
29
|
+
"""
|
30
|
+
self.filename = filename
|
31
|
+
self.source_code = source_code
|
32
|
+
self.source_lines = source_code.splitlines()
|
33
|
+
self.parent_stack: list[ASTNode] = []
|
34
|
+
|
35
|
+
def push_parent(self, node: ASTNode) -> None:
|
36
|
+
"""Push a parent node onto the stack.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
node: The parent node to push.
|
40
|
+
"""
|
41
|
+
self.parent_stack.append(node)
|
42
|
+
|
43
|
+
def pop_parent(self) -> ASTNode | None:
|
44
|
+
"""Pop a parent node from the stack.
|
45
|
+
|
46
|
+
Returns:
|
47
|
+
The popped parent node, or None if stack is empty.
|
48
|
+
"""
|
49
|
+
return self.parent_stack.pop() if self.parent_stack else None
|
50
|
+
|
51
|
+
@property
|
52
|
+
def current_parent(self) -> ASTNode | None:
|
53
|
+
"""Get the current parent node without removing it.
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
The current parent node, or None if stack is empty.
|
57
|
+
"""
|
58
|
+
return self.parent_stack[-1] if self.parent_stack else None
|
59
|
+
|
60
|
+
|
61
|
+
class Rule(ABC):
|
62
|
+
"""Abstract base class for linting rules.
|
63
|
+
|
64
|
+
All linting rules must inherit from this class and implement
|
65
|
+
the required methods.
|
66
|
+
"""
|
67
|
+
|
68
|
+
@property
|
69
|
+
@abstractmethod
|
70
|
+
def rule_id(self) -> str:
|
71
|
+
"""Return the unique identifier for this rule.
|
72
|
+
|
73
|
+
Returns:
|
74
|
+
A string identifier like "MD001" or "style-naming".
|
75
|
+
"""
|
76
|
+
pass
|
77
|
+
|
78
|
+
@property
|
79
|
+
@abstractmethod
|
80
|
+
def description(self) -> str:
|
81
|
+
"""Return a human-readable description of what this rule checks.
|
82
|
+
|
83
|
+
Returns:
|
84
|
+
A description of the rule's purpose.
|
85
|
+
"""
|
86
|
+
pass
|
87
|
+
|
88
|
+
@abstractmethod
|
89
|
+
def check(self, node: ASTNode, context: Context) -> list[Violation]:
|
90
|
+
"""Check the given AST node for violations of this rule.
|
91
|
+
|
92
|
+
Args:
|
93
|
+
node: The AST node to check.
|
94
|
+
context: The linting context with additional information.
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
A list of violations found, or empty list if none.
|
98
|
+
"""
|
99
|
+
pass
|
100
|
+
|
101
|
+
def is_enabled(self, config: dict[str, Any]) -> bool:
|
102
|
+
"""Check if this rule is enabled in the configuration.
|
103
|
+
|
104
|
+
Args:
|
105
|
+
config: The linter configuration dictionary.
|
106
|
+
|
107
|
+
Returns:
|
108
|
+
True if the rule is enabled, False otherwise.
|
109
|
+
"""
|
110
|
+
# By default, rules are enabled unless explicitly disabled
|
111
|
+
rules_config = config.get("rules", {})
|
112
|
+
return rules_config.get(self.rule_id, True) is not False
|
@@ -0,0 +1,99 @@
|
|
1
|
+
"""Statement termination rule for Machine Dialect™.
|
2
|
+
|
3
|
+
This rule checks that all statements are properly terminated with periods.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from machine_dialect.ast import (
|
7
|
+
ASTNode,
|
8
|
+
ExpressionStatement,
|
9
|
+
ReturnStatement,
|
10
|
+
SetStatement,
|
11
|
+
)
|
12
|
+
from machine_dialect.linter.rules.base import Context, Rule
|
13
|
+
from machine_dialect.linter.violations import Violation, ViolationSeverity
|
14
|
+
|
15
|
+
|
16
|
+
class StatementTerminationRule(Rule):
|
17
|
+
"""Check that all statements end with periods.
|
18
|
+
|
19
|
+
Machine Dialect™ requires statements to be terminated with periods.
|
20
|
+
This rule checks that the source code follows this convention.
|
21
|
+
"""
|
22
|
+
|
23
|
+
@property
|
24
|
+
def rule_id(self) -> str:
|
25
|
+
"""Return the rule identifier."""
|
26
|
+
return "MD101"
|
27
|
+
|
28
|
+
@property
|
29
|
+
def description(self) -> str:
|
30
|
+
"""Return the rule description."""
|
31
|
+
return "Statements must end with periods"
|
32
|
+
|
33
|
+
def check(self, node: ASTNode, context: Context) -> list[Violation]:
|
34
|
+
"""Check if statements are properly terminated.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
node: The AST node to check.
|
38
|
+
context: The linting context.
|
39
|
+
|
40
|
+
Returns:
|
41
|
+
A list of violations found.
|
42
|
+
"""
|
43
|
+
violations: list[Violation] = []
|
44
|
+
|
45
|
+
# Only check statement nodes
|
46
|
+
if not isinstance(node, SetStatement | ReturnStatement | ExpressionStatement):
|
47
|
+
return violations
|
48
|
+
|
49
|
+
# Get the token that represents this statement
|
50
|
+
token = node.token
|
51
|
+
if not token:
|
52
|
+
return violations
|
53
|
+
|
54
|
+
# Find the line in the source code
|
55
|
+
if token.line <= len(context.source_lines):
|
56
|
+
line = context.source_lines[token.line - 1]
|
57
|
+
|
58
|
+
# Find where this statement likely ends
|
59
|
+
# This is a simplified check - in reality we'd need more sophisticated logic
|
60
|
+
# For now, check if there's a period after the statement
|
61
|
+
|
62
|
+
# Skip if this is not the last statement on the line
|
63
|
+
# (simplified check - just look for period somewhere after the token position)
|
64
|
+
remaining_line = line[token.position :]
|
65
|
+
|
66
|
+
# Check if there's any non-whitespace after the statement before a period
|
67
|
+
found_period = False
|
68
|
+
found_content = False
|
69
|
+
|
70
|
+
for char in remaining_line:
|
71
|
+
if char == ".":
|
72
|
+
found_period = True
|
73
|
+
break
|
74
|
+
elif not char.isspace():
|
75
|
+
found_content = True
|
76
|
+
|
77
|
+
# If we found content but no period, it's likely missing termination
|
78
|
+
# This is a heuristic and may have false positives
|
79
|
+
# Exception: statements at EOF don't require periods
|
80
|
+
is_last_line = token.line == len(context.source_lines)
|
81
|
+
is_last_statement_on_line = not any(
|
82
|
+
line[i:].strip() for i in range(token.position + len(remaining_line.rstrip()), len(line))
|
83
|
+
)
|
84
|
+
is_at_eof = is_last_line and is_last_statement_on_line
|
85
|
+
|
86
|
+
if found_content and not found_period and not is_at_eof:
|
87
|
+
violations.append(
|
88
|
+
Violation(
|
89
|
+
rule_id=self.rule_id,
|
90
|
+
message="Statement should end with a period",
|
91
|
+
severity=ViolationSeverity.STYLE,
|
92
|
+
line=token.line,
|
93
|
+
column=token.position + len(remaining_line.rstrip()),
|
94
|
+
node=node,
|
95
|
+
fix_suggestion="Add a period at the end of the statement",
|
96
|
+
)
|
97
|
+
)
|
98
|
+
|
99
|
+
return violations
|
@@ -0,0 +1 @@
|
|
1
|
+
"""Tests for the Machine Dialect™ linter."""
|
File without changes
|