mirror of
https://github.com/szdytom/LADRSolutions.git
synced 2025-10-19 16:30:16 +00:00
175 lines
5.1 KiB
Python
175 lines
5.1 KiB
Python
import re
|
|
import toml
|
|
import argparse
|
|
import sys
|
|
from collections import defaultdict
|
|
import time
|
|
|
|
class LintRule:
|
|
def __init__(self, name, pattern, exception_names=None, enforce=False, comment=""):
|
|
self.name = name
|
|
self.pattern = re.compile(pattern)
|
|
self.exception_names = exception_names if exception_names else []
|
|
self.exceptions = [] # 将被填充为实际的规则对象
|
|
self.enforce = enforce
|
|
self.comment = comment
|
|
self.depth = 0 # 用于检测循环引用
|
|
|
|
def add_exception(self, rule):
|
|
"""添加例外规则引用"""
|
|
self.exceptions.append(rule)
|
|
|
|
class Linter:
|
|
def __init__(self, config_file="lint.toml"):
|
|
self.rules = []
|
|
self.rule_map = {}
|
|
self.load_rules(config_file)
|
|
self.resolve_exceptions()
|
|
|
|
def load_rules(self, config_file):
|
|
"""从TOML文件加载规则配置"""
|
|
try:
|
|
with open(config_file, "r") as f:
|
|
config = toml.load(f)
|
|
except FileNotFoundError:
|
|
print(f"Error: Config file '{config_file}' not found", file=sys.stderr)
|
|
sys.exit(1)
|
|
except toml.TomlDecodeError as e:
|
|
print(f"Error parsing TOML: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
for rule_config in config.get("rule", []):
|
|
try:
|
|
rule = LintRule(
|
|
name=rule_config["name"],
|
|
pattern=rule_config["pattern"],
|
|
exception_names=rule_config.get("exceptions"),
|
|
enforce=rule_config.get("enforce", False),
|
|
comment=rule_config.get("comment", "")
|
|
)
|
|
self.rules.append(rule)
|
|
self.rule_map[rule.name] = rule
|
|
except KeyError as e:
|
|
print(f"Error: Missing required field '{e}' in rule", file=sys.stderr)
|
|
sys.exit(1)
|
|
except re.error as e:
|
|
print(f"Error compiling regex in rule '{rule_config.get('name', '')}': {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
def resolve_exceptions(self):
|
|
"""解析规则间的依赖关系"""
|
|
# 第一遍:添加直接例外引用
|
|
for rule in self.rules:
|
|
for exception_name in rule.exception_names:
|
|
if exception_name in self.rule_map:
|
|
rule.add_exception(self.rule_map[exception_name])
|
|
else:
|
|
print(f"Error: Rule '{rule.name}' references undefined exception rule '{exception_name}'", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# 第二遍:计算规则深度(用于检测循环依赖)
|
|
changed = True
|
|
while changed:
|
|
changed = False
|
|
for rule in self.rules:
|
|
if rule.exceptions:
|
|
new_depth = max(e.depth for e in rule.exceptions) + 1
|
|
if new_depth > len(self.rules):
|
|
print(f"Error: Circular dependency detected in rule '{rule.name}'", file=sys.stderr)
|
|
sys.exit(1)
|
|
if new_depth > rule.depth:
|
|
rule.depth = new_depth
|
|
changed = True
|
|
|
|
def match_rule(self, rule, text, visited=None):
|
|
"""
|
|
递归检查规则匹配
|
|
- 主规则匹配成功
|
|
- 所有例外规则都不匹配
|
|
"""
|
|
if visited is None:
|
|
visited = set()
|
|
|
|
# 检测循环引用
|
|
if rule.name in visited:
|
|
print(f"Warning: Circular rule reference detected while matching '{rule.name}', skipping")
|
|
return False
|
|
|
|
visited.add(rule.name)
|
|
|
|
# 检查主规则是否匹配
|
|
if not rule.pattern.search(text):
|
|
return False
|
|
|
|
# 检查例外规则:任何一个例外规则匹配则整个规则不匹配
|
|
for exception in rule.exceptions:
|
|
if self.match_rule(exception, text, visited.copy()):
|
|
return False
|
|
|
|
return True
|
|
|
|
def lint_file(self, file_path):
|
|
"""检查单个文件并返回违规列表"""
|
|
try:
|
|
with open(file_path, "r") as f:
|
|
lines = f.readlines()
|
|
except IOError as e:
|
|
print(f"Error reading file {file_path}: {e}", file=sys.stderr)
|
|
return []
|
|
|
|
violations = []
|
|
for line_num, line in enumerate(lines, start=1):
|
|
# 保留行尾换行符以正确匹配行尾模式
|
|
line_with_nl = line
|
|
|
|
for rule in self.rules:
|
|
if rule.enforce and self.match_rule(rule, line_with_nl):
|
|
violations.append({
|
|
"file": file_path,
|
|
"line": line_num,
|
|
"rule": rule.name,
|
|
"comment": rule.comment,
|
|
"text": line.rstrip('\n') # 去除换行符用于显示
|
|
})
|
|
|
|
return violations
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Simple Document Format Linter")
|
|
parser.add_argument("files", nargs="+", help="Files to lint")
|
|
parser.add_argument("--config", default="lint.toml", help="Path to config file")
|
|
args = parser.parse_args()
|
|
|
|
start_time = time.time() # Start timing
|
|
|
|
linter = Linter(args.config)
|
|
|
|
total_violations = 0
|
|
is_tty = sys.stdout.isatty()
|
|
RED = "\033[31m" if is_tty else ""
|
|
RESET = "\033[0m" if is_tty else ""
|
|
GRAY = "\033[90m" if is_tty else ""
|
|
GREEN = "\033[32m" if is_tty else ""
|
|
|
|
for file_path in args.files:
|
|
violations = linter.lint_file(file_path)
|
|
total_violations += len(violations)
|
|
|
|
for v in violations:
|
|
comment = f" - {v['comment']}" if v['comment'] else ""
|
|
# Highlight violation header in red, violation text in gray if TTY
|
|
print(f"{RED}{v['file']}:{v['line']}{RESET} [{v['rule']}]{comment}")
|
|
print(f"{GRAY} | {v['text']}{RESET}")
|
|
|
|
elapsed = time.time() - start_time # End timing
|
|
|
|
if total_violations == 0:
|
|
print(f"{GREEN}[OK] No violations found.{RESET}")
|
|
else:
|
|
print(f"\nFound {total_violations} violation(s).")
|
|
print(f"Lint done in {elapsed:.3f} seconds.") # Show elapsed time
|
|
sys.exit(1 if total_violations > 0 else 0)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|