summaryrefslogtreecommitdiffstats
path: root/tools/lint/ignorefile/__init__.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/lint/ignorefile/__init__.py')
-rw-r--r--tools/lint/ignorefile/__init__.py162
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}