162 lines
4.9 KiB
Python
162 lines
4.9 KiB
Python
# 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) 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 punctuation.
|
|
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}
|