forked from mirrors/gecko-dev
		
	 de29bc2f0e
			
		
	
	
		de29bc2f0e
		
	
	
	
	
		
			
			# ignore-this-changeset Differential Revision: https://phabricator.services.mozilla.com/D162660
		
			
				
	
	
		
			237 lines
		
	
	
	
		
			7.4 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			237 lines
		
	
	
	
		
			7.4 KiB
		
	
	
	
		
			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/.
 | |
| 
 | |
| import os
 | |
| import re
 | |
| import signal
 | |
| import sys
 | |
| import xml.etree.ElementTree as ET
 | |
| 
 | |
| from mozboot.util import get_tools_dir
 | |
| from mozlint import result
 | |
| from mozlint.pathutils import expand_exclusions
 | |
| from mozprocess import ProcessHandler
 | |
| 
 | |
| CLANG_FORMAT_NOT_FOUND = """
 | |
| Could not find clang-format! It should've been installed automatically - \
 | |
| please report a bug here:
 | |
| https://bugzilla.mozilla.org/enter_bug.cgi?product=Firefox%20Build%20System&component=Lint%20and%20Formatting
 | |
| """.strip()
 | |
| 
 | |
| 
 | |
| def setup(root, mach_command_context, **lintargs):
 | |
|     if get_clang_format_binary():
 | |
|         return 0
 | |
| 
 | |
|     from mozbuild.code_analysis.mach_commands import get_clang_tools
 | |
| 
 | |
|     rc, _ = get_clang_tools(mach_command_context)
 | |
|     if rc:
 | |
|         return 1
 | |
| 
 | |
| 
 | |
| class ClangFormatProcess(ProcessHandler):
 | |
|     def __init__(self, config, *args, **kwargs):
 | |
|         self.config = config
 | |
|         kwargs["stream"] = False
 | |
|         kwargs["universal_newlines"] = True
 | |
|         ProcessHandler.__init__(self, *args, **kwargs)
 | |
| 
 | |
|     def run(self, *args, **kwargs):
 | |
|         orig = signal.signal(signal.SIGINT, signal.SIG_IGN)
 | |
|         ProcessHandler.run(self, *args, **kwargs)
 | |
|         signal.signal(signal.SIGINT, orig)
 | |
| 
 | |
| 
 | |
| def run_process(config, cmd):
 | |
|     proc = ClangFormatProcess(config, cmd)
 | |
|     proc.run()
 | |
|     try:
 | |
|         proc.wait()
 | |
|     except KeyboardInterrupt:
 | |
|         proc.kill()
 | |
| 
 | |
|     return proc.output
 | |
| 
 | |
| 
 | |
| def get_clang_format_binary():
 | |
|     """
 | |
|     Returns the path of the first clang-format binary available
 | |
|     if not found returns None
 | |
|     """
 | |
|     binary = os.environ.get("CLANG_FORMAT")
 | |
|     if binary:
 | |
|         return binary
 | |
| 
 | |
|     clang_tools_path = os.path.join(get_tools_dir(), "clang-tools")
 | |
|     bin_path = os.path.join(clang_tools_path, "clang-tidy", "bin")
 | |
|     binary = os.path.join(bin_path, "clang-format")
 | |
| 
 | |
|     if sys.platform.startswith("win"):
 | |
|         binary += ".exe"
 | |
| 
 | |
|     if not os.path.isfile(binary):
 | |
|         return None
 | |
| 
 | |
|     return binary
 | |
| 
 | |
| 
 | |
| def is_ignored_path(ignored_dir_re, topsrcdir, f):
 | |
|     # Remove up to topsrcdir in pathname and match
 | |
|     if f.startswith(topsrcdir + "/"):
 | |
|         match_f = f[len(topsrcdir + "/") :]
 | |
|     else:
 | |
|         match_f = f
 | |
|     return re.match(ignored_dir_re, match_f)
 | |
| 
 | |
| 
 | |
| def remove_ignored_path(paths, topsrcdir, log):
 | |
|     path_to_third_party = os.path.join(topsrcdir, ".clang-format-ignore")
 | |
| 
 | |
|     ignored_dir = []
 | |
|     with open(path_to_third_party, "r") as fh:
 | |
|         for line in fh:
 | |
|             # In case it starts with a space
 | |
|             line = line.strip()
 | |
|             # Remove comments and empty lines
 | |
|             if line.startswith("#") or len(line) == 0:
 | |
|                 continue
 | |
|             # The regexp is to make sure we are managing relative paths
 | |
|             ignored_dir.append(r"^[\./]*" + line.rstrip())
 | |
| 
 | |
|     # Generates the list of regexp
 | |
|     ignored_dir_re = "(%s)" % "|".join(ignored_dir)
 | |
| 
 | |
|     path_list = []
 | |
|     for f in paths:
 | |
|         if is_ignored_path(ignored_dir_re, topsrcdir, f):
 | |
|             # Early exit if we have provided an ignored directory
 | |
|             log.debug("Ignored third party code '{0}'".format(f))
 | |
|             continue
 | |
|         path_list.append(f)
 | |
| 
 | |
|     return path_list
 | |
| 
 | |
| 
 | |
| def lint(paths, config, fix=None, **lintargs):
 | |
|     log = lintargs["log"]
 | |
|     paths = list(expand_exclusions(paths, config, lintargs["root"]))
 | |
| 
 | |
|     # We ignored some specific files for a bunch of reasons.
 | |
|     # Not using excluding to avoid duplication
 | |
|     if lintargs.get("use_filters", True):
 | |
|         paths = remove_ignored_path(paths, lintargs["root"], log)
 | |
| 
 | |
|     # An empty path array can occur when the user passes in `-n`. If we don't
 | |
|     # return early in this case, rustfmt will attempt to read stdin and hang.
 | |
|     if not paths:
 | |
|         return []
 | |
| 
 | |
|     binary = get_clang_format_binary()
 | |
| 
 | |
|     if not binary:
 | |
|         print(CLANG_FORMAT_NOT_FOUND)
 | |
|         if "MOZ_AUTOMATION" in os.environ:
 | |
|             return 1
 | |
|         return []
 | |
| 
 | |
|     cmd_args = [binary]
 | |
| 
 | |
|     base_command = cmd_args + ["--version"]
 | |
|     version = run_process(config, base_command)
 | |
|     log.debug("Version: {}".format(version))
 | |
| 
 | |
|     cmd_args.append("--output-replacements-xml")
 | |
|     base_command = cmd_args + paths
 | |
|     log.debug("Command: {}".format(" ".join(cmd_args)))
 | |
|     output = run_process(config, base_command)
 | |
| 
 | |
|     def replacement(parser):
 | |
|         for end, e in parser.read_events():
 | |
|             assert end == "end"
 | |
|             if e.tag == "replacement":
 | |
|                 item = {k: int(v) for k, v in e.items()}
 | |
|                 assert sorted(item.keys()) == ["length", "offset"]
 | |
|                 item["with"] = (e.text or "").encode("utf-8")
 | |
|                 yield item
 | |
| 
 | |
|     # When given multiple paths as input, --output-replacements-xml
 | |
|     # will output one xml per path, in the order they are given, but
 | |
|     # XML parsers don't know how to handle that, so do it manually.
 | |
|     parser = None
 | |
|     replacements = []
 | |
|     for line in output:
 | |
|         if line.startswith("<?xml "):
 | |
|             if parser:
 | |
|                 replacements.append(list(replacement(parser)))
 | |
|             parser = ET.XMLPullParser(["end"])
 | |
|         parser.feed(line)
 | |
|     replacements.append(list(replacement(parser)))
 | |
| 
 | |
|     results = []
 | |
|     fixed = 0
 | |
|     for path, replacement in zip(paths, replacements):
 | |
|         if not replacement:
 | |
|             continue
 | |
|         with open(path, "rb") as fh:
 | |
|             data = fh.read()
 | |
| 
 | |
|         linenos = []
 | |
|         patched_data = b""
 | |
|         last_offset = 0
 | |
|         lineno_before = 1
 | |
|         lineno_after = 1
 | |
| 
 | |
|         for item in replacement:
 | |
|             offset = item["offset"]
 | |
|             length = item["length"]
 | |
|             replace_with = item["with"]
 | |
|             since_last_offset = data[last_offset:offset]
 | |
|             replaced = data[offset : offset + length]
 | |
| 
 | |
|             lines_since_last_offset = since_last_offset.count(b"\n")
 | |
|             lineno_before += lines_since_last_offset
 | |
|             lineno_after += lines_since_last_offset
 | |
|             start_lineno = (lineno_before, lineno_after)
 | |
| 
 | |
|             lineno_before += replaced.count(b"\n")
 | |
|             lineno_after += replace_with.count(b"\n")
 | |
|             end_lineno = (lineno_before, lineno_after)
 | |
| 
 | |
|             if linenos and start_lineno[0] <= linenos[-1][1][0]:
 | |
|                 linenos[-1] = (linenos[-1][0], end_lineno)
 | |
|             else:
 | |
|                 linenos.append((start_lineno, end_lineno))
 | |
| 
 | |
|             patched_data += since_last_offset + replace_with
 | |
|             last_offset = offset + len(replaced)
 | |
|         patched_data += data[last_offset:]
 | |
| 
 | |
|         lines_before = data.decode("utf-8", "replace").splitlines()
 | |
|         lines_after = patched_data.decode("utf-8", "replace").splitlines()
 | |
|         for (start_before, start_after), (end_before, end_after) in linenos:
 | |
|             diff = "".join(
 | |
|                 "-" + l + "\n" for l in lines_before[start_before - 1 : end_before]
 | |
|             )
 | |
|             diff += "".join(
 | |
|                 "+" + l + "\n" for l in lines_after[start_after - 1 : end_after]
 | |
|             )
 | |
| 
 | |
|             results.append(
 | |
|                 result.from_config(
 | |
|                     config,
 | |
|                     path=path,
 | |
|                     diff=diff,
 | |
|                     level="warning",
 | |
|                     lineno=start_before,
 | |
|                     column=0,
 | |
|                 )
 | |
|             )
 | |
| 
 | |
|         if fix:
 | |
|             with open(path, "wb") as fh:
 | |
|                 fh.write(patched_data)
 | |
|             fixed += len(linenos)
 | |
| 
 | |
|     return {"results": results, "fixed": fixed}
 |