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