aboutsummaryrefslogtreecommitdiffstats
path: root/python/tidy/servo_tidy
diff options
context:
space:
mode:
authorMartin Robinson <mrobinson@igalia.com>2023-06-15 12:34:27 +0200
committerMartin Robinson <mrobinson@igalia.com>2023-06-15 13:10:06 +0200
commit81433a8684d078ae629ba24f30f877028e361136 (patch)
treec974ac6f5741138f4926e02ca6a3f35abfc46738 /python/tidy/servo_tidy
parentfa266abd29688833e1ffa5995230e5a6c30161f6 (diff)
downloadservo-81433a8684d078ae629ba24f30f877028e361136.tar.gz
servo-81433a8684d078ae629ba24f30f877028e361136.zip
Convert tidy to a non-egg Python package
It seems that servo-tidy is only used by webrender in my GitHub searches. WebRender could simply use `rustfmt` and the tidy on pypi hasn't been updated since 2018. Converting tidy to a normal Python package removes the maintenance burden of continually fixing the easy install configuration. Fixes #29094. Fixes #29334.
Diffstat (limited to 'python/tidy/servo_tidy')
-rw-r--r--python/tidy/servo_tidy/__init__.py8
-rw-r--r--python/tidy/servo_tidy/licenseck.py73
-rw-r--r--python/tidy/servo_tidy/tidy.py1164
3 files changed, 0 insertions, 1245 deletions
diff --git a/python/tidy/servo_tidy/__init__.py b/python/tidy/servo_tidy/__init__.py
deleted file mode 100644
index 6b6351ddd2b..00000000000
--- a/python/tidy/servo_tidy/__init__.py
+++ /dev/null
@@ -1,8 +0,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.
diff --git a/python/tidy/servo_tidy/licenseck.py b/python/tidy/servo_tidy/licenseck.py
deleted file mode 100644
index deeaeb55333..00000000000
--- a/python/tidy/servo_tidy/licenseck.py
+++ /dev/null
@@ -1,73 +0,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.
-
-OLD_MPL = """\
-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/.\
-"""
-
-MPL = """\
-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/.\
-"""
-
-APACHE = """\
-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 = [
- "See the COPYRIGHT file at the top-level directory of this distribution",
- "See http://rust-lang.org/COPYRIGHT",
-]
-
-# The valid licenses, in the form we'd expect to see them in a Cargo.toml file.
-licenses_toml = [
- 'license = "MPL-2.0"',
- 'license = "MIT/Apache-2.0"',
- 'license = "MIT OR Apache-2.0"',
-]
-
-# The valid dependency licenses, in the form we'd expect to see them in a Cargo.toml file.
-licenses_dep_toml = [
- # Licenses that are compatible with Servo's licensing
- 'license = "Apache-2 / MIT"',
- 'license = "Apache-2.0 / MIT"',
- 'license = "Apache-2.0"',
- 'license = "Apache-2.0/MIT"',
- 'license = "BSD-2-Clause"',
- 'license = "BSD-3-Clause"',
- 'license = "BSD-3-Clause/MIT"',
- 'license = "CC0-1.0"',
- 'license = "ISC"',
- 'license = "MIT / Apache-2.0"',
- 'license = "MIT OR Apache-2.0"',
- 'license = "MIT"',
- 'license = "MIT/Apache-2.0"',
- 'license = "MPL-2.0"',
- 'license = "Unlicense/MIT"',
- 'license = "zlib-acknowledgement"',
- 'license-file = "LICENSE-MIT"',
- 'license= "MIT / Apache-2.0"',
- # Whitelisted crates whose licensing has been checked manually
- 'name = "device"',
- 'name = "dylib"',
- 'name = "ipc-channel"',
- 'name = "mozjs_sys"',
- 'name = "freetype"',
- 'name = "js"',
- 'name = "servo-freetype-sys"',
- 'name = "webrender"',
- 'name = "webrender_api"',
-]
diff --git a/python/tidy/servo_tidy/tidy.py b/python/tidy/servo_tidy/tidy.py
deleted file mode 100644
index 7c45bb63d93..00000000000
--- a/python/tidy/servo_tidy/tidy.py
+++ /dev/null
@@ -1,1164 +0,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.
-
-from __future__ import print_function
-
-import fnmatch
-import glob
-import imp
-import itertools
-import json
-import os
-import re
-import subprocess
-import sys
-
-import colorama
-import toml
-import voluptuous
-import yaml
-from .licenseck import OLD_MPL, MPL, APACHE, COPYRIGHT, licenses_toml, licenses_dep_toml
-topdir = os.path.abspath(os.path.dirname(sys.argv[0]))
-wpt = os.path.join(topdir, "tests", "wpt")
-
-
-def wpt_path(*args):
- return os.path.join(wpt, *args)
-
-
-CONFIG_FILE_PATH = os.path.join(".", "servo-tidy.toml")
-WPT_MANIFEST_PATH = wpt_path("include.ini")
-# regex source https://stackoverflow.com/questions/6883049/
-URL_REGEX = re.compile(br'https?://(?:[-\w.]|(?:%[\da-fA-F]{2}))+')
-
-# Import wptmanifest only when we do have wpt in tree, i.e. we're not
-# inside a Firefox checkout.
-if os.path.isfile(WPT_MANIFEST_PATH):
- sys.path.append(wpt_path("web-platform-tests", "tools", "wptrunner", "wptrunner"))
- from wptmanifest import parser, node
-
-# Default configs
-config = {
- "skip-check-length": False,
- "skip-check-licenses": False,
- "check-alphabetical-order": True,
- "check-ordered-json-keys": [],
- "lint-scripts": [],
- "blocked-packages": {},
- "ignore": {
- "files": [
- os.path.join(".", "."), # ignore hidden files
- ],
- "directories": [
- os.path.join(".", "."), # ignore hidden directories
- ],
- "packages": [],
- },
- "check_ext": {}
-}
-
-COMMENTS = [b"// ", b"# ", b" *", b"/* "]
-
-# File patterns to include in the non-WPT tidy check.
-FILE_PATTERNS_TO_CHECK = ["*.rs", "*.rc", "*.cpp", "*.c",
- "*.h", "*.py", "*.sh",
- "*.toml", "*.webidl", "*.json", "*.html",
- "*.yml"]
-
-# File patterns that are ignored for all tidy and lint checks.
-FILE_PATTERNS_TO_IGNORE = ["*.#*", "*.pyc", "fake-ld.sh", "*.ogv", "*.webm"]
-
-SPEC_BASE_PATH = "components/script/dom/"
-
-WEBIDL_STANDARDS = [
- b"//www.khronos.org/registry/webgl/extensions",
- b"//www.khronos.org/registry/webgl/specs",
- b"//developer.mozilla.org/en-US/docs/Web/API",
- b"//dev.w3.org/2006/webapi",
- b"//dev.w3.org/csswg",
- b"//dev.w3.org/fxtf",
- b"//dvcs.w3.org/hg",
- b"//dom.spec.whatwg.org",
- b"//drafts.csswg.org",
- b"//drafts.css-houdini.org",
- b"//drafts.fxtf.org",
- b"//console.spec.whatwg.org",
- b"//encoding.spec.whatwg.org",
- b"//fetch.spec.whatwg.org",
- b"//html.spec.whatwg.org",
- b"//url.spec.whatwg.org",
- b"//xhr.spec.whatwg.org",
- b"//w3c.github.io",
- b"//heycam.github.io/webidl",
- b"//webbluetoothcg.github.io/web-bluetooth/",
- b"//svgwg.org/svg2-draft",
- b"//wicg.github.io",
- b"//webaudio.github.io",
- b"//immersive-web.github.io/",
- b"//github.com/immersive-web/webxr-test-api/",
- b"//github.com/immersive-web/webxr-hands-input/",
- b"//gpuweb.github.io",
- # Not a URL
- b"// This interface is entirely internal to Servo, and should not be"
- + b" accessible to\n// web pages."
-]
-
-
-def is_iter_empty(iterator):
- try:
- obj = next(iterator)
- return True, itertools.chain((obj,), iterator)
- except StopIteration:
- return False, iterator
-
-
-def normilize_paths(paths):
- if isinstance(paths, str):
- return os.path.join(*paths.split('/'))
- else:
- return [os.path.join(*path.split('/')) for path in paths]
-
-
-# A simple wrapper for iterators to show progress
-# (Note that it's inefficient for giant iterators, since it iterates once to get the upper bound)
-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
-
-
-class FileList(object):
- def __init__(self, directory, only_changed_files=False, exclude_dirs=[], progress=True):
- self.directory = directory
- self.excluded = exclude_dirs
- self.generator = self._filter_excluded() if exclude_dirs else self._default_walk()
- if only_changed_files:
- self.generator = self._git_changed_files()
- if progress:
- self.generator = progress_wrapper(self.generator)
-
- def _default_walk(self):
- for root, _, files in os.walk(self.directory):
- for f in files:
- yield os.path.join(root, f)
-
- def _git_changed_files(self):
- args = ["git", "log", "-n1", "--merges", "--format=%H"]
- last_merge = subprocess.check_output(args, universal_newlines=True).strip()
- if not last_merge:
- return
-
- args = ["git", "diff", "--name-only", last_merge, self.directory]
- file_list = normilize_paths(subprocess.check_output(args, universal_newlines=True).splitlines())
-
- for f in file_list:
- if not any(os.path.join('.', os.path.dirname(f)).startswith(path) for path in self.excluded):
- yield os.path.join('.', f)
-
- def _filter_excluded(self):
- for root, dirs, files in os.walk(self.directory, topdown=True):
- # modify 'dirs' in-place so that we don't do unnecessary traversals in excluded directories
- dirs[:] = [d for d in dirs if not any(os.path.join(root, d).startswith(name) for name in self.excluded)]
- for rel_path in files:
- yield os.path.join(root, rel_path)
-
- def __iter__(self):
- return self.generator
-
- def next(self):
- return next(self.generator)
-
-
-def filter_file(file_name):
- if any(file_name.startswith(ignored_file) for ignored_file in config["ignore"]["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, only_changed_files, progress):
- file_iter = FileList(start_dir, only_changed_files=only_changed_files,
- exclude_dirs=config["ignore"]["directories"], progress=progress)
- # always yield Cargo.lock so that the correctness of transitive dependencies is checked
- yield "./Cargo.lock"
-
- for file_name in iter(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
-
-
-def uncomment(line):
- for c in COMMENTS:
- if line.startswith(c):
- if line.endswith(b"*/"):
- return line[len(c):(len(line) - 3)].strip()
- return line[len(c):].strip()
-
-
-def is_apache_licensed(header):
- if APACHE in header:
- return any(c in header for c in COPYRIGHT)
-
-
-def check_license(file_name, lines):
- if any(file_name.endswith(ext) for ext in (".yml", ".toml", ".lock", ".json", ".html")) or \
- config["skip-check-licenses"]:
- return
-
- if lines[0].startswith(b"#!") and lines[1].strip():
- yield (1, "missing blank line after shebang")
-
- blank_lines = 0
- max_blank_lines = 2 if lines[0].startswith(b"#!") else 1
- license_block = []
-
- for line in lines:
- line = line.rstrip(b'\n')
- if not line.strip():
- blank_lines += 1
- if blank_lines >= max_blank_lines:
- break
- continue
- line = uncomment(line)
- if line is not None:
- license_block.append(line)
-
- header = (b" ".join(license_block)).decode("utf-8")
- valid_license = OLD_MPL in header or MPL in header or is_apache_licensed(header)
- acknowledged_bad_license = "xfail-license" in header
- if not (valid_license or acknowledged_bad_license):
- yield (1, "incorrect license")
-
-
-def check_modeline(file_name, lines):
- for idx, line in enumerate(lines[:5]):
- if re.search(b'^.*[ \t](vi:|vim:|ex:)[ \t]', line):
- yield (idx + 1, "vi modeline present")
- elif re.search(br'-\*-.*-\*-', line, re.IGNORECASE):
- yield (idx + 1, "emacs file variables present")
-
-
-def check_length(file_name, idx, line):
- if any(file_name.endswith(ext) for ext in (".yml", ".lock", ".json", ".html", ".toml")) or \
- config["skip-check-length"]:
- return
-
- # Prefer shorter lines when shell scripting.
- max_length = 80 if file_name.endswith(".sh") else 120
- if len(line.rstrip(b'\n')) > max_length and not is_unsplittable(file_name, line):
- yield (idx + 1, "Line is longer than %d characters" % max_length)
-
-
-def contains_url(line):
- return bool(URL_REGEX.search(line))
-
-
-def is_unsplittable(file_name, line):
- return (
- contains_url(line)
- or file_name.endswith(".rs")
- and line.startswith(b"use ")
- and b"{" not in line
- )
-
-
-def check_whatwg_specific_url(idx, line):
- match = re.search(br"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(br"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.endswith(b"\n"):
- line = line[:-1]
- else:
- yield (idx + 1, "no newline at EOF")
-
- if line.endswith(b" "):
- yield (idx + 1, "trailing whitespace")
-
- if b"\t" in line:
- yield (idx + 1, "tab on line")
-
- if b"\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):
- if not file_name.endswith(".py"):
- return
-
- ignore = {
- "W291", # trailing whitespace; the standard tidy process will enforce no trailing whitespace
- "W503", # linebreak before binary operator; replaced by W504 - linebreak after binary operator
- "E501", # 80 character line length; the standard tidy process will enforce line length
- }
-
- output = ""
- try:
- args = ["flake8", "--ignore=" + ",".join(ignore), file_name]
- subprocess.check_output(args, universal_newlines=True)
- except subprocess.CalledProcessError as e:
- output = e.output
- for error in output.splitlines():
- _, line_num, _, message = error.split(":", 3)
- yield line_num, message.strip()
-
-
-def check_lock(file_name, contents):
- def find_reverse_dependencies(name, content):
- for package in itertools.chain([content.get("root", {})], content["package"]):
- for dependency in package.get("dependencies", []):
- parts = dependency.split()
- dependency = (parts[0], parts[1] if len(parts) > 1 else None, parts[2] if len(parts) > 2 else None)
- if dependency[0] == name:
- yield package["name"], package["version"], dependency
-
- if not file_name.endswith(".lock"):
- return
-
- # Package names to be neglected (as named by cargo)
- exceptions = config["ignore"]["packages"]
-
- content = toml.loads(contents.decode("utf-8"))
-
- packages_by_name = {}
- for package in content.get("package", []):
- if "replace" in package:
- continue
- source = package.get("source", "")
- if source == r"registry+https://github.com/rust-lang/crates.io-index":
- source = "crates.io"
- packages_by_name.setdefault(package["name"], []).append((package["version"], source))
-
- for name in exceptions:
- if name not in packages_by_name:
- yield (1, "duplicates are allowed for `{}` but it is not a dependency".format(name))
-
- for (name, packages) in packages_by_name.items():
- has_duplicates = len(packages) > 1
- duplicates_allowed = name in exceptions
-
- if has_duplicates == duplicates_allowed:
- continue
-
- if duplicates_allowed:
- message = 'duplicates for `{}` are allowed, but only single version found'.format(name)
- else:
- message = "duplicate versions for package `{}`".format(name)
-
- packages.sort()
- packages_dependencies = list(find_reverse_dependencies(name, content))
- for version, source in packages:
- short_source = source.split("#")[0].replace("git+", "")
- message += "\n\t\033[93mThe following packages depend on version {} from '{}':\033[0m" \
- .format(version, short_source)
- for pname, package_version, dependency in packages_dependencies:
- if (not dependency[1] or version in dependency[1]) and \
- (not dependency[2] or short_source in dependency[2]):
- message += "\n\t\t" + pname + " " + package_version
- yield (1, message)
-
- # Check to see if we are transitively using any blocked packages
- blocked_packages = config["blocked-packages"]
- # Create map to keep track of visited exception packages
- visited_whitelisted_packages = {package: {} for package in blocked_packages.keys()}
-
- for package in content.get("package", []):
- package_name = package.get("name")
- package_version = package.get("version")
- for dependency in package.get("dependencies", []):
- dependency = dependency.split()
- dependency_name = dependency[0]
- whitelist = blocked_packages.get(dependency_name)
- if whitelist is not None:
- if package_name not in whitelist:
- fmt = "Package {} {} depends on blocked package {}."
- message = fmt.format(package_name, package_version, dependency_name)
- yield (1, message)
- else:
- visited_whitelisted_packages[dependency_name][package_name] = True
-
- # Check if all the exceptions to blocked packages actually depend on the blocked package
- for dependency_name, package_names in blocked_packages.items():
- for package_name in package_names:
- if not visited_whitelisted_packages[dependency_name].get(package_name):
- fmt = "Package {} is not required to be an exception of blocked package {}."
- message = fmt.format(package_name, dependency_name)
- yield (1, message)
-
-
-def check_toml(file_name, lines):
- if not file_name.endswith("Cargo.toml"):
- return
- ok_licensed = False
- for idx, line in enumerate(map(lambda line: line.decode("utf-8"), lines)):
- if idx == 0 and "[workspace]" in line:
- return
- line_without_comment, _, _ = line.partition("#")
- if line_without_comment.find("*") != -1:
- yield (idx + 1, "found asterisk instead of minimum version number")
- for license_line in licenses_toml:
- ok_licensed |= (license_line in line)
- if not ok_licensed:
- yield (0, ".toml file should contain a valid license.")
-
-
-def check_shell(file_name, lines):
- if not file_name.endswith(".sh"):
- return
-
- shebang = "#!/usr/bin/env bash"
- required_options = ["set -o errexit", "set -o nounset", "set -o pipefail"]
-
- did_shebang_check = False
-
- if not lines:
- yield (0, 'script is an empty file')
- return
-
- if lines[0].rstrip() != shebang.encode("utf-8"):
- yield (1, 'script does not have shebang "{}"'.format(shebang))
-
- for idx, line in enumerate(map(lambda line: line.decode("utf-8"), lines[1:])):
- stripped = line.rstrip()
- # Comments or blank lines are ignored. (Trailing whitespace is caught with a separate linter.)
- if line.startswith("#") or stripped == "":
- continue
-
- if not did_shebang_check:
- if stripped in required_options:
- required_options.remove(stripped)
- else:
- # The first non-comment, non-whitespace, non-option line is the first "real" line of the script.
- # The shebang, options, etc. must come before this.
- if required_options:
- formatted = ['"{}"'.format(opt) for opt in required_options]
- yield (idx + 1, "script is missing options {}".format(", ".join(formatted)))
- did_shebang_check = True
-
- if "`" in stripped:
- yield (idx + 1, "script should not use backticks for command substitution")
-
- if " [ " in stripped or stripped.startswith("[ "):
- yield (idx + 1, "script should use `[[` instead of `[` for conditional testing")
-
- for dollar in re.finditer(r'\$', stripped):
- next_idx = dollar.end()
- if next_idx < len(stripped):
- next_char = stripped[next_idx]
- if not (next_char == '{' or next_char == '('):
- yield(idx + 1, "variable substitutions should use the full \"${VAR}\" form")
-
-
-def rec_parse(current_path, root_node):
- dirs = []
- for item in root_node.children:
- if isinstance(item, node.DataNode):
- next_depth = os.path.join(current_path, item.data)
- dirs.append(next_depth)
- dirs += rec_parse(next_depth, item)
- return dirs
-
-
-def check_manifest_dirs(config_file, print_text=True):
- if not os.path.exists(config_file):
- yield(config_file, 0, "%s manifest file is required but was not found" % config_file)
- return
-
- # Load configs from include.ini
- with open(config_file, "rb") as content:
- conf_file = content.read()
- lines = conf_file.splitlines(True)
-
- if print_text:
- print('\rChecking the wpt manifest file...')
-
- p = parser.parse(lines)
- paths = rec_parse(wpt_path("web-platform-tests"), p)
- for idx, path in enumerate(paths):
- if '_mozilla' in path or '_webgl' in path or '_webgpu' in path:
- continue
- if not os.path.isdir(path):
- yield(config_file, idx + 1, "Path in manifest was not found: {}".format(path))
-
-
-def check_rust(file_name, lines):
- if not file_name.endswith(".rs") or \
- file_name.endswith(".mako.rs") or \
- file_name.endswith(os.path.join("style", "build.rs")) or \
- file_name.endswith(os.path.join("unit", "style", "stylesheets.rs")):
- return
-
- comment_depth = 0
- merged_lines = ''
- import_block = False
- whitespace = False
-
- is_lib_rs_file = file_name.endswith("lib.rs")
-
- PANIC_NOT_ALLOWED_PATHS = [
- os.path.join("*", "components", "compositing", "compositor.rs"),
- os.path.join("*", "components", "constellation", "*"),
- os.path.join("*", "ports", "winit", "headed_window.rs"),
- os.path.join("*", "ports", "winit", "headless_window.rs"),
- os.path.join("*", "ports", "winit", "embedder.rs"),
- os.path.join("*", "rust_tidy.rs"), # This is for the tests.
- ]
- is_panic_not_allowed_rs_file = any([
- glob.fnmatch.fnmatch(file_name, path) for path in PANIC_NOT_ALLOWED_PATHS])
-
- prev_open_brace = False
- multi_line_string = False
- prev_crate = {}
- prev_mod = {}
- prev_feature_name = ""
- indent = 0
-
- check_alphabetical_order = config["check-alphabetical-order"]
- decl_message = "{} is not in alphabetical order"
- decl_expected = "\n\t\033[93mexpected: {}\033[0m"
- decl_found = "\n\t\033[91mfound: {}\033[0m"
- panic_message = "unwrap() or panic!() found in code which should not panic."
-
- for idx, original_line in enumerate(map(lambda line: line.decode("utf-8"), lines)):
- # simplify the analysis
- line = original_line.strip()
- indent = len(original_line) - len(line)
-
- is_attribute = re.search(r"#\[.*\]", line)
- is_comment = re.search(r"^//|^/\*|^\*", line)
-
- # 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 = ''
-
- if multi_line_string:
- line, count = re.subn(
- r'^(\\.|[^"\\])*?"', '', line, count=1)
- if count == 1:
- multi_line_string = False
- else:
- continue
-
- # Ignore attributes, comments, and imports
- # Keep track of whitespace to enable checking for a merged import block
- if import_block:
- if not (is_comment or is_attribute 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 is_attribute and not is_comment:
- line = re.sub(r'"(\\.|[^\\"])*?"', '""', line)
- line = re.sub(
- r"'(\\.|[^\\']|(\\x[0-9a-fA-F]{2})|(\\u{[0-9a-fA-F]{1,6}}))'",
- "''", line)
- # If, after parsing all single-line strings, we still have
- # an odd number of double quotes, this line starts a
- # multiline string
- if line.count('"') % 2 == 1:
- line = re.sub(r'"(\\.|[^\\"])*?$', '""', line)
- multi_line_string = True
-
- # get rid of comments
- line = re.sub(r'//.*?$|/\*.*?$|^\*.*?$', '//', line)
-
- # get rid of attributes that do not contain =
- line = re.sub(r'^#[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))
- def no_filter(match, line):
- return True
- regex_rules = [
- # 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),
- # There should be any use of banned types:
- # Cell<JSVal>, Cell<Dom<T>>, DomRefCell<Dom<T>>, DomRefCell<HEAP<T>>
- (r"(\s|:)+Cell<JSVal>", "Banned type Cell<JSVal> detected. Use MutDom<JSVal> instead", no_filter),
- (r"(\s|:)+Cell<Dom<.+>>", "Banned type Cell<Dom<T>> detected. Use MutDom<T> instead", no_filter),
- (r"DomRefCell<Dom<.+>>", "Banned type DomRefCell<Dom<T>> detected. Use MutDom<T> instead", no_filter),
- (r"DomRefCell<Heap<.+>>", "Banned type DomRefCell<Heap<T>> detected. Use MutDom<T> instead", no_filter),
- # No benefit to using &Root<T>
- (r": &Root<", "use &T instead of &Root<T>", no_filter),
- (r": &DomRoot<", "use &T instead of &DomRoot<T>", no_filter),
- (r"^&&", "operators should go at the end of the first line", no_filter),
- # -> () is unnecessary
- (r"-> \(\)", "encountered function signature with -> ()", no_filter),
- ]
-
- for pattern, message, filter_func in regex_rules:
- for match in re.finditer(pattern, line):
- if filter_func(match, line):
- yield (idx + 1, message.format(*match.groups(), **match.groupdict()))
-
- if prev_open_brace and not line:
- yield (idx + 1, "found an empty line following a {")
- prev_open_brace = line.endswith("{")
-
- # 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]
- if indent not in prev_crate:
- prev_crate[indent] = ""
- if prev_crate[indent] > crate_name and check_alphabetical_order:
- 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
-
- if line == "}":
- for i in [i for i in prev_crate.keys() if i > indent]:
- del prev_crate[i]
-
- # check alphabetical order of feature attributes in lib.rs files
- if is_lib_rs_file:
- match = re.search(r"#!\[feature\((.*)\)\]", line)
-
- if match:
- features = list(map(lambda w: w.strip(), match.group(1).split(',')))
- sorted_features = sorted(features)
- if sorted_features != features and check_alphabetical_order:
- yield(idx + 1, decl_message.format("feature attribute")
- + decl_expected.format(tuple(sorted_features))
- + decl_found.format(tuple(features)))
-
- if prev_feature_name > sorted_features[0] and check_alphabetical_order:
- yield(idx + 1, decl_message.format("feature attribute")
- + decl_expected.format(prev_feature_name + " after " + sorted_features[0])
- + decl_found.format(prev_feature_name + " before " + sorted_features[0]))
-
- prev_feature_name = sorted_features[0]
- else:
- # not a feature attribute line, so empty previous name
- prev_feature_name = ""
-
- if is_panic_not_allowed_rs_file:
- match = re.search(r"unwrap\(|panic!\(", line)
- if match:
- yield (idx + 1, panic_message)
-
- # modules must be in the same line and alphabetically sorted
- if line.startswith("mod ") or line.startswith("pub mod "):
- # 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].decode("utf-8"):
- 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 prev_mod[indent] and mod < prev_mod[indent] and check_alphabetical_order:
- 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 = {}
-
- # derivable traits should be alphabetically ordered
- if is_attribute:
- # match the derivable traits filtering out macro expansions
- match = re.search(r"#\[derive\(([a-zA-Z, ]*)", line)
- if match:
- derives = list(map(lambda w: w.strip(), match.group(1).split(',')))
- # sort, compare and report
- sorted_derives = sorted(derives)
- if sorted_derives != derives and check_alphabetical_order:
- yield(idx + 1, decl_message.format("derivable traits list")
- + decl_expected.format(", ".join(sorted_derives))
- + decl_found.format(", ".join(derives)))
-
-
-# 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 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"):
- return
-
- for i in WEBIDL_STANDARDS:
- if contents.find(i) != -1:
- return
- yield (0, "No specification link found.")
-
-
-def duplicate_key_yaml_constructor(loader, node, deep=False):
- mapping = {}
- for key_node, value_node in node.value:
- key = loader.construct_object(key_node, deep=deep)
- if key in mapping:
- raise KeyError(key)
- value = loader.construct_object(value_node, deep=deep)
- mapping[key] = value
- return loader.construct_mapping(node, deep)
-
-
-def lint_buildbot_steps_yaml(mapping):
- from voluptuous import Any, Extra, Required, Schema
-
- # Note: dictionary keys are optional by default in voluptuous
- env = Schema({Extra: str})
- commands = Schema([str])
- schema = Schema({
- 'env': env,
- Extra: Any(
- commands,
- {
- 'env': env,
- Required('commands'): commands,
- },
- ),
- })
-
- # Signals errors via exception throwing
- schema(mapping)
-
-
-class SafeYamlLoader(yaml.SafeLoader):
- """Subclass of yaml.SafeLoader to avoid mutating the global SafeLoader."""
- pass
-
-
-def check_yaml(file_name, contents):
- if not file_name.endswith("buildbot_steps.yml"):
- return
-
- # YAML specification doesn't explicitly disallow
- # duplicate keys, but they shouldn't be allowed in
- # buildbot_steps.yml as it could lead to confusion
- SafeYamlLoader.add_constructor(
- yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
- duplicate_key_yaml_constructor
- )
-
- try:
- contents = yaml.load(contents, Loader=SafeYamlLoader)
- lint_buildbot_steps_yaml(contents)
- except yaml.YAMLError as e:
- line = e.problem_mark.line + 1 if hasattr(e, 'problem_mark') else None
- yield (line, e)
- except KeyError as e:
- yield (None, "Duplicated Key ({})".format(e.args[0]))
- except voluptuous.MultipleInvalid as e:
- yield (None, str(e))
-
-
-def check_for_possible_duplicate_json_keys(key_value_pairs):
- keys = [x[0] for x in key_value_pairs]
- seen_keys = set()
- for key in keys:
- if key in seen_keys:
- raise KeyError("Duplicated Key (%s)" % key)
-
- seen_keys.add(key)
-
-
-def check_for_alphabetical_sorted_json_keys(key_value_pairs):
- for a, b in zip(key_value_pairs[:-1], key_value_pairs[1:]):
- if a[0] > b[0]:
- raise KeyError("Unordered key (found %s before %s)" % (a[0], b[0]))
-
-
-def check_json_requirements(filename):
- def check_fn(key_value_pairs):
- check_for_possible_duplicate_json_keys(key_value_pairs)
- if filename in normilize_paths(config["check-ordered-json-keys"]):
- check_for_alphabetical_sorted_json_keys(key_value_pairs)
- return check_fn
-
-
-def check_json(filename, contents):
- if not filename.endswith(".json"):
- return
-
- try:
- json.loads(contents, object_pairs_hook=check_json_requirements(filename))
- except ValueError as e:
- match = re.search(r"line (\d+) ", e.args[0])
- line_no = match and match.group(1)
- yield (line_no, e.args[0])
- except KeyError as e:
- yield (None, e.args[0])
-
-
-def check_spec(file_name, lines):
- if SPEC_BASE_PATH not in file_name:
- return
- file_name = os.path.relpath(os.path.splitext(file_name)[0], SPEC_BASE_PATH)
- patt = re.compile(r"^\s*\/\/.+")
-
- # Pattern representing a line with a macro
- macro_patt = re.compile(r"^\s*\S+!(.*)$")
-
- # Pattern representing a line with comment containing a spec link
- link_patt = re.compile(r"^\s*///? (<https://.+>|https://.+)$")
-
- # Pattern representing a line with comment or attribute
- comment_patt = re.compile(r"^\s*(///?.+|#\[.+\])$")
-
- brace_count = 0
- in_impl = False
- pattern = "impl {}Methods for {} {{".format(file_name, file_name)
-
- for idx, line in enumerate(map(lambda line: line.decode("utf-8"), 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].decode("utf-8")
- 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_impl:
- brace_count += line.count('{')
- brace_count -= line.count('}')
- if brace_count < 1:
- break
-
-
-def check_config_file(config_file, print_text=True, no_wpt=False):
- # Check if config file exists
- if not os.path.exists(config_file):
- print("%s config file is required but was not found" % config_file)
- sys.exit(1)
-
- # Load configs from servo-tidy.toml
- with open(config_file) as content:
- conf_file = content.read()
- lines = conf_file.splitlines(True)
-
- if print_text:
- print('\rChecking the config file...')
-
- config_content = toml.loads(conf_file)
- exclude = config_content.get("ignore", {})
-
- # Check for invalid listed ignored directories
- exclude_dirs = [d for p in exclude.get("directories", []) for d in (glob.glob(p) or [p])]
- skip_dirs = ["./target", "./tests"]
- invalid_dirs = [d for d in exclude_dirs if not os.path.isdir(d) and not any(s in d for s in skip_dirs)]
-
- # Check for invalid listed ignored files
- invalid_files = [f for f in exclude.get("files", []) if not os.path.exists(f)]
-
- # Do not check for the existense of ignored files under tests/wpts if --no-wpt is used
- if no_wpt:
- wpt_dir = './tests/wpt/'
- invalid_files = [f for f in invalid_files if not os.path.commonprefix([wpt_dir, f]) == wpt_dir]
-
- current_table = ""
- for idx, line in enumerate(lines):
- # Ignore comment lines
- if line.strip().startswith("#"):
- continue
-
- # Check for invalid tables
- if re.match(r"\[(.*?)\]", line.strip()):
- table_name = re.findall(r"\[(.*?)\]", line)[0].strip()
- if table_name not in ("configs", "blocked-packages", "ignore", "check_ext"):
- yield config_file, idx + 1, "invalid config table [%s]" % table_name
- current_table = table_name
- continue
-
- # Print invalid listed ignored directories
- if current_table == "ignore" and invalid_dirs:
- for d in invalid_dirs:
- if line.strip().strip('\'",') == d:
- yield config_file, idx + 1, "ignored directory '%s' doesn't exist" % d
- invalid_dirs.remove(d)
- break
-
- # Print invalid listed ignored files
- if current_table == "ignore" and invalid_files:
- for f in invalid_files:
- if line.strip().strip('\'",') == f:
- yield config_file, idx + 1, "ignored file '%s' doesn't exist" % f
- invalid_files.remove(f)
- break
-
- # Skip if there is no equal sign in line, assuming it's not a key
- if "=" not in line:
- continue
-
- key = line.split("=")[0].strip()
-
- # Check for invalid keys inside [configs] and [ignore] table
- if (current_table == "configs" and key not in config
- or current_table == "ignore" and key not in config["ignore"]
- # Any key outside of tables
- or current_table == ""):
- yield config_file, idx + 1, "invalid config key '%s'" % key
-
- # Parse config file
- parse_config(config_content)
-
-
-def parse_config(config_file):
- exclude = config_file.get("ignore", {})
- # Add list of ignored directories to config
- ignored_directories = [d for p in exclude.get("directories", []) for d in (glob.glob(p) or [p])]
- config["ignore"]["directories"] += normilize_paths(ignored_directories)
- # Add list of ignored files to config
- config["ignore"]["files"] += normilize_paths(exclude.get("files", []))
- # Add list of ignored packages to config
- config["ignore"]["packages"] = exclude.get("packages", [])
-
- # Add dict of dir, list of expected ext to config
- dirs_to_check = config_file.get("check_ext", {})
- # Fix the paths (OS-dependent)
- for path, exts in dirs_to_check.items():
- config['check_ext'][normilize_paths(path)] = exts
-
- # Add list of blocked packages
- config["blocked-packages"] = config_file.get("blocked-packages", {})
-
- # Override default configs
- user_configs = config_file.get("configs", [])
- for pref in user_configs:
- if pref in config:
- config[pref] = user_configs[pref]
-
-
-def check_directory_files(directories, print_text=True):
- if print_text:
- print('\rChecking directories for correct file extensions...')
- for directory, file_extensions in directories.items():
- files = sorted(os.listdir(directory))
- for filename in files:
- if not any(filename.endswith(ext) for ext in file_extensions):
- details = {
- "name": os.path.basename(filename),
- "ext": ", ".join(file_extensions),
- "dir_name": directory
- }
- message = '''Unexpected extension found for {name}. \
-We only expect files with {ext} extensions in {dir_name}'''.format(**details)
- yield (filename, 1, message)
-
-
-def collect_errors_for_files(files_to_check, checking_functions, line_checking_functions, print_text=True):
- (has_element, files_to_check) = is_iter_empty(files_to_check)
- if not has_element:
- return
- if print_text:
- print('\rChecking files for tidiness...')
-
- for filename in files_to_check:
- if not os.path.exists(filename):
- continue
- with open(filename, "rb") as f:
- contents = f.read()
- if not contents.strip():
- yield filename, 0, "file is empty"
- continue
- 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_dep_toml_files(only_changed_files=False):
- if not only_changed_files:
- print('\nRunning the dependency licensing lint...')
- for root, directories, filenames in os.walk(".cargo"):
- for filename in filenames:
- if filename == "Cargo.toml":
- yield os.path.join(root, filename)
-
-
-def check_dep_license_errors(filenames, progress=True):
- filenames = progress_wrapper(filenames) if progress else filenames
- for filename in filenames:
- with open(filename, "r") as f:
- ok_licensed = False
- lines = f.readlines()
- for idx, line in enumerate(lines):
- for license_line in licenses_dep_toml:
- ok_licensed |= (license_line in line)
- if not ok_licensed:
- yield (filename, 0, "dependency should contain a valid license.")
-
-
-class LintRunner(object):
- def __init__(self, lint_path=None, only_changed_files=True,
- exclude_dirs=[], progress=True, stylo=False, no_wpt=False):
- self.only_changed_files = only_changed_files
- self.exclude_dirs = exclude_dirs
- self.progress = progress
- self.path = lint_path
- self.stylo = stylo
- self.no_wpt = no_wpt
-
- def check(self):
- if not os.path.exists(self.path):
- yield (self.path, 0, "file does not exist")
- return
- if not self.path.endswith('.py'):
- yield (self.path, 0, "lint should be a python script")
- return
- dir_name, filename = os.path.split(self.path)
- sys.path.append(dir_name)
- module = imp.load_source(filename[:-3], self.path)
- sys.path.remove(dir_name)
- if not hasattr(module, 'Lint'):
- yield (self.path, 1, "script should contain a class named 'Lint'")
- return
-
- if not issubclass(module.Lint, LintRunner):
- yield (self.path, 1, "class 'Lint' should inherit from 'LintRunner'")
- return
-
- lint = module.Lint(self.path, self.only_changed_files,
- self.exclude_dirs, self.progress, stylo=self.stylo, no_wpt=self.no_wpt)
- for error in lint.run():
- if type(error) is not tuple or (type(error) is tuple and len(error) != 3):
- yield (self.path, 1, "errors should be a tuple of (path, line, reason)")
- return
- yield error
-
- def get_files(self, path, **kwargs):
- args = ['only_changed_files', 'exclude_dirs', 'progress']
- kwargs = {k: kwargs.get(k, getattr(self, k)) for k in args}
- return FileList(path, **kwargs)
-
- def run(self):
- yield (self.path, 0, "class 'Lint' should implement 'run' method")
-
-
-def run_lint_scripts(only_changed_files=False, progress=True, stylo=False, no_wpt=False):
- runner = LintRunner(only_changed_files=only_changed_files, progress=progress, stylo=stylo, no_wpt=no_wpt)
- for path in config['lint-scripts']:
- runner.path = path
- for error in runner.check():
- yield error
-
-
-def scan(only_changed_files=False, progress=True, stylo=False, no_wpt=False):
- # check config file for errors
- config_errors = check_config_file(CONFIG_FILE_PATH, no_wpt=no_wpt)
- # check ini directories exist
- if not no_wpt and os.path.isfile(WPT_MANIFEST_PATH):
- manifest_errors = check_manifest_dirs(WPT_MANIFEST_PATH)
- else:
- manifest_errors = ()
- # check directories contain expected files
- directory_errors = check_directory_files(config['check_ext'])
- # standard checks
- files_to_check = filter_files('.', only_changed_files and not stylo, progress)
- checking_functions = (check_flake8, check_lock, check_webidl_spec, check_json, check_yaml)
- line_checking_functions = (check_license, check_by_line, check_toml, check_shell,
- check_rust, check_spec, check_modeline)
- file_errors = collect_errors_for_files(files_to_check, checking_functions, line_checking_functions)
- # check dependecy licenses
- dep_license_errors = check_dep_license_errors(get_dep_toml_files(only_changed_files), progress)
- # other lint checks
- lint_errors = run_lint_scripts(only_changed_files, progress, stylo=stylo, no_wpt=no_wpt)
- # chain all the iterators
- errors = itertools.chain(config_errors, manifest_errors, directory_errors, lint_errors,
- file_errors, dep_license_errors)
-
- error = None
- colorama.init()
- for error in errors:
- print("\r\033[94m{}\033[0m:\033[93m{}\033[0m: \033[91m{}\033[0m".format(*error))
-
- print()
- if error is None:
- print("\033[92mtidy reported no errors.\033[0m")
-
- return int(error is not None)