#!/usr/bin/env python # line-directive.py - Transform line numbers in error messages -*- python -*- # # This source file is part of the Swift.org open source project # # Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors # Licensed under Apache License v2.0 with Runtime Library Exception # # See https://swift.org/LICENSE.txt for license information # See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors # # ---------------------------------------------------------------------------- # # Converts line numbers in error messages according to "line directive" # comments. # # ---------------------------------------------------------------------------- from __future__ import print_function import bisect import os import re import subprocess import sys line_pattern = re.compile( r'^// ###sourceLocation\(file:\s*"([^"]+)",\s*line:\s*([0-9]+)\s*\)') def _make_line_map(target_filename, stream=None): """ >>> from StringIO import StringIO >>> _make_line_map('box', ... StringIO('''// ###sourceLocation(file: "foo.bar", line: 3) ... line 2 ... line 3 ... line 4 ... // ###sourceLocation(file: "baz.txt", line: 20) ... line 6 ... line 7 ... ''')) [(0, 'box', 1), (1, 'foo.bar', 3), (5, 'baz.txt', 20)] """ result = [(0, target_filename, 1)] input = stream or open(target_filename) for i, l in enumerate(input.readlines()): m = line_pattern.match(l) if m: result.append((i + 1, m.group(1), int(m.group(2)))) return result _line_maps = {} def fline_map(target_filename): map = _line_maps.get(target_filename) if map is None: map = _make_line_map(target_filename) _line_maps[target_filename] = map return map def map_line_to_source_file(target_filename, target_line_num): """ >>> from tempfile import * >>> t = NamedTemporaryFile() >>> t.write('''line 1 ... line 2 ... // ###sourceLocation(file: "foo.bar", line: 20) ... line 4 ... line 5 ... // ###sourceLocation(file: "baz.txt", line: 5) ... line 7 ... line 8 ... ''') >>> t.flush() >>> (t2, l) = map_line_to_source_file(t.name, 1) >>> t2 == t.name, l (True, 1) >>> (t2, l) = map_line_to_source_file(t.name, 2) >>> t2 == t.name, l (True, 2) >>> (t2, l) = map_line_to_source_file(t.name, 3) >>> t2 == t.name, l (True, 3) >>> map_line_to_source_file(t.name, 4) ('foo.bar', 20) >>> map_line_to_source_file(t.name, 5) ('foo.bar', 21) >>> map_line_to_source_file(t.name, 6) ('foo.bar', 22) >>> map_line_to_source_file(t.name, 7) ('baz.txt', 5) >>> map_line_to_source_file(t.name, 8) ('baz.txt', 6) >>> map_line_to_source_file(t.name, 42) ('baz.txt', 40) """ assert(target_line_num > 0) map = fline_map(target_filename) index = bisect.bisect_left(map, (target_line_num, '', 0)) base = map[index - 1] return base[1], base[2] + (target_line_num - base[0] - 1) def map_line_from_source_file(source_filename, source_line_num, target_filename): """ >>> from tempfile import * >>> t = NamedTemporaryFile() >>> t.write('''line 1 ... line 2 ... // ###sourceLocation(file: "foo.bar", line: 20) ... line 4 ... line 5 ... // ###sourceLocation(file: "baz.txt", line: 5) ... line 7 ... line 8 ... ''') >>> t.flush() >>> map_line_from_source_file(t.name, 1, t.name) 1 >>> map_line_from_source_file(t.name, 2, t.name) 2 >>> map_line_from_source_file(t.name, 3, t.name) 3 >>> try: map_line_from_source_file(t.name, 4, t.name) ... except RuntimeError: pass >>> try: map_line_from_source_file('foo.bar', 19, t.name) ... except RuntimeError: pass >>> map_line_from_source_file('foo.bar', 20, t.name) 4 >>> map_line_from_source_file('foo.bar', 21, t.name) 5 >>> map_line_from_source_file('foo.bar', 22, t.name) 6 >>> try: map_line_from_source_file('foo.bar', 23, t.name) ... except RuntimeError: pass >>> map_line_from_source_file('baz.txt', 5, t.name) 7 >>> map_line_from_source_file('baz.txt', 6, t.name) 8 >>> map_line_from_source_file('baz.txt', 33, t.name) 35 >>> try: map_line_from_source_file(t.name, 33, t.name) ... except RuntimeError: pass >>> try: map_line_from_source_file('foo.bar', 2, t.name) ... except RuntimeError: pass """ assert(source_line_num > 0) map = fline_map(target_filename) for i, (target_line_num, found_source_filename, found_source_line_num) in enumerate(map): if found_source_filename != source_filename: continue if found_source_line_num > source_line_num: continue result = target_line_num + (source_line_num - found_source_line_num) if i + 1 == len(map) or map[i + 1][0] > result: return result + 1 raise RuntimeError("line not found") def read_response_file(file_path): with open(file_path, 'r') as files: return filter(None, files.read().split(';')) def expand_response_files(files): expanded_files = [] for file_path in files: # Read a list of files from a response file. if file_path[0] == '@': expanded_files.extend(read_response_file(file_path[1:])) else: expanded_files.append(file_path) return expanded_files def run(): """Simulate a couple of gyb-generated files >>> from tempfile import * >>> target1 = NamedTemporaryFile() >>> target1.write('''line 1 ... line 2 ... // ###sourceLocation(file: "foo.bar", line: 20) ... line 4 ... line 5 ... // ###sourceLocation(file: "baz.txt", line: 5) ... line 7 ... line 8 ... ''') >>> target1.flush() >>> target2 = NamedTemporaryFile() >>> target2.write('''// ###sourceLocation(file: "foo.bar", line: 7) ... line 2 ... line 3 ... // ###sourceLocation(file: "fox.box", line: 11) ... line 5 ... line 6 ... ''') >>> target2.flush() Simulate the raw output of compilation >>> raw_output = NamedTemporaryFile() >>> target1_name, target2_name = target1.name, target2.name >>> raw_output.write('''A ... %(target1_name)s:2:111: error one ... B ... %(target1_name)s:4:222: error two ... C ... %(target1_name)s:8:333: error three ... D ... glitch in file %(target2_name)s:1 assert one ... E ... glitch in file %(target2_name)s, line 2 assert two ... glitch at %(target2_name)s, line 3 assert three ... glitch at %(target2_name)s:4 assert four ... glitch in [%(target2_name)s, line 5 assert five ... glitch in [%(target2_name)s:22 assert six ... ''' % locals()) >>> raw_output.flush() Run this tool on the two targets, using a portable version of Unix 'cat' to dump the output file. >>> import subprocess >>> output = subprocess.check_output([ ... __file__, target1.name, target2.name, '--', ... sys.executable, '-c', ... 'import sys;sys.stdout.write(open(sys.argv[1]).read())', ... raw_output.name]) Replace temporary filenames and check it. >>> print output.replace(target1.name,'TARGET1-NAME') ... .replace(target2.name,'TARGET2-NAME') + 'EOF' A TARGET1-NAME:2:111: error one B foo.bar:20:222: error two C baz.txt:6:333: error three D glitch in file TARGET2-NAME:1 assert one E glitch in file foo.bar, line 7 assert two glitch at foo.bar, line 8 assert three glitch at foo.bar:9 assert four glitch in [fox.box, line 11 assert five glitch in [fox.box:28 assert six EOF >>> print subprocess.check_output([__file__, 'foo.bar', '21', ... target1.name]), 5 >>> print subprocess.check_output([__file__, 'foo.bar', '8', ... target2.name]), 3 """ if len(sys.argv) <= 1: import doctest doctest.testmod() elif '--' not in sys.argv: source_file = sys.argv[1] source_line = int(sys.argv[2]) target_file = sys.argv[3] print(map_line_from_source_file(source_file, source_line, target_file)) else: dashes = sys.argv.index('--') sources = expand_response_files(sys.argv[1:dashes]) # The first argument of command_args is the process to open. # subprocess.Popen doesn't normalize arguments. This means that trying # to open a non-normalized file (e.g. C:/swift/./bin/swiftc.exe) on # Windows results in file/directory not found errors, as Popen # delegates to the Win32 CreateProcess API. Unix systems handle # non-normalized paths, so don't have this problem. # Arguments passed to the process are normalized by the process. command_args = expand_response_files(sys.argv[dashes + 1:]) command_args[0] = os.path.normpath(command_args[0]) command = subprocess.Popen( command_args, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, universal_newlines=True ) sources = '(?P' + '|'.join(re.escape(s) for s in sources) + ')' error_pattern = re.compile( '^' + sources + ':(?P[0-9]+):(?P[0-9]+):;(?P.*?)\n?$') assertion_pattern = re.compile( '^(?P.*( file | at |#[0-9]+: |[[]))' + sources + '(?P, line |:)(?P[0-9]+)(?P.*?)\n?$') while True: input = command.stdout.readline() if input == '': break output = input def decode_match(p, l): m = p.match(l) if m is None: return () file, line_num = map_line_to_source_file( m.group('file'), int(m.group('line'))) return ((m, file, line_num),) for (m, file, line_num) in decode_match(error_pattern, input): output = '%s:%s:%s:%s\n' % ( file, line_num, int(m.group(3)), m.group(4)) break else: for (m, file, line_num) in decode_match(assertion_pattern, input): output = '%s%s%s%s%s\n' % ( m.group('head'), file, m.group('middle'), line_num, m.group('tail')) sys.stdout.write(output) sys.exit(command.wait()) if __name__ == '__main__': run()