Source code

Revision control

Copy as Markdown

Other Tools

# 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}