1
+ package io .bazel .rulesscala .scalac .reporter ;
2
+
3
+ import io .bazel .rulesscala .deps .proto .ScalaDeps ;
4
+ import io .bazel .rulesscala .deps .proto .ScalaDeps .Dependency ;
5
+ import io .bazel .rulesscala .deps .proto .ScalaDeps .Dependency .Kind ;
6
+ import io .bazel .rulesscala .scalac .compileoptions .CompileOptions ;
7
+ import java .io .BufferedOutputStream ;
8
+ import java .io .IOException ;
9
+ import java .io .OutputStream ;
10
+ import java .nio .file .Files ;
11
+ import java .nio .file .Paths ;
12
+ import java .util .Arrays ;
13
+ import java .util .Collection ;
14
+ import java .util .HashMap ;
15
+ import java .util .HashSet ;
16
+ import java .util .Map ;
17
+ import java .util .Set ;
18
+ import java .util .jar .JarFile ;
19
+ import java .util .stream .Collectors ;
20
+ import scala .collection .immutable .List$ ;
21
+ import scala .reflect .internal .util .CodeAction ;
22
+ import scala .reflect .internal .util .NoPosition$ ;
23
+ import scala .reflect .internal .util .Position ;
24
+ import scala .tools .nsc .Settings ;
25
+ import scala .tools .nsc .reporters .ConsoleReporter ;
26
+ import scala .tools .nsc .reporters .Reporter ;
27
+
28
+ import javax .print .attribute .standard .Severity ;
29
+
30
+ public class DepsTrackingReporter extends ConsoleReporter {
31
+
32
+ private static final String HJAR_JAR_SUFFIX = "-hjar.jar" ;
33
+ private static final String IJAR_JAR_SUFFIX = "-ijar.jar" ;
34
+ private final Set <String > usedJars = new HashSet <>();
35
+
36
+ private final Map <String , String > jarToTarget = new HashMap <>();
37
+ private final Map <String , String > indirectJarToTarget = new HashMap <>();
38
+
39
+ private final Set <String > ignoredTargets ;
40
+ private final Set <String > directTargets ;
41
+
42
+ private final CompileOptions ops ;
43
+ public final Reporter delegateReporter ;
44
+ private Set <String > astUsedJars = new HashSet <>();
45
+
46
+ public DepsTrackingReporter (Settings settings , CompileOptions ops , Reporter delegate ) {
47
+ super (settings );
48
+ this .ops = ops ;
49
+ this .delegateReporter = delegate ;
50
+
51
+ if (ops .directJars .length == ops .directTargets .length ) {
52
+ for (int i = 0 ; i < ops .directJars .length ; i ++) {
53
+ jarToTarget .put (ops .directJars [i ], ops .directTargets [i ]);
54
+ }
55
+ } else {
56
+ throw new IllegalArgumentException (
57
+ "mismatched size: directJars " + ops .directJars .length + " vs directTargets"
58
+ + ops .directTargets .length );
59
+ }
60
+
61
+ if (ops .indirectJars .length == ops .indirectTargets .length ) {
62
+ for (int i = 0 ; i < ops .indirectJars .length ; i ++) {
63
+ indirectJarToTarget .put (ops .indirectJars [i ], ops .indirectTargets [i ]);
64
+ }
65
+ } else {
66
+ throw new IllegalArgumentException (
67
+ "mismatched size: indirectJars " + ops .directJars .length + " vs indirectTargets "
68
+ + ops .directTargets .length );
69
+ }
70
+
71
+ ignoredTargets = Arrays .stream (ops .unusedDepsIgnoredTargets ).collect (Collectors .toSet ());
72
+ directTargets = Arrays .stream (ops .directTargets ).collect (Collectors .toSet ());
73
+ }
74
+
75
+ private boolean isDependencyTrackingOn () {
76
+ return "ast-plus" .equals (ops .dependencyTrackingMethod )
77
+ && (!"off" .equals (ops .strictDepsMode ) || !"off" .equals (ops .unusedDependencyCheckerMode ));
78
+ }
79
+
80
+ @ Override
81
+ public void doReport (Position pos , String msg , Severity severity , scala .collection .immutable .List <CodeAction > actions ) {
82
+ if (msg .startsWith ("DT:" )) {
83
+ if (isDependencyTrackingOn ()) {
84
+ parseOpenedJar (msg );
85
+ }
86
+ } else {
87
+ if (delegateReporter != null ) {
88
+ delegateReporter .doReport (pos , msg , severity , List$ .MODULE$ .<CodeAction >empty ());
89
+ } else {
90
+ super .doReport (pos , msg , severity , List$ .MODULE$ .<CodeAction >empty ());
91
+ }
92
+ }
93
+ }
94
+
95
+ private void parseOpenedJar (String msg ) {
96
+ String jar = msg .split (":" )[1 ];
97
+
98
+ //normalize path separators (scalac passes os-specific path separators.)
99
+ jar = jar .replace ("\\ " , "/" );
100
+
101
+ // track only jars from dependency targets
102
+ // this should exclude things like rt.jar which come from JDK
103
+ if (jarToTarget .containsKey (jar ) || indirectJarToTarget .containsKey (jar )) {
104
+ usedJars .add (jar );
105
+ }
106
+ }
107
+
108
+ public void prepareReport () throws IOException {
109
+ Set <String > usedTargets = new HashSet <>();
110
+ Set <Dependency > usedDeps = new HashSet <>();
111
+
112
+ for (String jar : usedJars ) {
113
+ String target = jarToTarget .get (jar );
114
+
115
+ if (target == null ) {
116
+ target = indirectJarToTarget .get (jar );
117
+ }
118
+
119
+ if (target .startsWith ("Unknown" )) {
120
+ target = jarLabel (jar );
121
+ }
122
+
123
+ if (target == null ) {
124
+ // probably a bug if we get here
125
+ continue ;
126
+ }
127
+
128
+ Dependency dep = buildDependency (
129
+ jar ,
130
+ target ,
131
+ astUsedJars .contains (jar ) ? Kind .EXPLICIT : Kind .IMPLICIT ,
132
+ ignoredTargets .contains (target )
133
+ );
134
+
135
+ usedTargets .add (target );
136
+ usedDeps .add (dep );
137
+ }
138
+
139
+ Set <Dependency > unusedDeps = new HashSet <>();
140
+ for (int i = 0 ; i < ops .directTargets .length ; i ++) {
141
+ String directTarget = ops .directTargets [i ];
142
+ if (usedTargets .contains (directTarget )) {
143
+ continue ;
144
+ }
145
+
146
+ unusedDeps .add (
147
+ buildDependency (
148
+ ops .directJars [i ],
149
+ directTarget ,
150
+ Kind .UNUSED ,
151
+ ignoredTargets .contains (directTarget ) || "off" .equals (ops .unusedDependencyCheckerMode )
152
+ )
153
+ );
154
+ }
155
+
156
+ writeSdepsFile (usedDeps , unusedDeps );
157
+
158
+ Reporter reporter = this .delegateReporter != null ? this .delegateReporter : this ;
159
+ reportDeps (usedDeps , unusedDeps , reporter );
160
+ }
161
+
162
+ private Dependency buildDependency (String jar , String target , Kind kind , boolean ignored ) {
163
+ ScalaDeps .Dependency .Builder dependecyBuilder = ScalaDeps .Dependency .newBuilder ();
164
+
165
+ dependecyBuilder .setKind (kind );
166
+ dependecyBuilder .setLabel (target );
167
+ dependecyBuilder .setIjarPath (jar );
168
+ dependecyBuilder .setPath (guessFullJarPath (jar ));
169
+ dependecyBuilder .setIgnored (ignored );
170
+
171
+ return dependecyBuilder .build ();
172
+ }
173
+
174
+ private void writeSdepsFile (Collection <Dependency > usedDeps , Collection <Dependency > unusedDeps )
175
+ throws IOException {
176
+
177
+ ScalaDeps .Dependencies .Builder builder = ScalaDeps .Dependencies .newBuilder ();
178
+ builder .setRuleLabel (ops .currentTarget );
179
+ builder .setDependencyTrackingMethod (ops .dependencyTrackingMethod );
180
+ builder .addAllDependency (usedDeps );
181
+ builder .addAllDependency (unusedDeps );
182
+
183
+ try (OutputStream outputStream = new BufferedOutputStream (
184
+ Files .newOutputStream (Paths .get (ops .scalaDepsFile )))) {
185
+ outputStream .write (builder .build ().toByteArray ());
186
+ }
187
+ }
188
+
189
+ private void reportDeps (Collection <Dependency > usedDeps , Collection <Dependency > unusedDeps ,
190
+ Reporter reporter ) {
191
+ if (ops .dependencyTrackingMethod .equals ("ast-plus" )) {
192
+
193
+ if (!ops .strictDepsMode .equals ("off" )) {
194
+ boolean isWarning = ops .strictDepsMode .equals ("warn" );
195
+ StringBuilder strictDepsReport = new StringBuilder ("Missing strict dependencies:\n " );
196
+ StringBuilder compilerDepsReport = new StringBuilder ("Missing compiler dependencies:\n " );
197
+ int strictDepsCount = 0 ;
198
+ int compilerDepsCount = 0 ;
199
+ for (Dependency dep : usedDeps ) {
200
+ String depReport = addDepMessage (dep );
201
+ if (dep .getIgnored ()) {
202
+ continue ;
203
+ }
204
+
205
+ if (directTargets .contains (dep .getLabel ())) {
206
+ continue ;
207
+ }
208
+
209
+ if (dep .getKind () == Kind .EXPLICIT ) {
210
+ strictDepsCount ++;
211
+ strictDepsReport
212
+ .append (isWarning ? "warning: " : "error: " )
213
+ .append (depReport );
214
+ } else {
215
+ compilerDepsCount ++;
216
+ compilerDepsReport
217
+ .append (isWarning ? "warning: " : "error: " )
218
+ .append (depReport );
219
+ }
220
+ }
221
+
222
+ if (strictDepsCount > 0 ) {
223
+ if (ops .strictDepsMode .equals ("warn" )) {
224
+ reporter .warning (NoPosition$ .MODULE$ , strictDepsReport .toString (), List$ .MODULE$ .<CodeAction >empty ());
225
+ } else {
226
+ reporter .error (NoPosition$ .MODULE$ , strictDepsReport .toString (), List$ .MODULE$ .<CodeAction >empty ());
227
+ }
228
+ }
229
+
230
+ if (!ops .compilerDepsMode .equals ("off" ) && compilerDepsCount > 0 ) {
231
+ if (ops .compilerDepsMode .equals ("warn" )) {
232
+ reporter .warning (NoPosition$ .MODULE$ , compilerDepsReport .toString (), List$ .MODULE$ .<CodeAction >empty ());
233
+ } else {
234
+ reporter .error (NoPosition$ .MODULE$ , compilerDepsReport .toString (), List$ .MODULE$ .<CodeAction >empty ());
235
+ }
236
+ }
237
+ }
238
+
239
+ if (!ops .unusedDependencyCheckerMode .equals ("off" )) {
240
+ boolean isWarning = ops .unusedDependencyCheckerMode .equals ("warn" );
241
+ StringBuilder unusedDepsReport = new StringBuilder ("Unused dependencies:\n " );
242
+ int count = 0 ;
243
+ for (Dependency dep : unusedDeps ) {
244
+ if (dep .getIgnored ()) {
245
+ continue ;
246
+ }
247
+ count ++;
248
+ unusedDepsReport
249
+ .append (isWarning ? "warning: " : "error: " )
250
+ .append (removeDepMessage (dep ));
251
+ }
252
+ if (count > 0 ) {
253
+ if (isWarning ) {
254
+ reporter .warning (NoPosition$ .MODULE$ , unusedDepsReport .toString (), List$ .MODULE$ .<CodeAction >empty ());
255
+ } else if (ops .unusedDependencyCheckerMode .equals ("error" )) {
256
+ reporter .error (NoPosition$ .MODULE$ , unusedDepsReport .toString (), List$ .MODULE$ .<CodeAction >empty ());
257
+ }
258
+ }
259
+ }
260
+ }
261
+ }
262
+
263
+ private String addDepMessage (Dependency dep ) {
264
+ String target = dep .getLabel ();
265
+ String jar = dep .getPath ();
266
+
267
+ String message = "Target '" + target + "' (via jar: ' " + jar + " ') "
268
+ + "is being used by " + ops .currentTarget
269
+ + " but is is not specified as a dependency, please add it to the deps.\n "
270
+ + "You can use the following buildozer command:\n " ;
271
+ String command = "buildozer 'add deps " + target + "' " + ops .currentTarget + "\n " ;
272
+ return message + command ;
273
+ }
274
+
275
+ private String removeDepMessage (Dependency dep ) {
276
+ String target = dep .getLabel ();
277
+ String jar = dep .getPath ();
278
+
279
+ String message = "Target '" + target + "' (via jar: ' " + jar + " ') "
280
+ + "is specified as a dependency to " + ops .currentTarget
281
+ + " but isn't used, please remove it from the deps.\n "
282
+ + "You can use the following buildozer command:\n " ;
283
+ String command = "buildozer 'remove deps " + target + "' " + ops .currentTarget + "\n " ;
284
+
285
+ return message + command ;
286
+ }
287
+
288
+ private String guessFullJarPath (String jar ) {
289
+ if (jar .endsWith (IJAR_JAR_SUFFIX )) {
290
+ return stripIjarSuffix (jar , IJAR_JAR_SUFFIX );
291
+ } else if (jar .endsWith (HJAR_JAR_SUFFIX )) {
292
+ return stripIjarSuffix (jar , HJAR_JAR_SUFFIX );
293
+ } else {
294
+ return jar ;
295
+ }
296
+ }
297
+
298
+ private static String stripIjarSuffix (String jar , String suffix ) {
299
+ return jar .substring (0 , jar .length () - suffix .length ()) + ".jar" ;
300
+ }
301
+
302
+ private String jarLabel (String path ) throws IOException {
303
+ try (JarFile jar = new JarFile (path )) {
304
+ return jar .getManifest ().getMainAttributes ().getValue ("Target-Label" );
305
+ }
306
+ }
307
+
308
+ public void registerAstUsedJars (Set <String > jars ) {
309
+ astUsedJars = jars ;
310
+ }
311
+
312
+ public void writeDiagnostics (String diagnosticsFile ) throws IOException {
313
+ if (delegateReporter == null ) {
314
+ return ;
315
+ }
316
+
317
+ ProtoReporter protoReporter = (ProtoReporter ) delegateReporter ;
318
+ protoReporter .writeTo (Paths .get (diagnosticsFile ));
319
+ }
320
+ }
0 commit comments