diff --git a/tools/lint/lib/checks/frontmatter.py b/tools/lint/lib/checks/frontmatter.py index 197d42c3876..87701e73e1a 100644 --- a/tools/lint/lib/checks/frontmatter.py +++ b/tools/lint/lib/checks/frontmatter.py @@ -25,7 +25,9 @@ def run(self, name, meta, source): for parsing_event in meta.parsing_events: if not isinstance(parsing_event, yaml.ScalarEvent): continue - if parsing_event.style is not None: + # The pure-Python parser reports plain scalars with a style of + # `None`; the libyaml parser uses the empty string. + if parsing_event.style: continue if parsing_event.start_mark.line != parsing_event.end_mark.line: return 'YAML multiline scalar values in flow notation are disallowed (use "|" or ">")' diff --git a/tools/lint/lib/frontmatter.py b/tools/lint/lib/frontmatter.py index 2bfc9aea463..3c01ed598d2 100644 --- a/tools/lint/lib/frontmatter.py +++ b/tools/lint/lib/frontmatter.py @@ -1,23 +1,18 @@ import re import yaml +try: + # The libyaml-backed parser is roughly an order of magnitude faster than + # the pure-Python one, but it is not available in every environment. + from yaml import CSafeLoader as Loader +except ImportError: + from yaml import SafeLoader as Loader + class Result(dict): def __init__(self, meta, events): self.parsing_events = events super(Result, self).__init__(**meta) -class MyLoader(yaml.SafeLoader): - events = None - - def __init__(self, *args, **kwargs): - MyLoader.events = [] - super(MyLoader, self).__init__(*args, **kwargs) - - def get_event(self): - event = super(MyLoader, self).get_event() - MyLoader.events.append(event) - return event - def parse(src): '''Parse the YAML-formatted metadata found in a given string of source code. Tolerate missing or invalid metadata; those conditions are handled by @@ -27,8 +22,13 @@ def parse(src): if not match: return None + # NB: Call strip() to match parseTestRecord. + attrs = match.group(1).strip() + try: - # NB: Call strip() to match parseTestRecord. - return Result(yaml.load(match.group(1).strip(), MyLoader), MyLoader.events) + # The event stream takes a second pass, since the + # libyaml parser cannot be instrumented from Python. + events = list(yaml.parse(attrs, Loader=Loader)) + return Result(yaml.load(attrs, Loader), events) except (yaml.scanner.ScannerError, yaml.parser.ParserError): return None diff --git a/tools/lint/lint.py b/tools/lint/lint.py index 61ddf22d349..28744fd242d 100755 --- a/tools/lint/lint.py +++ b/tools/lint/lint.py @@ -76,6 +76,10 @@ def checks(features): def lint(file_names, features): errors = dict() + # The checks are stateless with respect to the files they inspect, so they + # are created once and reused for every file (some read data files during + # construction, which is expensive to repeat). + all_checks = checks(features) for file_name in file_names: if not file_name.endswith((".js", ".json")): @@ -84,7 +88,7 @@ def lint(file_names, features): with open(file_name, 'r') as f: content = f.read() meta = lib.frontmatter.parse(content) - for check in checks(features): + for check in all_checks: error = check.run(file_name, meta, content) if error is not None: diff --git a/tools/lint/requirements.txt b/tools/lint/requirements.txt index 14ff6344020..f62ce0c56db 100644 --- a/tools/lint/requirements.txt +++ b/tools/lint/requirements.txt @@ -1 +1 @@ -PyYAML==5.1.2 +PyYAML==6.0.3