Commit 22f9a8e1 authored by Sigurd Schneider's avatar Sigurd Schneider Committed by Commit Bot

[tools] Improve locs.py

This CL fixes some style issues and improves json output for the LoC
counting script tools/locs.py.

Notry: true
Change-Id: I0805904e44ab240945ef88dd8214abb8ae02cf7d
Reviewed-on: https://chromium-review.googlesource.com/c/1352271
Commit-Queue: Sigurd Schneider <sigurds@chromium.org>
Reviewed-by: 's avatarMichael Achenbach <machenbach@chromium.org>
Cr-Commit-Position: refs/heads/master@{#57873}
parent de20e6d3
...@@ -7,29 +7,34 @@ ...@@ -7,29 +7,34 @@
Consult --help for more information. Consult --help for more information.
""" """
import argparse
import json import json
import sys
import re
import os import os
import re
import subprocess import subprocess
import sys
import tempfile import tempfile
import time import time
from pathlib import Path from pathlib import Path
import argparse
ARGPARSE = argparse.ArgumentParser( ARGPARSE = argparse.ArgumentParser(
description="A script that computes LoC for a build dir or from a compile_commands.json file", description=("A script that computes LoC for a build dir or from a"
"compile_commands.json file"),
epilog="""Examples: epilog="""Examples:
Count with default settings for build in out/Default: Count with default settings for build in out/Default:
locs.py --build-dir out/Default locs.py --build-dir out/Default
Count with default settings according to given compile_commands file: Count with default settings according to given compile_commands file:
locs.py --compile-commands compile_commands.json locs.py --compile-commands compile_commands.json
Count only a custom group of files settings for build in out/Default: Count only a custom group of files settings for build in out/Default:
tools/locs.py --build-dir out/Default --group src-compiler '\.\./\.\./src/compiler' --only src-compiler tools/locs.py --build-dir out/Default
--group src-compiler '\.\./\.\./src/compiler'
--only src-compiler
Report the 10 files with the worst expansion: Report the 10 files with the worst expansion:
tools/locs.py --build-dir out/Default --worst 10 tools/locs.py --build-dir out/Default --worst 10
Report the 10 files with the worst expansion in src/compiler: Report the 10 files with the worst expansion in src/compiler:
tools/locs.py --build-dir out/Default --worst 10 --group src-compiler '\.\./\.\./src/compiler' --only src-compiler tools/locs.py --build-dir out/Default --worst 10
--group src-compiler '\.\./\.\./src/compiler'
--only src-compiler
Report the 10 largest files after preprocessing: Report the 10 largest files after preprocessing:
tools/locs.py --build-dir out/Default --largest 10 tools/locs.py --build-dir out/Default --largest 10
Report the 10 smallest input files: Report the 10 smallest input files:
...@@ -61,7 +66,7 @@ ARGPARSE.add_argument( ...@@ -61,7 +66,7 @@ ARGPARSE.add_argument(
'--only', '--only',
action='append', action='append',
default=[], default=[],
help="Restrict counting to specific report group (can be passed multiple times)") help="Restrict counting to report group (can be passed multiple times)")
ARGPARSE.add_argument( ARGPARSE.add_argument(
'--not', '--not',
action='append', action='append',
...@@ -117,16 +122,21 @@ def MaxWidth(strings): ...@@ -117,16 +122,21 @@ def MaxWidth(strings):
return max_width return max_width
def GenerateCompileCommandsAndBuild(build_dir, compile_commands_file): def GenerateCompileCommandsAndBuild(build_dir, compile_commands_file, out):
if not os.path.isdir(build_dir): if not os.path.isdir(build_dir):
print("Error: Specified build dir {} is not a directory.".format(build_dir)) print("Error: Specified build dir {} is not a directory.".format(
build_dir), file=sys.stderr)
exit(1) exit(1)
compile_commands_file = "{}/compile_commands.json".format(build_dir) compile_commands_file = "{}/compile_commands.json".format(build_dir)
print("Generating compile commands in {}.".format(compile_commands_file))
subprocess.call("ninja -C {} -t compdb cxx cc > {}".format(build_dir, print("Generating compile commands in {}.".format(
compile_commands_file), shell=True) compile_commands_file), file=out)
subprocess.call(
"autoninja -C {}".format(build_dir, compile_commands_file), shell=True) ninja = "ninja -C {} -t compdb cxx cc > {}".format(
build_dir, compile_commands_file)
subprocess.call(ninja, shell=True, stdout=out)
autoninja = "autoninja -C {}".format(build_dir)
subprocess.call(autoninja, shell=True, stdout=out)
return compile_commands_file return compile_commands_file
...@@ -166,14 +176,8 @@ class Group(CompilationData): ...@@ -166,14 +176,8 @@ class Group(CompilationData):
self.count += 1 self.count += 1
def to_string(self, name_width): def to_string(self, name_width):
return "{:<{}} ({:>5} files): {}".format(self.name, name_width, self.count, super().to_string()) return "{:<{}} ({:>5} files): {}".format(
self.name, name_width, self.count, super().to_string())
class CompilationUnitEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, File):
return {"file": obj.file, "loc": obj.loc, "expanded": obj.expanded}
return json.JSONEncoder.default(self, obj)
def SetupReportGroups(): def SetupReportGroups():
...@@ -217,7 +221,7 @@ class Results: ...@@ -217,7 +221,7 @@ class Results:
self.groups = SetupReportGroups() self.groups = SetupReportGroups()
self.units = [] self.units = []
def isTracked(self, filename): def track(self, filename):
is_tracked = False is_tracked = False
for group in self.groups.values(): for group in self.groups.values():
if group.regexp.match(filename): if group.regexp.match(filename):
...@@ -233,24 +237,40 @@ class Results: ...@@ -233,24 +237,40 @@ class Results:
def maxGroupWidth(self): def maxGroupWidth(self):
return MaxWidth([v.name for v in self.groups.values()]) return MaxWidth([v.name for v in self.groups.values()])
def printGroupResults(self): def printGroupResults(self, file):
for key in sorted(self.groups.keys()): for key in sorted(self.groups.keys()):
print(self.groups[key].to_string(self.maxGroupWidth())) print(self.groups[key].to_string(self.maxGroupWidth()), file=file)
def printSorted(self, key, count, reverse, out):
for unit in sorted(self.units, key=key, reverse=reverse)[:count]:
print(unit.to_string(), file=out)
class LocsEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, File):
return {"file": o.file, "loc": o.loc, "expanded": o.expanded}
if isinstance(o, Group):
return {"name": o.name, "loc": o.loc, "expanded": o.expanded}
if isinstance(o, Results):
return {"groups": o.groups, "units": o.units}
return json.JSONEncoder.default(self, o)
class StatusLine: class StatusLine:
def __init__(self): def __init__(self):
self.max_width = 0 self.max_width = 0
def print(self, statusline, end="\r"): def print(self, statusline, end="\r", file=sys.stdout):
self.max_width = max(self.max_width, len(statusline)) self.max_width = max(self.max_width, len(statusline))
print("{0:<{1}}".format(statusline, self.max_width), end=end) print("{0:<{1}}".format(statusline, self.max_width), end=end, file=file)
class CommandSplitter: class CommandSplitter:
def __init__(self): def __init__(self):
self.cmd_pattern = re.compile( self.cmd_pattern = re.compile(
"([^\\s]*\\s+)?(?P<clangcmd>[^\\s]*clang.*) -c (?P<infile>.*) -o (?P<outfile>.*)") "([^\\s]*\\s+)?(?P<clangcmd>[^\\s]*clang.*)"
" -c (?P<infile>.*) -o (?P<outfile>.*)")
def process(self, compilation_unit, temp_file_name): def process(self, compilation_unit, temp_file_name):
cmd = self.cmd_pattern.match(compilation_unit['command']) cmd = self.cmd_pattern.match(compilation_unit['command'])
...@@ -260,12 +280,16 @@ class CommandSplitter: ...@@ -260,12 +280,16 @@ class CommandSplitter:
outfile = Path(str(temp_file_name)).joinpath(outfilename) outfile = Path(str(temp_file_name)).joinpath(outfilename)
return [cmd.group('clangcmd'), infilename, infile, outfile] return [cmd.group('clangcmd'), infilename, infile, outfile]
def Main(): def Main():
compile_commands_file = ARGS['compile_commands'] compile_commands_file = ARGS['compile_commands']
out = sys.stdout
if ARGS['json']:
out = sys.stderr
if ARGS['build_dir']: if ARGS['build_dir']:
compile_commands_file = GenerateCompileCommandsAndBuild( compile_commands_file = GenerateCompileCommandsAndBuild(
ARGS['build_dir'], compile_commands_file) ARGS['build_dir'], compile_commands_file, out)
try: try:
with open(compile_commands_file) as file: with open(compile_commands_file) as file:
...@@ -283,15 +307,18 @@ def Main(): ...@@ -283,15 +307,18 @@ def Main():
cmd_splitter = CommandSplitter() cmd_splitter = CommandSplitter()
for i, key in enumerate(data): for i, key in enumerate(data):
if not result.isTracked(key['file']): if not result.track(key['file']):
continue continue
if not ARGS['json']: if not ARGS['json']:
status.print("[{}/{}] Counting LoCs of {}".format(i, len(data), key['file'])) status.print(
"[{}/{}] Counting LoCs of {}".format(i, len(data), key['file']))
clangcmd, infilename, infile, outfile = cmd_splitter.process(key, temp) clangcmd, infilename, infile, outfile = cmd_splitter.process(key, temp)
outfile.parent.mkdir(parents=True, exist_ok=True) outfile.parent.mkdir(parents=True, exist_ok=True)
if infile.is_file(): if infile.is_file():
clangcmd = clangcmd + " -E -P " + str(infile) + " -o /dev/stdout | sed '/^\\s*$/d' | wc -l" clangcmd = clangcmd + " -E -P " + \
loccmd = "cat {} | sed '\\;^\\s*//;d' | sed '\\;^/\\*;d' | sed '/^\\*/d' | sed '/^\\s*$/d' | wc -l".format( str(infile) + " -o /dev/stdout | sed '/^\\s*$/d' | wc -l"
loccmd = ("cat {} | sed '\\;^\\s*//;d' | sed '\\;^/\\*;d'"
" | sed '/^\\*/d' | sed '/^\\s*$/d' | wc -l").format(
infile) infile)
runcmd = " {} ; {}".format(clangcmd, loccmd) runcmd = " {} ; {}".format(clangcmd, loccmd)
if ARGS['echocmd']: if ARGS['echocmd']:
...@@ -301,40 +328,41 @@ def Main(): ...@@ -301,40 +328,41 @@ def Main():
processes.append({'process': p, 'infile': infilename}) processes.append({'process': p, 'infile': infilename})
for i, p in enumerate(processes): for i, p in enumerate(processes):
if not ARGS['json']: status.print("[{}/{}] Summing up {}".format(
status.print("[{}/{}] Summing up {}".format(i, len(processes), p['infile'])) i, len(processes), p['infile']), file=out)
output, err = p['process'].communicate() output, err = p['process'].communicate()
expanded, loc = list(map(int, output.split())) expanded, loc = list(map(int, output.split()))
result.recordFile(p['infile'], loc, expanded) result.recordFile(p['infile'], loc, expanded)
end = time.time() end = time.time()
if ARGS['json']: if ARGS['json']:
print(json.dumps(result, ensure_ascii=False, cls=CompilationUnitEncoder)) print(json.dumps(result, ensure_ascii=False, cls=LocsEncoder))
else: status.print("Processed {:,} files in {:,.2f} sec.".format(
status.print("Processed {:,} files in {:,.2f} sec.".format(len(processes), end-start), end="\n") len(processes), end-start), end="\n", file=out)
result.printGroupResults() result.printGroupResults(file=out)
if ARGS['largest']: if ARGS['largest']:
print("Largest {} files after expansion:".format(ARGS['largest'])) print("Largest {} files after expansion:".format(ARGS['largest']))
for u in sorted(result.units, key=lambda v: v.expanded, reverse=True)[:ARGS['largest']]: result.printSorted(
print(u.to_string()) lambda v: v.expanded, ARGS['largest'], reverse=True, out=out)
if ARGS['worst']: if ARGS['worst']:
print("Worst expansion ({} files):".format(ARGS['worst'])) print("Worst expansion ({} files):".format(ARGS['worst']))
for u in sorted(result.units, key=lambda v: v.ratio(), reverse=True)[:ARGS['worst']]: result.printSorted(
print(u.to_string()) lambda v: v.ratio(), ARGS['worst'], reverse=True, out=out)
if ARGS['smallest']: if ARGS['smallest']:
print("Smallest {} input files:".format(ARGS['smallest'])) print("Smallest {} input files:".format(ARGS['smallest']))
for u in sorted(result.units, key=lambda v: v.loc, reverse=False)[:ARGS['smallest']]: result.printSorted(
print(u.to_string()) lambda v: v.loc, ARGS['smallest'], reverse=False, out=out)
if ARGS['files']: if ARGS['files']:
print("List of input files:") print("List of input files:")
for u in sorted(result.units, key=lambda v: v.file, reverse=False): result.printSorted(
print(u.to_string()) lambda v: v.file, ARGS['files'], reverse=False, out=out)
return 0 return 0
if __name__ == '__main__': if __name__ == '__main__':
sys.exit(Main()) sys.exit(Main())
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment