aboutsummaryrefslogtreecommitdiffstats
path: root/python/tidy/servo_tidy
diff options
context:
space:
mode:
authorAskeing Yen (fyen) <askeing@gmail.com>2016-04-13 16:00:23 -0700
committeredunham <edunham@mozilla.com>2016-04-13 16:07:04 -0700
commitaf2ca1fa8076a5ab78f1162aaac819bb8e826ca3 (patch)
tree6342d11b329eb247c1a4580a57ae6f6ec86e77d9 /python/tidy/servo_tidy
parent711bfe2860a308acc12a7c0437b9832d3d454c1f (diff)
downloadservo-af2ca1fa8076a5ab78f1162aaac819bb8e826ca3.tar.gz
servo-af2ca1fa8076a5ab78f1162aaac819bb8e826ca3.zip
Create a Python package of Tidy
Diffstat (limited to 'python/tidy/servo_tidy')
-rw-r--r--python/tidy/servo_tidy/__init__.py8
-rw-r--r--python/tidy/servo_tidy/licenseck.py62
-rw-r--r--python/tidy/servo_tidy/tidy.py650
3 files changed, 720 insertions, 0 deletions
diff --git a/python/tidy/servo_tidy/__init__.py b/python/tidy/servo_tidy/__init__.py
new file mode 100644
index 00000000000..6b6351ddd2b
--- /dev/null
+++ b/python/tidy/servo_tidy/__init__.py
@@ -0,0 +1,8 @@
+# Copyright 2013 The Servo Project Developers. See the COPYRIGHT
+# file at the top-level directory of this distribution.
+#
+# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+# http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+# <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+# option. This file may not be copied, modified, or distributed
+# except according to those terms.
diff --git a/python/tidy/servo_tidy/licenseck.py b/python/tidy/servo_tidy/licenseck.py
new file mode 100644
index 00000000000..705faf8bcb6
--- /dev/null
+++ b/python/tidy/servo_tidy/licenseck.py
@@ -0,0 +1,62 @@
+# Copyright 2013 The Servo Project Developers. See the COPYRIGHT
+# file at the top-level directory of this distribution.
+#
+# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+# http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+# <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+# option. This file may not be copied, modified, or distributed
+# except according to those terms.
+
+
+# These licenses are valid for use in Servo
+licenses = [
+
+"""\
+/* 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 http://mozilla.org/MPL/2.0/. */
+""",
+
+"""\
+# 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 http://mozilla.org/MPL/2.0/.
+""",
+
+"""\
+#!/usr/bin/env 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 http://mozilla.org/MPL/2.0/.
+""",
+
+"""\
+// 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 http://mozilla.org/MPL/2.0/.
+""",
+
+"""\
+// Copyright 2013 The Servo Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+""",
+
+"""\
+# Copyright 2013 The Servo Project Developers. See the COPYRIGHT
+# file at the top-level directory of this distribution.
+#
+# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+# http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+# <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+# option. This file may not be copied, modified, or distributed
+# except according to those terms.
+""",
+
+] # noqa: Indicate to flake8 that we do not want to check indentation here
diff --git a/python/tidy/servo_tidy/tidy.py b/python/tidy/servo_tidy/tidy.py
new file mode 100644
index 00000000000..6110a9ea4eb
--- /dev/null
+++ b/python/tidy/servo_tidy/tidy.py
@@ -0,0 +1,650 @@
+# Copyright 2013 The Servo Project Developers. See the COPYRIGHT
+# file at the top-level directory of this distribution.
+#
+# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+# http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+# <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+# option. This file may not be copied, modified, or distributed
+# except according to those terms.
+
+import contextlib
+import fnmatch
+import itertools
+import json
+import os
+import re
+import site
+import StringIO
+import subprocess
+import sys
+from licenseck import licenses
+
+# File patterns to include in the non-WPT tidy check.
+file_patterns_to_check = ["*.rs", "*.rc", "*.cpp", "*.c",
+ "*.h", "Cargo.lock", "*.py",
+ "*.toml", "*.webidl", "*.json"]
+
+# File patterns that are ignored for all tidy and lint checks.
+file_patterns_to_ignore = [
+ "*.#*",
+ "*.pyc",
+]
+
+# Files that are ignored for all tidy and lint checks.
+ignored_files = [
+ # Generated and upstream code combined with our own. Could use cleanup
+ os.path.join(".", "ports", "gonk", "src", "native_window_glue.cpp"),
+ os.path.join(".", "ports", "geckolib", "bindings.rs"),
+ os.path.join(".", "ports", "geckolib", "gecko_style_structs.rs"),
+ os.path.join(".", "resources", "hsts_preload.json"),
+ os.path.join(".", "tests", "wpt", "metadata", "MANIFEST.json"),
+ os.path.join(".", "tests", "wpt", "metadata-css", "MANIFEST.json"),
+ os.path.join(".", "components", "script", "dom", "webidls", "ForceTouchEvent.webidl"),
+ # Hidden files
+ os.path.join(".", "."),
+]
+
+# Directories that are ignored for the non-WPT tidy check.
+ignored_dirs = [
+ # Upstream
+ os.path.join(".", "support", "android", "apk"),
+ os.path.join(".", "support", "rust-task_info"),
+ os.path.join(".", "tests", "wpt", "css-tests"),
+ os.path.join(".", "tests", "wpt", "harness"),
+ os.path.join(".", "tests", "wpt", "update"),
+ os.path.join(".", "tests", "wpt", "web-platform-tests"),
+ os.path.join(".", "tests", "wpt", "sync"),
+ os.path.join(".", "tests", "wpt", "sync_css"),
+ os.path.join(".", "python", "mach"),
+ os.path.join(".", "python", "tidy_self_test"),
+ os.path.join(".", "components", "script", "dom", "bindings", "codegen", "parser"),
+ os.path.join(".", "components", "script", "dom", "bindings", "codegen", "ply"),
+ os.path.join(".", "python", "_virtualenv"),
+ # Generated and upstream code combined with our own. Could use cleanup
+ os.path.join(".", "target"),
+ os.path.join(".", "ports", "cef"),
+ # Tooling, generated locally from external repos.
+ os.path.join(".", "ports", "geckolib", "tools"),
+ # Hidden directories
+ os.path.join(".", "."),
+]
+
+
+def is_iter_empty(iterator):
+ try:
+ obj = iterator.next()
+ return True, itertools.chain((obj,), iterator)
+ except StopIteration:
+ return False, iterator
+
+
+# A simple wrapper for iterators to show progress (note that it's inefficient for giant iterators)
+def progress_wrapper(iterator):
+ list_of_stuff = list(iterator)
+ total_files, progress = len(list_of_stuff), 0
+ for idx, thing in enumerate(list_of_stuff):
+ progress = int(float(idx + 1) / total_files * 100)
+ sys.stdout.write('\r Progress: %s%% (%d/%d)' % (progress, idx + 1, total_files))
+ sys.stdout.flush()
+ yield thing
+
+
+def filter_file(file_name):
+ if any(file_name.startswith(ignored_file) for ignored_file in ignored_files):
+ return False
+ base_name = os.path.basename(file_name)
+ if any(fnmatch.fnmatch(base_name, pattern) for pattern in file_patterns_to_ignore):
+ return False
+ return True
+
+
+def filter_files(start_dir, faster, progress):
+ file_iter = get_file_list(start_dir, faster, ignored_dirs)
+ (has_element, file_iter) = is_iter_empty(file_iter)
+ if not has_element:
+ raise StopIteration
+ if progress:
+ file_iter = progress_wrapper(file_iter)
+ for file_name in file_iter:
+ base_name = os.path.basename(file_name)
+ if not any(fnmatch.fnmatch(base_name, pattern) for pattern in file_patterns_to_check):
+ continue
+ if not filter_file(file_name):
+ continue
+ yield file_name
+
+
+EMACS_HEADER = "/* -*- Mode:"
+VIM_HEADER = "/* vim:"
+MAX_LICENSE_LINESPAN = max(len(license.splitlines()) for license in licenses)
+
+
+def check_license(file_name, lines):
+ if any(file_name.endswith(ext) for ext in (".toml", ".lock", ".json")):
+ raise StopIteration
+ while lines and (lines[0].startswith(EMACS_HEADER) or lines[0].startswith(VIM_HEADER)):
+ lines = lines[1:]
+ contents = "".join(lines[:MAX_LICENSE_LINESPAN])
+ valid_license = any(contents.startswith(license) for license in licenses)
+ acknowledged_bad_license = "xfail-license" in contents
+ if not (valid_license or acknowledged_bad_license):
+ yield (1, "incorrect license")
+
+
+def check_length(file_name, idx, line):
+ if file_name.endswith(".lock") or file_name.endswith(".json"):
+ raise StopIteration
+ max_length = 120
+ if len(line.rstrip('\n')) > max_length:
+ yield (idx + 1, "Line is longer than %d characters" % max_length)
+
+
+def check_whatwg_specific_url(idx, line):
+ match = re.search(r"https://html\.spec\.whatwg\.org/multipage/[\w-]+\.html#([\w\:-]+)", line)
+ if match is not None:
+ preferred_link = "https://html.spec.whatwg.org/multipage/#{}".format(match.group(1))
+ yield (idx + 1, "link to WHATWG may break in the future, use this format instead: {}".format(preferred_link))
+
+
+def check_whatwg_single_page_url(idx, line):
+ match = re.search(r"https://html\.spec\.whatwg\.org/#([\w\:-]+)", line)
+ if match is not None:
+ preferred_link = "https://html.spec.whatwg.org/multipage/#{}".format(match.group(1))
+ yield (idx + 1, "links to WHATWG single-page url, change to multi page: {}".format(preferred_link))
+
+
+def check_whitespace(idx, line):
+ if line[-1] == "\n":
+ line = line[:-1]
+ else:
+ yield (idx + 1, "no newline at EOF")
+
+ if line.endswith(" "):
+ yield (idx + 1, "trailing whitespace")
+
+ if "\t" in line:
+ yield (idx + 1, "tab on line")
+
+ if "\r" in line:
+ yield (idx + 1, "CR on line")
+
+
+def check_by_line(file_name, lines):
+ for idx, line in enumerate(lines):
+ errors = itertools.chain(
+ check_length(file_name, idx, line),
+ check_whitespace(idx, line),
+ check_whatwg_specific_url(idx, line),
+ check_whatwg_single_page_url(idx, line),
+ )
+
+ for error in errors:
+ yield error
+
+
+def check_flake8(file_name, contents):
+ from flake8.main import check_code
+
+ if not file_name.endswith(".py"):
+ raise StopIteration
+
+ @contextlib.contextmanager
+ def stdout_redirect(where):
+ sys.stdout = where
+ try:
+ yield where
+ finally:
+ sys.stdout = sys.__stdout__
+
+ ignore = {
+ "W291", # trailing whitespace; the standard tidy process will enforce no trailing whitespace
+ "E501", # 80 character line length; the standard tidy process will enforce line length
+ }
+
+ output = StringIO.StringIO()
+ with stdout_redirect(output):
+ check_code(contents, ignore=ignore)
+ for error in output.getvalue().splitlines():
+ _, line_num, _, message = error.split(":", 3)
+ yield line_num, message.strip()
+
+
+def check_lock(file_name, contents):
+ def find_reverse_dependencies(dependency, version, content):
+ dependency_prefix = "{} {}".format(dependency, version)
+ for package in itertools.chain([content["root"]], content["package"]):
+ for dependency in package.get("dependencies", []):
+ if dependency.startswith(dependency_prefix):
+ yield package["name"]
+
+ if not file_name.endswith(".lock"):
+ raise StopIteration
+
+ # package names to be neglected (as named by cargo)
+ exceptions = ["bitflags", "xml-rs", "byteorder"]
+
+ import toml
+ content = toml.loads(contents)
+
+ packages = {}
+ for package in content.get("package", []):
+ packages.setdefault(package["name"], []).append(package["version"])
+
+ for (name, versions) in packages.iteritems():
+ if name in exceptions or len(versions) <= 1:
+ continue
+
+ highest = max(versions)
+ for version in versions:
+ if version != highest:
+ reverse_dependencies = "\n".join(
+ "\t\t{}".format(n)
+ for n in find_reverse_dependencies(name, version, content)
+ )
+ substitutions = {
+ "package": name,
+ "old_version": version,
+ "new_version": highest,
+ "reverse_dependencies": reverse_dependencies
+ }
+ message = """
+duplicate versions for package "{package}"
+\t\033[93mfound dependency on version {old_version}\033[0m
+\t\033[91mbut highest version is {new_version}\033[0m
+\t\033[93mtry upgrading with\033[0m \033[96m./mach cargo-update -p {package}:{old_version}\033[0m
+\tThe following packages depend on version {old_version}:
+{reverse_dependencies}
+""".format(**substitutions).strip()
+ yield (1, message)
+
+
+def maybe_int(value):
+ try:
+ return int(value)
+ except ValueError:
+ return value
+
+
+def check_toml(file_name, lines):
+ if not file_name.endswith(".toml"):
+ raise StopIteration
+ for idx, line in enumerate(lines):
+ if line.find("*") != -1:
+ yield (idx + 1, "found asterisk instead of minimum version number")
+
+
+def check_rust(file_name, lines):
+ if not file_name.endswith(".rs") or \
+ file_name.endswith("properties.mako.rs") or \
+ file_name.endswith(os.path.join("style", "build.rs")) or \
+ file_name.endswith(os.path.join("geckolib", "build.rs")) or \
+ file_name.endswith(os.path.join("unit", "style", "stylesheets.rs")):
+ raise StopIteration
+ comment_depth = 0
+ merged_lines = ''
+
+ import_block = False
+ whitespace = False
+
+ prev_use = None
+ current_indent = 0
+ prev_crate = {}
+ prev_mod = {}
+
+ decl_message = "{} is not in alphabetical order"
+ decl_expected = "\n\t\033[93mexpected: {}\033[0m"
+ decl_found = "\n\t\033[91mfound: {}\033[0m"
+
+ for idx, original_line in enumerate(lines):
+ # simplify the analysis
+ line = original_line.strip()
+
+ # Simple heuristic to avoid common case of no comments.
+ if '/' in line:
+ comment_depth += line.count('/*')
+ comment_depth -= line.count('*/')
+
+ if line.endswith('\\'):
+ merged_lines += line[:-1]
+ continue
+ if comment_depth:
+ merged_lines += line
+ continue
+ if merged_lines:
+ line = merged_lines + line
+ merged_lines = ''
+
+ # Keep track of whitespace to enable checking for a merged import block
+ #
+ # Ignore attributes, comments, and imports
+ if import_block:
+ if not (line_is_comment(line) or line_is_attribute(line) or line.startswith("use ")):
+ whitespace = line == ""
+
+ if not whitespace:
+ import_block = False
+
+ # get rid of strings and chars because cases like regex expression, keep attributes
+ if not line_is_attribute(line):
+ line = re.sub(r'"(\\.|[^\\"])*?"|' + r"'(\\.|[^\\'])*?'", '', line)
+
+ # get rid of comments
+ line = re.sub('//.*?$|/\*.*?$|^\*.*?$', '', line)
+
+ # get rid of attributes that do not contain =
+ line = re.sub('^#[A-Za-z0-9\(\)\[\]_]*?$', '', line)
+
+ # flag this line if it matches one of the following regular expressions
+ # tuple format: (pattern, format_message, filter_function(match, line))
+ no_filter = lambda match, line: True
+ regex_rules = [
+ (r",[^\s]", "missing space after ,", lambda match, line: '$' not in line),
+ (r"[A-Za-z0-9\"]=", "missing space before =",
+ lambda match, line: line_is_attribute(line)),
+ (r"=[A-Za-z0-9\"]", "missing space after =",
+ lambda match, line: line_is_attribute(line)),
+ # ignore scientific notation patterns like 1e-6
+ (r"[A-DF-Za-df-z0-9]-", "missing space before -",
+ lambda match, line: not line_is_attribute(line)),
+ (r"[A-Za-z0-9]([\+/\*%=])", "missing space before {0}",
+ lambda match, line: (not line_is_attribute(line) and
+ not is_associated_type(match, line))),
+ # * not included because of dereferencing and casting
+ # - not included because of unary negation
+ (r'([\+/\%=])[A-Za-z0-9"]', "missing space after {0}",
+ lambda match, line: (not line_is_attribute(line) and
+ not is_associated_type(match, line))),
+ (r"\)->", "missing space before ->", no_filter),
+ (r"->[A-Za-z]", "missing space after ->", no_filter),
+ (r"[^ ]=>", "missing space before =>", lambda match, line: match.start() != 0),
+ (r"=>[^ ]", "missing space after =>", lambda match, line: match.end() != len(line)),
+ (r"=> ", "extra space after =>", no_filter),
+ # ignore " ::crate::mod" and "trait Foo : Bar"
+ (r" :[^:]", "extra space before :",
+ lambda match, line: 'trait ' not in line[:match.start()]),
+ # ignore "crate::mod" and ignore flagging macros like "$t1:expr"
+ (r"[^:]:[A-Za-z]", "missing space after :",
+ lambda match, line: '$' not in line[:match.end()]),
+ (r"[A-Za-z0-9\)]{", "missing space before {{", no_filter),
+ # ignore cases like "{}", "}`", "}}" and "use::std::{Foo, Bar}"
+ (r"[^\s{}]}[^`]", "missing space before }}",
+ lambda match, line: not re.match(r'^(pub )?use', line)),
+ # ignore cases like "{}", "`{", "{{" and "use::std::{Foo, Bar}"
+ (r"[^`]{[^\s{}]", "missing space after {{",
+ lambda match, line: not re.match(r'^(pub )?use', line)),
+ # There should not be any extra pointer dereferencing
+ (r": &Vec<", "use &[T] instead of &Vec<T>", no_filter),
+ # No benefit over using &str
+ (r": &String", "use &str instead of &String", no_filter),
+ ]
+
+ for pattern, message, filter_func in regex_rules:
+ for match in re.finditer(pattern, line):
+ if not filter_func(match, line):
+ continue
+
+ yield (idx + 1, message.format(*match.groups(), **match.groupdict()))
+
+ # check alphabetical order of extern crates
+ if line.startswith("extern crate "):
+ # strip "extern crate " from the begin and ";" from the end
+ crate_name = line[13:-1]
+ indent = len(original_line) - len(line)
+ if indent not in prev_crate:
+ prev_crate[indent] = ""
+ if prev_crate[indent] > crate_name:
+ yield(idx + 1, decl_message.format("extern crate declaration")
+ + decl_expected.format(prev_crate[indent])
+ + decl_found.format(crate_name))
+ prev_crate[indent] = crate_name
+
+ # imports must be in the same line, alphabetically sorted, and merged
+ # into a single import block
+ if line.startswith("use "):
+ import_block = True
+ indent = len(original_line) - len(line)
+ if not line.endswith(";") and '{' in line:
+ yield (idx + 1, "use statement spans multiple lines")
+ # strip "use" from the begin and ";" from the end
+ current_use = line[4:-1]
+ if indent == current_indent and prev_use and current_use < prev_use:
+ yield(idx + 1, decl_message.format("use statement")
+ + decl_expected.format(prev_use)
+ + decl_found.format(current_use))
+ prev_use = current_use
+ current_indent = indent
+
+ if whitespace or not import_block:
+ current_indent = 0
+
+ # do not allow blank lines in an import block
+ if import_block and whitespace and line.startswith("use "):
+ whitespace = False
+ yield(idx, "encountered whitespace following a use statement")
+
+ # modules must be in the same line and alphabetically sorted
+ if line.startswith("mod ") or line.startswith("pub mod "):
+ indent = len(original_line) - len(line)
+ # strip /(pub )?mod/ from the left and ";" from the right
+ mod = line[4:-1] if line.startswith("mod ") else line[8:-1]
+
+ if (idx - 1) < 0 or "#[macro_use]" not in lines[idx - 1]:
+ match = line.find(" {")
+ if indent not in prev_mod:
+ prev_mod[indent] = ""
+ if match == -1 and not line.endswith(";"):
+ yield (idx + 1, "mod declaration spans multiple lines")
+ if len(prev_mod[indent]) > 0 and mod < prev_mod[indent]:
+ yield(idx + 1, decl_message.format("mod declaration")
+ + decl_expected.format(prev_mod[indent])
+ + decl_found.format(mod))
+ prev_mod[indent] = mod
+ else:
+ # we now erase previous entries
+ prev_mod = {}
+
+
+# Avoid flagging <Item=Foo> constructs
+def is_associated_type(match, line):
+ if match.group(1) != '=':
+ return False
+ open_angle = line[0:match.end()].rfind('<')
+ close_angle = line[open_angle:].find('>') if open_angle != -1 else -1
+ generic_open = open_angle != -1 and open_angle < match.start()
+ generic_close = close_angle != -1 and close_angle + open_angle >= match.end()
+ return generic_open and generic_close
+
+
+def line_is_attribute(line):
+ return re.search(r"#\[.*\]", line)
+
+
+def line_is_comment(line):
+ return re.search(r"^//|^/\*|^\*", line)
+
+
+def check_webidl_spec(file_name, contents):
+ # Sorted by this function (in pseudo-Rust). The idea is to group the same
+ # organization together.
+ # fn sort_standards(a: &Url, b: &Url) -> Ordering {
+ # let a_domain = a.domain().split(".");
+ # a_domain.pop();
+ # a_domain.reverse();
+ # let b_domain = b.domain().split(".");
+ # b_domain.pop();
+ # b_domain.reverse();
+ # for i in a_domain.into_iter().zip(b_domain.into_iter()) {
+ # match i.0.cmp(b.0) {
+ # Less => return Less,
+ # Greater => return Greater,
+ # _ => (),
+ # }
+ # }
+ # a_domain.path().cmp(b_domain.path())
+ # }
+ if not file_name.endswith(".webidl"):
+ raise StopIteration
+ standards = [
+ "//www.khronos.org/registry/webgl/specs",
+ "//developer.mozilla.org/en-US/docs/Web/API",
+ "//dev.w3.org/2006/webapi",
+ "//dev.w3.org/csswg",
+ "//dev.w3.org/fxtf",
+ "//dvcs.w3.org/hg",
+ "//dom.spec.whatwg.org",
+ "//domparsing.spec.whatwg.org",
+ "//drafts.csswg.org/cssom",
+ "//drafts.fxtf.org",
+ "//encoding.spec.whatwg.org",
+ "//html.spec.whatwg.org",
+ "//url.spec.whatwg.org",
+ "//xhr.spec.whatwg.org",
+ "//w3c.github.io",
+ "//heycam.github.io/webidl",
+ "//webbluetoothcg.github.io/web-bluetooth/",
+ # Not a URL
+ "// This interface is entirely internal to Servo, and should not be" +
+ " accessible to\n// web pages."
+ ]
+ for i in standards:
+ if contents.find(i) != -1:
+ raise StopIteration
+ yield 0, "No specification link found."
+
+
+def check_json(filename, contents):
+ if not filename.endswith(".json"):
+ raise StopIteration
+
+ try:
+ json.loads(contents)
+ except ValueError as e:
+ match = re.search(r"line (\d+) ", e.message)
+ line_no = match and match.group(1)
+ yield (line_no, e.message)
+
+
+def check_spec(file_name, lines):
+ base_path = "components/script/dom/"
+ if base_path not in file_name:
+ raise StopIteration
+ file_name = os.path.relpath(os.path.splitext(file_name)[0], base_path)
+ patt = re.compile("^\s*\/\/.+")
+
+ # Pattern representing a line with a macro
+ macro_patt = re.compile("^\s*\S+!(.*)$")
+
+ # Pattern representing a line with comment containing a spec link
+ link_patt = re.compile("^\s*///? https://.+$")
+
+ # Pattern representing a line with comment
+ comment_patt = re.compile("^\s*///?.+$")
+
+ pattern = "impl {}Methods for {} {{".format(file_name, file_name)
+ brace_count = 0
+ in_impl = False
+ for idx, line in enumerate(lines):
+ if "// check-tidy: no specs after this line" in line:
+ break
+ if not patt.match(line):
+ if pattern.lower() in line.lower():
+ in_impl = True
+ if ("fn " in line or macro_patt.match(line)) and brace_count == 1:
+ for up_idx in range(1, idx + 1):
+ up_line = lines[idx - up_idx]
+ if link_patt.match(up_line):
+ # Comment with spec link exists
+ break
+ if not comment_patt.match(up_line):
+ # No more comments exist above, yield warning
+ yield (idx + 1, "method declared in webidl is missing a comment with a specification link")
+ break
+ if '{' in line and in_impl:
+ brace_count += 1
+ if '}' in line and in_impl:
+ if brace_count == 1:
+ break
+ brace_count -= 1
+
+
+def collect_errors_for_files(files_to_check, checking_functions, line_checking_functions):
+ (has_element, files_to_check) = is_iter_empty(files_to_check)
+ if not has_element:
+ raise StopIteration
+ print '\rChecking files for tidiness...'
+ for filename in files_to_check:
+ with open(filename, "r") as f:
+ contents = f.read()
+ for check in checking_functions:
+ for error in check(filename, contents):
+ # the result will be: `(filename, line, message)`
+ yield (filename,) + error
+ lines = contents.splitlines(True)
+ for check in line_checking_functions:
+ for error in check(filename, lines):
+ yield (filename,) + error
+
+
+def get_wpt_files(only_changed_files, progress):
+ wpt_dir = os.path.join(".", "tests", "wpt", "web-platform-tests" + os.sep)
+ file_iter = get_file_list(os.path.join(wpt_dir), only_changed_files)
+ (has_element, file_iter) = is_iter_empty(file_iter)
+ if not has_element:
+ raise StopIteration
+ print '\nRunning the WPT lint...'
+ if progress:
+ file_iter = progress_wrapper(file_iter)
+ for f in file_iter:
+ if filter_file(f):
+ yield f[len(wpt_dir):]
+
+
+def check_wpt_lint_errors(files):
+ wpt_working_dir = os.path.abspath(os.path.join(".", "tests", "wpt", "web-platform-tests"))
+ site.addsitedir(wpt_working_dir)
+ from tools.lint import lint
+ returncode = lint.lint(files)
+ if returncode:
+ yield ("WPT Lint Tool", "", "lint error(s) in Web Platform Tests: exit status {0}".format(returncode))
+
+
+def get_file_list(directory, only_changed_files=False, exclude_dirs=[]):
+ if only_changed_files:
+ # only check the files that have been changed since the last merge
+ args = ["git", "log", "-n1", "--author=bors-servo", "--format=%H"]
+ last_merge = subprocess.check_output(args).strip()
+ args = ["git", "diff", "--name-only", last_merge, directory]
+ file_list = subprocess.check_output(args)
+ # also check untracked files
+ args = ["git", "ls-files", "--others", "--exclude-standard", directory]
+ file_list += subprocess.check_output(args)
+ for f in file_list.splitlines():
+ yield os.path.join('.', f)
+ elif exclude_dirs:
+ for root, dirs, files in os.walk(directory, topdown=True):
+ # modify 'dirs' in-place so that we don't do unwanted traversals in excluded directories
+ dirs[:] = [d for d in dirs if not any(os.path.join(root, d).startswith(name) for name in ignored_dirs)]
+ for rel_path in files:
+ yield os.path.join(root, rel_path)
+ else:
+ for root, _, files in os.walk(directory):
+ for f in files:
+ yield os.path.join(root, f)
+
+
+def scan(faster=False, progress=True):
+ # standard checks
+ files_to_check = filter_files('.', faster, progress)
+ checking_functions = (check_flake8, check_lock, check_webidl_spec, check_json)
+ line_checking_functions = (check_license, check_by_line, check_toml, check_rust, check_spec)
+ errors = collect_errors_for_files(files_to_check, checking_functions, line_checking_functions)
+ # wpt lint checks
+ wpt_lint_errors = check_wpt_lint_errors(get_wpt_files(faster, progress))
+ # collect errors
+ errors = itertools.chain(errors, wpt_lint_errors)
+ error = None
+ for error in errors:
+ print "\r\033[94m{}\033[0m:\033[93m{}\033[0m: \033[91m{}\033[0m".format(*error)
+ if error is None:
+ print "\n\033[92mtidy reported no errors.\033[0m"
+ return int(error is not None)