diff options
Diffstat (limited to 'tools/lint/ignorefile')
-rw-r--r-- | tools/lint/ignorefile/__init__.py | 162 |
1 files changed, 162 insertions, 0 deletions
diff --git a/tools/lint/ignorefile/__init__.py b/tools/lint/ignorefile/__init__.py new file mode 100644 index 0000000000..2b8ed8301f --- /dev/null +++ b/tools/lint/ignorefile/__init__.py @@ -0,0 +1,162 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import pathlib +import re + +from mozlint import result +from mozlint.pathutils import expand_exclusions + +validReasons = [ + "git-only", + "hg-only", + "syntax-difference", +] + + +class IgnorePattern: + """A class that represents single pattern in .gitignore or .hgignore, and + provides rough comparison, and also hashing for SequenceMatcher.""" + + def __init__(self, pattern, lineno): + self.pattern = pattern + self.lineno = lineno + + self.simplePattern = self.removePunctuation(pattern) + self.hash = hash(self.pattern) + + @staticmethod + def removePunctuation(s): + # Remove special characters. + # '.' is also removed because .hgignore uses regular expression. + s = re.sub(r"[^A-Za-z0-9_~/]", "", s) + + # '/' is kept, except for the following cases: + # * leading '/' in .gitignore to specify top-level file, + # which is represented as '^' in .hgignore + # * leading '(^|/)' in .hgignore to specify filename + # (characters except for '/' are removed above) + # * leading '[^/]*' in .hgignore + # (characters except for '/' are removed above) + s = re.sub(r"^/", "", s) + + return s + + def __eq__(self, other): + return self.simplePattern == other.simplePattern + + def __hash__(self): + return self.hash + + +def parseFile(results, path, config): + patterns = [] + ignoreNextLine = False + + lineno = 0 + with open(path, "r") as f: + for line in f: + line = line.rstrip("\n") + lineno += 1 + + if ignoreNextLine: + ignoreNextLine = False + continue + + m = re.match(r"^# lint-ignore-next-line: (.+)", line) + if m: + reason = m.group(1) + if reason not in validReasons: + res = { + "path": str(path), + "lineno": lineno, + "message": f'Unknown lint rule: "{reason}"', + "level": "error", + } + results.append(result.from_config(config, **res)) + continue + + ignoreNextLine = True + continue + + m = line.startswith("#") + if m: + continue + + if line == "": + continue + + patterns.append(IgnorePattern(line, lineno)) + + return patterns + + +def getLineno(patterns, index): + if index >= len(patterns): + return patterns[-1].lineno + + return patterns[index].lineno + + +def doLint(results, path1, config): + if path1.name == ".gitignore": + path2 = path1.parent / ".hgignore" + elif path1.name == ".hgignore": + path2 = path1.parent / ".gitignore" + else: + res = { + "path": str(path1), + "lineno": 0, + "message": "Unsupported file", + "level": "error", + } + results.append(result.from_config(config, **res)) + return + + patterns1 = parseFile(results, path1, config) + patterns2 = parseFile(results, path2, config) + + # Comparison for each line is done via IgnorePattern.__eq__, which + # ignores punctuations. + if patterns1 == patterns2: + return + + # Report minimal differences. + from difflib import SequenceMatcher + + s = SequenceMatcher(None, patterns1, patterns2) + for tag, index1, _, index2, _ in s.get_opcodes(): + if tag == "replace": + res = { + "path": str(path1), + "lineno": getLineno(patterns1, index1), + "message": f'Pattern mismatch: "{patterns1[index1].pattern}" in {path1.name} vs "{patterns2[index2].pattern}" in {path2.name}', + "level": "error", + } + results.append(result.from_config(config, **res)) + elif tag == "delete": + res = { + "path": str(path1), + "lineno": getLineno(patterns1, index1), + "message": f'Pattern "{patterns1[index1].pattern}" not found in {path2.name}', + "level": "error", + } + results.append(result.from_config(config, **res)) + elif tag == "insert": + res = { + "path": str(path1), + "lineno": getLineno(patterns1, index1), + "message": f'Pattern "{patterns2[index2].pattern}" not found in {path1.name}', + "level": "error", + } + results.append(result.from_config(config, **res)) + + +def lint(paths, config, fix=None, **lintargs): + results = [] + + for path in expand_exclusions(paths, config, lintargs["root"]): + doLint(results, pathlib.Path(path), config) + + return {"results": results, "fixed": 0} |