| #!/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<file>' + '|'.join(re.escape(s) for s in sources) + ')' |
| |
| error_pattern = re.compile( |
| '^' + sources + |
| ':(?P<line>[0-9]+):(?P<column>[0-9]+):;(?P<tail>.*?)\n?$') |
| |
| assertion_pattern = re.compile( |
| '^(?P<head>.*( file | at |#[0-9]+: |[[]))' + |
| sources + |
| '(?P<middle>, line |:)(?P<line>[0-9]+)(?P<tail>.*?)\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() |