diff --git a/lint.py b/lint.py new file mode 100644 index 0000000..2c444da --- /dev/null +++ b/lint.py @@ -0,0 +1,174 @@ +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() diff --git a/lint.toml b/lint.toml new file mode 100644 index 0000000..22c4c03 --- /dev/null +++ b/lint.toml @@ -0,0 +1,17 @@ +[[rule]] +name = "trailing-whitespace" +pattern = "\\s+\\n$" +enforce = true +comment = "Trailing whitespace at line end" + +[[rule]] +name = "not-centered-dots-within-big-notations" +pattern = "[=+][\\s&]*dots[^\\.]" +enforce = true +comment = "Please use dots.c within = or +" + +[[rule]] +name = "centered-dots-within-small-notations" +pattern = "[,][\\s&]*dots\\.c" +enforce = true +comment = "Please use dots within ,"