from __future__ import print_function, unicode_literals import abc import argparse import ast import json import os import re import subprocess import sys from collections import defaultdict from . import fnmatch from ..localpaths import repo_root from ..gitignore.gitignore import PathFilter from manifest.sourcefile import SourceFile, meta_re from six import binary_type, iteritems, itervalues from six.moves import range here = os.path.abspath(os.path.split(__file__)[0]) ERROR_MSG = """You must fix all errors; for details on how to fix them, see https://github.com/w3c/web-platform-tests/blob/master/docs/lint-tool.md However, instead of fixing a particular error, it's sometimes OK to add a line to the lint.whitelist file in the root of the web-platform-tests directory to make the lint tool ignore it. For example, to make the lint tool ignore all '%s' errors in the %s file, you could add the following line to the lint.whitelist file. %s:%s""" def all_git_paths(repo_root): command_line = ["git", "ls-tree", "-r", "--name-only", "HEAD"] output = subprocess.check_output(command_line, cwd=repo_root) for item in output.split("\n"): yield item def all_filesystem_paths(repo_root): path_filter = PathFilter(repo_root, extras=[".git/*"]) for dirpath, dirnames, filenames in os.walk(repo_root): for filename in filenames: path = os.path.relpath(os.path.join(dirpath, filename), repo_root) if path_filter(path): yield path dirnames[:] = [item for item in dirnames if path_filter(os.path.relpath(os.path.join(dirpath, item) + "/", repo_root))] def all_paths(repo_root, ignore_local): fn = all_git_paths if ignore_local else all_filesystem_paths for item in fn(repo_root): yield item def check_path_length(repo_root, path, css_mode): if len(path) + 1 > 150: return [("PATH LENGTH", "/%s longer than maximum path length (%d > 150)" % (path, len(path) + 1), path, None)] return [] def check_worker_collision(repo_root, path, css_mode): endings = [(".any.html", ".any.js"), (".any.worker.html", ".any.js"), (".worker.html", ".worker.js")] for path_ending, generated in endings: if path.endswith(path_ending): return [("WORKER COLLISION", "path ends with %s which collides with generated tests from %s files" % (path_ending, generated), path, None)] return [] def parse_whitelist(f): """ Parse the whitelist file given by `f`, and return the parsed structure. """ data = defaultdict(lambda:defaultdict(set)) ignored_files = set() for line in f: line = line.strip() if not line or line.startswith("#"): continue parts = [item.strip() for item in line.split(":")] if len(parts) == 2: parts.append(None) else: parts[-1] = int(parts[-1]) error_type, file_match, line_number = parts file_match = os.path.normcase(file_match) if error_type == "*": ignored_files.add(file_match) else: data[file_match][error_type].add(line_number) return data, ignored_files def filter_whitelist_errors(data, path, errors): """ Filter out those errors that are whitelisted in `data`. """ if not errors: return [] whitelisted = [False for item in range(len(errors))] normpath = os.path.normcase(path) for file_match, whitelist_errors in iteritems(data): if fnmatch.fnmatchcase(normpath, file_match): for i, (error_type, msg, path, line) in enumerate(errors): if error_type in whitelist_errors: allowed_lines = whitelist_errors[error_type] if None in allowed_lines or line in allowed_lines: whitelisted[i] = True return [item for i, item in enumerate(errors) if not whitelisted[i]] class Regexp(object): pattern = None file_extensions = None error = None _re = None def __init__(self): self._re = re.compile(self.pattern) def applies(self, path): return (self.file_extensions is None or os.path.splitext(path)[1] in self.file_extensions) def search(self, line): return self._re.search(line) class TrailingWhitespaceRegexp(Regexp): pattern = b"[ \t\f\v]$" error = "TRAILING WHITESPACE" description = "Whitespace at EOL" class TabsRegexp(Regexp): pattern = b"^\t" error = "INDENT TABS" description = "Tabs used for indentation" class CRRegexp(Regexp): pattern = b"\r$" error = "CR AT EOL" description = "CR character in line separator" class W3CTestOrgRegexp(Regexp): pattern = b"w3c\-test\.org" error = "W3C-TEST.ORG" description = "External w3c-test.org domain used" class Webidl2Regexp(Regexp): pattern = b"webidl2\.js" error = "WEBIDL2.JS" description = "Legacy webidl2.js script used" class ConsoleRegexp(Regexp): pattern = b"console\.[a-zA-Z]+\s*\(" error = "CONSOLE" file_extensions = [".html", ".htm", ".js", ".xht", ".xhtml", ".svg"] description = "Console logging API used" class PrintRegexp(Regexp): pattern = b"print(?:\s|\s*\()" error = "PRINT STATEMENT" file_extensions = [".py"] description = "Print function used" regexps = [item() for item in [TrailingWhitespaceRegexp, TabsRegexp, CRRegexp, W3CTestOrgRegexp, Webidl2Regexp, ConsoleRegexp, PrintRegexp]] def check_regexp_line(repo_root, path, f, css_mode): errors = [] applicable_regexps = [regexp for regexp in regexps if regexp.applies(path)] for i, line in enumerate(f): for regexp in applicable_regexps: if regexp.search(line): errors.append((regexp.error, regexp.description, path, i+1)) return errors def check_parsed(repo_root, path, f, css_mode): source_file = SourceFile(repo_root, path, "/", contents=f.read()) errors = [] if css_mode or path.startswith("css/"): if (source_file.type == "support" and not source_file.name_is_non_test and not source_file.name_is_reference): return [("SUPPORT-WRONG-DIR", "Support file not in support directory", path, None)] if source_file.name_is_non_test or source_file.name_is_manual: return [] if source_file.markup_type is None: return [] if source_file.root is None: return [("PARSE-FAILED", "Unable to parse file", path, None)] if source_file.type == "manual" and not source_file.name_is_manual: return [("CONTENT-MANUAL", "Manual test whose filename doesn't end in '-manual'", path, None)] if source_file.type == "visual" and not source_file.name_is_visual: return [("CONTENT-VISUAL", "Visual test whose filename doesn't end in '-visual'", path, None)] if len(source_file.timeout_nodes) > 1: errors.append(("MULTIPLE-TIMEOUT", "More than one meta name='timeout'", path, None)) for timeout_node in source_file.timeout_nodes: timeout_value = timeout_node.attrib.get("content", "").lower() if timeout_value != "long": errors.append(("INVALID-TIMEOUT", "Invalid timeout value %s" % timeout_value, path, None)) if source_file.testharness_nodes: if len(source_file.testharness_nodes) > 1: errors.append(("MULTIPLE-TESTHARNESS", "More than one