1#!/usr/bin/env python3
2#
3# Copyright 2021 The Chromium Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6"""Contains helper class for processing javac output."""
7
8import os
9import pathlib
10import re
11import sys
12
13from util import build_utils
14
15sys.path.insert(
16    0,
17    os.path.join(build_utils.DIR_SOURCE_ROOT, 'third_party', 'colorama', 'src'))
18import colorama
19sys.path.insert(
20    0,
21    os.path.join(build_utils.DIR_SOURCE_ROOT, 'tools', 'android',
22                 'modularization', 'convenience'))
23import lookup_dep
24
25
26class JavacOutputProcessor:
27  def __init__(self, target_name):
28    self._target_name = target_name
29
30    # Example: ../../ui/android/java/src/org/chromium/ui/base/Clipboard.java:45:
31    fileline_prefix = (
32        r'(?P<fileline>(?P<file>[-.\w/\\]+.java):(?P<line>[0-9]+):)')
33
34    self._warning_re = re.compile(
35        fileline_prefix + r'(?P<full_message> warning: (?P<message>.*))$')
36    self._error_re = re.compile(fileline_prefix +
37                                r'(?P<full_message> (?P<message>.*))$')
38    self._marker_re = re.compile(r'\s*(?P<marker>\^)\s*$')
39
40    # Matches output modification performed by _ElaborateLineForUnknownSymbol()
41    # so that it can be colorized.
42    # Example: org.chromium.base.Log found in dep //base:base_java.
43    self._please_add_dep_re = re.compile(
44        r'(?P<full_message>Please add //[\w/:]+ dep to //[\w/:]+.*)$')
45
46    # First element in pair is bool which indicates whether the missing
47    # class/package is part of the error message.
48    self._symbol_not_found_re_list = [
49        # Example:
50        # error: package org.chromium.components.url_formatter does not exist
51        (True,
52         re.compile(fileline_prefix +
53                    r'( error: package [\w.]+ does not exist)$')),
54        # Example: error: cannot find symbol
55        (False, re.compile(fileline_prefix + r'( error: cannot find symbol)$')),
56        # Example: error: symbol not found org.chromium.url.GURL
57        (True,
58         re.compile(fileline_prefix + r'( error: symbol not found [\w.]+)$')),
59    ]
60
61    # Example: import org.chromium.url.GURL;
62    self._import_re = re.compile(r'\s*import (?P<imported_class>[\w\.]+);$')
63
64    self._warning_color = [
65        'full_message', colorama.Fore.YELLOW + colorama.Style.DIM
66    ]
67    self._error_color = [
68        'full_message', colorama.Fore.MAGENTA + colorama.Style.BRIGHT
69    ]
70    self._marker_color = ['marker', colorama.Fore.BLUE + colorama.Style.BRIGHT]
71
72    self._class_lookup_index = None
73
74    colorama.init()
75
76  def Process(self, lines):
77    """ Processes javac output.
78
79      - Applies colors to output.
80      - Suggests GN dep to add for 'unresolved symbol in Java import' errors.
81      """
82    lines = self._ElaborateLinesForUnknownSymbol(iter(lines))
83    return (self._ApplyColors(l) for l in lines)
84
85  def _ElaborateLinesForUnknownSymbol(self, lines):
86    """ Elaborates passed-in javac output for unresolved symbols.
87
88    Looks for unresolved symbols in imports.
89    Adds:
90    - Line with GN target which cannot compile.
91    - Mention of unresolved class if not present in error message.
92    - Line with suggestion of GN dep to add.
93
94    Args:
95      lines: Generator with javac input.
96    Returns:
97      Generator with processed output.
98    """
99    previous_line = next(lines, None)
100    line = next(lines, None)
101    while previous_line != None:
102      elaborated_lines = self._ElaborateLineForUnknownSymbol(
103          previous_line, line)
104      for elaborated_line in elaborated_lines:
105        yield elaborated_line
106
107      previous_line = line
108      line = next(lines, None)
109
110  def _ApplyColors(self, line):
111    """Adds colors to passed-in line and returns processed line."""
112    if self._warning_re.match(line):
113      line = self._Colorize(line, self._warning_re, self._warning_color)
114    elif self._error_re.match(line):
115      line = self._Colorize(line, self._error_re, self._error_color)
116    elif self._please_add_dep_re.match(line):
117      line = self._Colorize(line, self._please_add_dep_re, self._error_color)
118    elif self._marker_re.match(line):
119      line = self._Colorize(line, self._marker_re, self._marker_color)
120    return line
121
122  def _ElaborateLineForUnknownSymbol(self, line, next_line):
123    if not next_line:
124      return [line]
125
126    import_re_match = self._import_re.match(next_line)
127    if not import_re_match:
128      return [line]
129
130    symbol_missing = False
131    has_missing_symbol_in_error_msg = False
132    for symbol_in_error_msg, regex in self._symbol_not_found_re_list:
133      if regex.match(line):
134        symbol_missing = True
135        has_missing_symbol_in_error_msg = symbol_in_error_msg
136        break
137
138    if not symbol_missing:
139      return [line]
140
141    class_to_lookup = import_re_match.group('imported_class')
142    if self._class_lookup_index == None:
143      self._class_lookup_index = lookup_dep.ClassLookupIndex(pathlib.Path(
144          os.getcwd()),
145                                                             should_build=False)
146    suggested_deps = self._class_lookup_index.match(class_to_lookup)
147
148    if len(suggested_deps) != 1:
149      suggested_deps = self._FindFactoryDep(suggested_deps)
150      if len(suggested_deps) != 1:
151        return [line]
152
153    suggested_target = suggested_deps[0].target
154
155    target_name = self._RemoveSuffixesIfPresent(
156        ["__compile_java", "__errorprone", "__header"], self._target_name)
157    if not has_missing_symbol_in_error_msg:
158      line = "{} {}".format(line, class_to_lookup)
159
160    return [
161        line,
162        "Please add {} dep to {}. ".format(suggested_target, target_name) +
163        "File a crbug if this suggestion is incorrect.",
164    ]
165
166  @staticmethod
167  def _FindFactoryDep(class_entries):
168    """Find the android_library_factory() GN target."""
169    if len(class_entries) != 2:
170      return []
171
172    # android_library_factory() targets set low_classpath_priority=true.
173    # This logic is correct if GN targets other than android_library_factory()
174    # set low_classpath_priority=true. low_classpath_priority=true indicates
175    # that the target is depended on (and overridden) by other targets which
176    # contain the same class. We want to recommend the leaf target.
177    if class_entries[0].low_classpath_priority == class_entries[
178        1].low_classpath_priority:
179      return []
180
181    if class_entries[0].low_classpath_priority:
182      return [class_entries[0]]
183    return [class_entries[1]]
184
185  @staticmethod
186  def _RemoveSuffixesIfPresent(suffixes, text):
187    for suffix in suffixes:
188      if text.endswith(suffix):
189        return text[:-len(suffix)]
190    return text
191
192  @staticmethod
193  def _Colorize(line, regex, color):
194    match = regex.match(line)
195    start = match.start(color[0])
196    end = match.end(color[0])
197    return (line[:start] + color[1] + line[start:end] + colorama.Fore.RESET +
198            colorama.Style.RESET_ALL + line[end:])
199