|
23 | 23 | import java.util.Iterator; |
24 | 24 | import java.util.List; |
25 | 25 | import java.util.Map; |
| 26 | +import java.util.Map.Entry; |
26 | 27 | import java.util.Objects; |
27 | 28 | import java.util.Optional; |
28 | 29 | import java.util.function.Function; |
@@ -144,6 +145,22 @@ private static Optional<CompilationUnit> findTargetDOM(Map<JavaFileObject, Compi |
144 | 145 | return Optional.empty(); |
145 | 146 | } |
146 | 147 | } |
| 148 | + |
| 149 | + private final class CollectDiagnosticsInMap implements DiagnosticListener<JavaFileObject> { |
| 150 | + private final JavacProblemConverter problemConverter; |
| 151 | + private final List<IProblem> problemCollection; |
| 152 | + |
| 153 | + private CollectDiagnosticsInMap( |
| 154 | + JavacProblemConverter problemConverter) { |
| 155 | + this.problemConverter = problemConverter; |
| 156 | + this.problemCollection = new ArrayList<>(); |
| 157 | + } |
| 158 | + |
| 159 | + @Override |
| 160 | + public void report(Diagnostic<? extends JavaFileObject> diagnostic) { |
| 161 | + problemCollection.add(problemConverter.createJavacProblem(diagnostic)); |
| 162 | + } |
| 163 | + } |
147 | 164 |
|
148 | 165 | private interface GenericRequestor { |
149 | 166 | public void acceptBinding(String bindingKey, IBinding binding); |
@@ -1105,4 +1122,284 @@ private boolean configureAPIfNecessary(JavacFileManager fileManager) { |
1105 | 1122 |
|
1106 | 1123 | return false; |
1107 | 1124 | } |
| 1125 | + |
| 1126 | + @Override |
| 1127 | + public CompilationUnit findProblems(org.eclipse.jdt.internal.core.CompilationUnit unitElement, |
| 1128 | + WorkingCopyOwner workingCopyOwner, Map<String, CategorizedProblem[]> groupedProblems, int astLevel, |
| 1129 | + int reconcileFlags, Map<String, String> compilerOptions, boolean resolveBindings, IProgressMonitor monitor) |
| 1130 | + throws JavaModelException { |
| 1131 | + // Implementation is a hacked together copy of toCompilationUnit |
| 1132 | + IJavaProject javaProject = unitElement.getJavaProject(); |
| 1133 | + var compiler = ToolProvider.getSystemJavaCompiler(); |
| 1134 | + Context context = new Context(); |
| 1135 | + CachingJarsJavaFileManager.preRegister(context); |
| 1136 | + Map<JavaFileObject, CompilationUnit> filesToUnits = new HashMap<>(); |
| 1137 | + final UnusedProblemFactory unusedProblemFactory = new UnusedProblemFactory(new DefaultProblemFactory(), compilerOptions); |
| 1138 | + var problemConverter = new JavacProblemConverter(compilerOptions, context); |
| 1139 | + CollectDiagnosticsInMap diagnosticListener = new CollectDiagnosticsInMap(problemConverter); |
| 1140 | + MultiTaskListener.instance(context).add(new TaskListener() { |
| 1141 | + @Override |
| 1142 | + public void finished(TaskEvent e) { |
| 1143 | + if (e.getCompilationUnit() instanceof JCCompilationUnit u) { |
| 1144 | + problemConverter.registerUnit(e.getSourceFile(), u); |
| 1145 | + } |
| 1146 | + |
| 1147 | + if (e.getKind() == TaskEvent.Kind.ANALYZE) { |
| 1148 | + final JavaFileObject file = e.getSourceFile(); |
| 1149 | + final CompilationUnit dom = filesToUnits.get(file); |
| 1150 | + if (dom == null) { |
| 1151 | + return; |
| 1152 | + } |
| 1153 | + |
| 1154 | + final TypeElement currentTopLevelType = e.getTypeElement(); |
| 1155 | + UnusedTreeScanner<Void, Void> scanner = new UnusedTreeScanner<>() { |
| 1156 | + @Override |
| 1157 | + public Void visitClass(ClassTree node, Void p) { |
| 1158 | + if (node instanceof JCClassDecl classDecl) { |
| 1159 | + /** |
| 1160 | + * If a Java file contains multiple top-level types, it will |
| 1161 | + * trigger multiple ANALYZE taskEvents for the same compilation |
| 1162 | + * unit. Each ANALYZE taskEvent corresponds to the completion |
| 1163 | + * of analysis for a single top-level type. Therefore, in the |
| 1164 | + * ANALYZE task event listener, we only visit the class and nested |
| 1165 | + * classes that belong to the currently analyzed top-level type. |
| 1166 | + */ |
| 1167 | + if (Objects.equals(currentTopLevelType, classDecl.sym) |
| 1168 | + || !(classDecl.sym.owner instanceof PackageSymbol)) { |
| 1169 | + return super.visitClass(node, p); |
| 1170 | + } else { |
| 1171 | + return null; // Skip if it does not belong to the currently analyzed top-level type. |
| 1172 | + } |
| 1173 | + } |
| 1174 | + |
| 1175 | + return super.visitClass(node, p); |
| 1176 | + } |
| 1177 | + }; |
| 1178 | + final CompilationUnitTree unit = e.getCompilationUnit(); |
| 1179 | + try { |
| 1180 | + scanner.scan(unit, null); |
| 1181 | + } catch (Exception ex) { |
| 1182 | + ILog.get().error("Internal error when visiting the AST Tree. " + ex.getMessage(), ex); |
| 1183 | + } |
| 1184 | + |
| 1185 | + List<CategorizedProblem> unusedProblems = scanner.getUnusedPrivateMembers(unusedProblemFactory); |
| 1186 | + if (!unusedProblems.isEmpty()) { |
| 1187 | + addProblemsToDOM(dom, unusedProblems); |
| 1188 | + } |
| 1189 | + |
| 1190 | + List<CategorizedProblem> unusedImports = scanner.getUnusedImports(unusedProblemFactory); |
| 1191 | + List<? extends Tree> topTypes = unit.getTypeDecls(); |
| 1192 | + int typeCount = topTypes.size(); |
| 1193 | + // Once all top level types of this Java file have been resolved, |
| 1194 | + // we can report the unused import to the DOM. |
| 1195 | + if (typeCount <= 1) { |
| 1196 | + addProblemsToDOM(dom, unusedImports); |
| 1197 | + } else if (typeCount > 1 && topTypes.get(typeCount - 1) instanceof JCClassDecl lastType) { |
| 1198 | + if (Objects.equals(currentTopLevelType, lastType.sym)) { |
| 1199 | + addProblemsToDOM(dom, unusedImports); |
| 1200 | + } |
| 1201 | + } |
| 1202 | + } |
| 1203 | + } |
| 1204 | + }); |
| 1205 | + // must be 1st thing added to context |
| 1206 | + context.put(DiagnosticListener.class, diagnosticListener); |
| 1207 | + boolean docEnabled = JavaCore.ENABLED.equals(compilerOptions.get(JavaCore.COMPILER_DOC_COMMENT_SUPPORT)); |
| 1208 | + JavacUtils.configureJavacContext(context, compilerOptions, javaProject, JavacUtils.isTest(javaProject, new org.eclipse.jdt.internal.core.CompilationUnit[] { unitElement })); |
| 1209 | + Options.instance(context).put(Option.PROC, "only"); |
| 1210 | + Optional.ofNullable(Platform.getProduct()) |
| 1211 | + .map(IProduct::getApplication) |
| 1212 | + // if application is not a test runner (so we don't have regressions with JDT test suite because of too many problems |
| 1213 | + .or(() -> Optional.ofNullable(System.getProperty("eclipse.application"))) |
| 1214 | + .filter(name -> !name.contains("test") && !name.contains("junit")) |
| 1215 | + // continue as far as possible to get extra warnings about unused |
| 1216 | + .ifPresent(id -> Options.instance(context).put("should-stop.ifError", CompileState.GENERATE.toString())); |
| 1217 | + var fileManager = (JavacFileManager)context.get(JavaFileManager.class); |
| 1218 | + List<JavaFileObject> fileObjects = new ArrayList<>(); // we need an ordered list of them |
| 1219 | + File unitFile; |
| 1220 | + if (javaProject != null && javaProject.getResource() != null) { |
| 1221 | + // path is relative to the workspace, make it absolute |
| 1222 | + IResource asResource = javaProject.getProject().getParent().findMember(new String(unitElement.getFileName())); |
| 1223 | + if (asResource != null) { |
| 1224 | + unitFile = asResource.getLocation().toFile(); |
| 1225 | + } else { |
| 1226 | + unitFile = new File(new String(unitElement.getFileName())); |
| 1227 | + } |
| 1228 | + } else { |
| 1229 | + unitFile = new File(new String(unitElement.getFileName())); |
| 1230 | + } |
| 1231 | + Path sourceUnitPath = null; |
| 1232 | + if (!unitFile.getName().endsWith(".java") || unitElement.getFileName() == null || unitElement.getFileName().length == 0) { |
| 1233 | + String uri1 = unitFile.toURI().toString().replaceAll("%7C", "/"); |
| 1234 | + if( uri1.endsWith(".class")) { |
| 1235 | + String[] split= uri1.split("/"); |
| 1236 | + String lastSegment = split[split.length-1].replace(".class", ".java"); |
| 1237 | + sourceUnitPath = Path.of(lastSegment); |
| 1238 | + } |
| 1239 | + if( sourceUnitPath == null ) |
| 1240 | + sourceUnitPath = Path.of(new File("whatever.java").toURI()); |
| 1241 | + } else { |
| 1242 | + sourceUnitPath = Path.of(unitFile.toURI()); |
| 1243 | + } |
| 1244 | + var fileObject = fileManager.getJavaFileObject(sourceUnitPath); |
| 1245 | + fileManager.cache(fileObject, CharBuffer.wrap(unitElement.getContents())); |
| 1246 | + |
| 1247 | + CompilationUnit res = null; |
| 1248 | + if (astLevel != ICompilationUnit.NO_AST) { |
| 1249 | + AST ast = createAST(compilerOptions, astLevel, context, reconcileFlags); |
| 1250 | + res = ast.newCompilationUnit(); |
| 1251 | + filesToUnits.put(fileObject, res); |
| 1252 | + } |
| 1253 | + fileObjects.add(fileObject); |
| 1254 | + |
| 1255 | + |
| 1256 | + JCCompilationUnit javacCompilationUnit = null; |
| 1257 | + Iterable<String> options = configureAPIfNecessary(fileManager) ? null : Arrays.asList("-proc:none"); |
| 1258 | + JavacTask task = ((JavacTool)compiler).getTask(null, fileManager, null /* already added to context */, options, List.of() /* already set */, fileObjects, context); |
| 1259 | + { |
| 1260 | + // don't know yet a better way to ensure those necessary flags get configured |
| 1261 | + var javac = com.sun.tools.javac.main.JavaCompiler.instance(context); |
| 1262 | + javac.keepComments = true; |
| 1263 | + javac.genEndPos = true; |
| 1264 | + javac.lineDebugInfo = true; |
| 1265 | + } |
| 1266 | + |
| 1267 | + List<JCCompilationUnit> javacCompilationUnits = new ArrayList<>(); |
| 1268 | + try { |
| 1269 | + var elements = task.parse().iterator(); |
| 1270 | + var aptPath = fileManager.getLocation(StandardLocation.ANNOTATION_PROCESSOR_PATH); |
| 1271 | + if ((reconcileFlags & ICompilationUnit.FORCE_PROBLEM_DETECTION) != 0 |
| 1272 | + || (aptPath != null && aptPath.iterator().hasNext())) { |
| 1273 | + task.analyze(); |
| 1274 | + } |
| 1275 | + |
| 1276 | + Throwable cachedThrown = null; |
| 1277 | + |
| 1278 | + if (elements.hasNext() && elements.next() instanceof JCCompilationUnit u) { |
| 1279 | + javacCompilationUnit = u; |
| 1280 | + javacCompilationUnits.add(u); |
| 1281 | + } else { |
| 1282 | + throw new IllegalStateException("expected a compiled cu"); |
| 1283 | + } |
| 1284 | + try { |
| 1285 | + String rawText = null; |
| 1286 | + try { |
| 1287 | + rawText = fileObject.getCharContent(true).toString(); |
| 1288 | + } catch( IOException ioe) { |
| 1289 | + ILog.get().error(ioe.getMessage(), ioe); |
| 1290 | + return null; |
| 1291 | + } |
| 1292 | + JavacConverter converter = null; |
| 1293 | + if (astLevel != ICompilationUnit.NO_AST) { |
| 1294 | + AST ast = res.ast; |
| 1295 | + converter = new JavacConverter(ast, javacCompilationUnit, context, rawText, docEnabled, -1); |
| 1296 | + converter.populateCompilationUnit(res, javacCompilationUnit); |
| 1297 | + // javadoc problems explicitly set as they're not sent to DiagnosticListener (maybe find a flag to do it?) |
| 1298 | + var javadocProblems = converter.javadocDiagnostics.stream() |
| 1299 | + .map(problemConverter::createJavacProblem) |
| 1300 | + .filter(Objects::nonNull) |
| 1301 | + .toArray(IProblem[]::new); |
| 1302 | + if (javadocProblems.length > 0) { |
| 1303 | + int initialSize = res.getProblems().length; |
| 1304 | + var newProblems = Arrays.copyOf(res.getProblems(), initialSize + javadocProblems.length); |
| 1305 | + System.arraycopy(javadocProblems, 0, newProblems, initialSize, javadocProblems.length); |
| 1306 | + res.setProblems(newProblems); |
| 1307 | + } |
| 1308 | + List<org.eclipse.jdt.core.dom.Comment> javadocComments = new ArrayList<>(); |
| 1309 | + res.accept(new ASTVisitor(true) { |
| 1310 | + @Override |
| 1311 | + public void postVisit(ASTNode node) { // fix some positions |
| 1312 | + if( node.getParent() != null ) { |
| 1313 | + if( node.getStartPosition() < node.getParent().getStartPosition()) { |
| 1314 | + int parentEnd = node.getParent().getStartPosition() + node.getParent().getLength(); |
| 1315 | + if( node.getStartPosition() >= 0 ) { |
| 1316 | + node.getParent().setSourceRange(node.getStartPosition(), parentEnd - node.getStartPosition()); |
| 1317 | + } |
| 1318 | + } |
| 1319 | + } |
| 1320 | + } |
| 1321 | + @Override |
| 1322 | + public boolean visit(Javadoc javadoc) { |
| 1323 | + javadocComments.add(javadoc); |
| 1324 | + return true; |
| 1325 | + } |
| 1326 | + }); |
| 1327 | + addCommentsToUnit(javadocComments, res); |
| 1328 | + addCommentsToUnit(converter.notAttachedComments, res); |
| 1329 | + attachMissingComments(res, context, rawText, converter, compilerOptions); |
| 1330 | + if ((reconcileFlags & ICompilationUnit.ENABLE_STATEMENTS_RECOVERY) == 0) { |
| 1331 | + // remove all possible RECOVERED node |
| 1332 | + res.accept(new ASTVisitor(false) { |
| 1333 | + private boolean reject(ASTNode node) { |
| 1334 | + return (node.getFlags() & ASTNode.RECOVERED) != 0 |
| 1335 | + || (node instanceof FieldDeclaration field && field.fragments().isEmpty()) |
| 1336 | + || (node instanceof VariableDeclarationStatement decl && decl.fragments().isEmpty()); |
| 1337 | + } |
| 1338 | + |
| 1339 | + @Override |
| 1340 | + public boolean preVisit2(ASTNode node) { |
| 1341 | + if (reject(node)) { |
| 1342 | + StructuralPropertyDescriptor prop = node.getLocationInParent(); |
| 1343 | + if ((prop instanceof SimplePropertyDescriptor simple && !simple.isMandatory()) |
| 1344 | + || (prop instanceof ChildPropertyDescriptor child && !child.isMandatory()) |
| 1345 | + || (prop instanceof ChildListPropertyDescriptor)) { |
| 1346 | + node.delete(); |
| 1347 | + } else if (node.getParent() != null) { |
| 1348 | + node.getParent().setFlags(node.getParent().getFlags() | ASTNode.RECOVERED); |
| 1349 | + } |
| 1350 | + return false; // branch will be cut, no need to inspect deeper |
| 1351 | + } |
| 1352 | + return true; |
| 1353 | + } |
| 1354 | + |
| 1355 | + @Override |
| 1356 | + public void postVisit(ASTNode node) { |
| 1357 | + // repeat on postVisit so trimming applies bottom-up |
| 1358 | + preVisit2(node); |
| 1359 | + } |
| 1360 | + }); |
| 1361 | + } |
| 1362 | + } |
| 1363 | + if (resolveBindings) { |
| 1364 | + JavacBindingResolver resolver = new JavacBindingResolver(javaProject, task, context, converter, workingCopyOwner, javacCompilationUnits); |
| 1365 | + resolver.isRecoveringBindings = (reconcileFlags & ICompilationUnit.ENABLE_BINDINGS_RECOVERY) != 0; |
| 1366 | + if (astLevel != ICompilationUnit.NO_AST) { |
| 1367 | + res.ast.setBindingResolver(resolver); |
| 1368 | + } |
| 1369 | + } |
| 1370 | + // |
| 1371 | + if (astLevel != ICompilationUnit.NO_AST) { |
| 1372 | + res.ast.setOriginalModificationCount(res.ast.modificationCount()); // "un-dirty" AST so Rewrite can process it |
| 1373 | + res.ast.setDefaultNodeFlag(res.ast.getDefaultNodeFlag() & ~ASTNode.ORIGINAL); |
| 1374 | + } |
| 1375 | + } catch (Throwable thrown) { |
| 1376 | + if (cachedThrown == null) { |
| 1377 | + cachedThrown = thrown; |
| 1378 | + } |
| 1379 | + ILog.get().error("Internal failure while parsing or converting AST for unit " + new String(unitElement.getFileName())); |
| 1380 | + ILog.get().error(thrown.getMessage(), thrown); |
| 1381 | + } |
| 1382 | + if (!resolveBindings) { |
| 1383 | + destroy(context); |
| 1384 | + } |
| 1385 | + if (cachedThrown != null) { |
| 1386 | + throw new RuntimeException(cachedThrown); |
| 1387 | + } |
| 1388 | + } catch (IOException ex) { |
| 1389 | + ILog.get().error(ex.getMessage(), ex); |
| 1390 | + } |
| 1391 | + |
| 1392 | + // compute grouped problems |
| 1393 | + Map<String, List<CategorizedProblem>> tempGroupedProblems = new HashMap<>(); |
| 1394 | + for (IProblem problem : diagnosticListener.problemCollection) { |
| 1395 | + if (problem instanceof CategorizedProblem categorizedProblem) { |
| 1396 | + tempGroupedProblems.computeIfAbsent(categorizedProblem.getMarkerType(), key -> new ArrayList<>()).add(categorizedProblem); |
| 1397 | + } |
| 1398 | + } |
| 1399 | + for (Entry<String, List<CategorizedProblem>> entry : tempGroupedProblems.entrySet()) { |
| 1400 | + groupedProblems.put(entry.getKey(), entry.getValue().toArray(CategorizedProblem[]::new)); |
| 1401 | + } |
| 1402 | + |
| 1403 | + return res; |
| 1404 | + } |
1108 | 1405 | } |
0 commit comments