diff --git a/extide/gradle/apichanges.xml b/extide/gradle/apichanges.xml index f73c190e8d9a..946a17b5c5a3 100644 --- a/extide/gradle/apichanges.xml +++ b/extide/gradle/apichanges.xml @@ -83,6 +83,19 @@ is the proper place. + + + Gradle project problems have severity and stacktraces + + + + + +

+ Reports from the NB tooling gradle plugin are now annotated by severity. +

+
+
Some Gradle Settings Removed from Use diff --git a/extide/gradle/manifest.mf b/extide/gradle/manifest.mf index 2d2f5c3f1320..6ce14a367421 100644 --- a/extide/gradle/manifest.mf +++ b/extide/gradle/manifest.mf @@ -3,4 +3,4 @@ AutoUpdate-Show-In-Client: true OpenIDE-Module: org.netbeans.modules.gradle/2 OpenIDE-Module-Layer: org/netbeans/modules/gradle/layer.xml OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/gradle/Bundle.properties -OpenIDE-Module-Specification-Version: 2.37 +OpenIDE-Module-Specification-Version: 2.38 diff --git a/extide/gradle/netbeans-gradle-tooling/src/main/java/org/netbeans/modules/gradle/tooling/NbProjectInfoBuilder.java b/extide/gradle/netbeans-gradle-tooling/src/main/java/org/netbeans/modules/gradle/tooling/NbProjectInfoBuilder.java index 66445fc31f2b..ad5ef7d1861d 100644 --- a/extide/gradle/netbeans-gradle-tooling/src/main/java/org/netbeans/modules/gradle/tooling/NbProjectInfoBuilder.java +++ b/extide/gradle/netbeans-gradle-tooling/src/main/java/org/netbeans/modules/gradle/tooling/NbProjectInfoBuilder.java @@ -110,6 +110,7 @@ import org.gradle.plugin.use.PluginId; import org.gradle.util.GradleVersion; import org.netbeans.modules.gradle.tooling.internal.NbProjectInfo; +import org.netbeans.modules.gradle.tooling.internal.NbProjectInfo.Report; /** * @@ -125,6 +126,18 @@ class NbProjectInfoBuilder { * project loader is enabled to FINER level. */ private static final Logger LOG = Logging.getLogger(NbProjectInfoBuilder.class); + + /** + * Maximum recursion depth into structures, arrays and maps. If this depth is reached, the + * builder will record a warning and stop descending. + */ + private static final int MAX_INTROSPECTION_DEPTH = 25; + + /** + * If a warning is recorded during introspection, further warnings will + * be suppressed until the introspector goes up to this level. + */ + private static final int INTROSPECTION_RESET_DEPTH_WARNING = 5; /** * Name of the default extensibility point for various domain objects defined by Gradle. @@ -213,6 +226,7 @@ public ValueAndType(Class type) { public NbProjectInfo buildAll() { adapter.setModel(model); + runAndRegisterPerf(model, "meta", this::detectProjectMetadata); detectProps(model); detectLicense(model); @@ -226,15 +240,13 @@ public NbProjectInfo buildAll() { // introspection is only allowed for gradle 7.4 and above. // TODO: investigate if some of the instrospection could be done for earlier Gradles. sinceGradle("7.0", () -> { + initIntrospection(); + runAndRegisterPerf(model, "detectExtensions", this::detectExtensions); - }); - sinceGradle("7.0", () -> { runAndRegisterPerf(model, "detectPlugins2", this::detectAdditionalPlugins); - }); - sinceGradle("7.0", () -> { runAndRegisterPerf(model, "taskDependencies", this::detectTaskDependencies); + runAndRegisterPerf(model, "taskProperties", this::detectTaskProperties); }); - runAndRegisterPerf(model, "taskProperties", this::detectTaskProperties); runAndRegisterPerf(model, "artifacts", this::detectConfigurationArtifacts); storeGlobalTypes(model); return model; @@ -305,7 +317,7 @@ private void detectTaskProperties(NbProjectInfoModel model) { Class nonDecorated = findNonDecoratedClass(taskClass); taskPropertyTypes.put(task.getName(), nonDecorated.getName()); - inspectObjectAndValues(taskClass, task, task.getName() + ".", globalTypes, taskPropertyTypes, taskProperties, EXCLUDE_TASK_PROPERTIES, true); // NOI18N + startInspectObjectAndValues(taskClass, task, task.getName() + ".", globalTypes, taskPropertyTypes, taskProperties, EXCLUDE_TASK_PROPERTIES, true); // NOI18N } model.getInfo().put("tasks.propertyValues", taskProperties); // NOI18N @@ -433,7 +445,7 @@ private void storeGlobalTypes(NbProjectInfoModel model) { model.getInfo().put("extensions.globalTypes", globalTypes); // NOI18N } - private void detectExtensions(NbProjectInfoModel model) { + private void initIntrospection() { StringBuilder sb = new StringBuilder(); for (String s : IGNORED_SYSTEM_CLASSES_REGEXP) { if (sb.length() > 0) { @@ -442,7 +454,9 @@ private void detectExtensions(NbProjectInfoModel model) { sb.append(s); } ignoreClassesPattern = Pattern.compile(sb.toString()); - + } + + private void detectExtensions(NbProjectInfoModel model) { inspectExtensions("", project.getExtensions()); model.getInfo().put("extensions.propertyTypes", propertyTypes); // NOI18N model.getInfo().put("extensions.propertyValues", values); // NOI18N @@ -539,19 +553,53 @@ private static boolean isPrimitiveOrString(Class c) { * @param propertyTypes * @param defaultValues */ + private void startInspectObjectAndValues(Class clazz, Object object, String prefix, Map> globalTypes, Map propertyTypes, Map defaultValues) { + depth = 0; + inspectObjectAndValues(clazz, object, prefix, globalTypes, propertyTypes, defaultValues, null, true); + } + + private void startInspectObjectAndValues(Class clazz, Object object, String prefix, Map> globalTypes, Map propertyTypes, Map defaultValues, Set excludes, boolean type) { + depth = 0; + inspectObjectAndValues(clazz, object, prefix, globalTypes, propertyTypes, defaultValues, excludes, type); + } + private void inspectObjectAndValues(Class clazz, Object object, String prefix, Map> globalTypes, Map propertyTypes, Map defaultValues) { inspectObjectAndValues(clazz, object, prefix, globalTypes, propertyTypes, defaultValues, null, true); } + /** + * The current introspection depth + */ + private int depth; + + /** + * If true, depth exceeded warnings will not be logged. + */ + private boolean suppressDepthWarning; private void inspectObjectAndValues(Class clazz, Object object, String prefix, Map> globalTypes, Map propertyTypes, Map defaultValues, Set excludes, boolean type) { if (valueIdentities.put(object, Boolean.TRUE) == Boolean.TRUE) { return; } try { + if (depth++ >= MAX_INTROSPECTION_DEPTH) { + if (!suppressDepthWarning) { + LOG.warn("Too deep structure, truncating"); + model.noteProblem(Report.Severity.WARNING, + String.format("Object structure too deep encountered in class %s", clazz), + String.format("Object structure is too deep for the project model builder. This is unlikely to affect basic project operations. " + + "Check logs and report this issue. The path for the object: %s, current value: %s", prefix, object)); + suppressDepthWarning = true; + } + return; + } else if (depth <= INTROSPECTION_RESET_DEPTH_WARNING) { + suppressDepthWarning = false; + } inspectObjectAndValues0(clazz, object, prefix, globalTypes, propertyTypes, defaultValues, excludes, type); } catch (RuntimeException ex) { LOG.warn("Error during inspection of {}, value {}, prefix {}", clazz, object, prefix); + model.noteProblem(ex, true); } finally { + depth--; valueIdentities.remove(object); } } @@ -753,7 +801,7 @@ private void dumpContainerProperties(Map m, String prefix, Map prope Object v = m.get(k); if (v == null) { defaultValues.put(prefix + "." + k, null); // NOI18N - } else { - defaultValues.put(prefix + "." + k, Objects.toString(v)); // NOI18N + } else if (!v.equals(value)) { + defaultValues.put(prefix + "." + k, objectToString(v)); // NOI18N inspectObjectAndValues(v.getClass(), v, newPrefix, globalTypes, propertyTypes, defaultValues, null, false); } } @@ -821,7 +869,7 @@ private boolean dumpValue(Object value, String prefix, Map prope String newPrefix = prefix + "[" + index + "]."; // NOI18N if (o == null) { defaultValues.put(prefix + "[" + index + "]", null); //NOI18N - } else { + } else if (!o.equals(value)) { defaultValues.put(prefix + "[" + index + "]", Objects.toString(o)); //NOI18N inspectObjectAndValues(o.getClass(), o, newPrefix, globalTypes, propertyTypes, defaultValues, null, false); } @@ -869,8 +917,8 @@ private boolean dumpValue(Object value, String prefix, Map prope Object v = mvalue.get(o); if (v == null) { defaultValues.put(newPrefix, null); // NOI18N - } else { - defaultValues.put(newPrefix, Objects.toString(v)); // NOI18N + } else if (!v.equals(value)) { + defaultValues.put(newPrefix, objectToString(v)); // NOI18N inspectObjectAndValues(v.getClass(), v, newPrefix + ".", globalTypes, propertyTypes, defaultValues, null, itemClass == null); } } @@ -880,6 +928,17 @@ private boolean dumpValue(Object value, String prefix, Map prope return dumped; } + private static String objectToString(Object o) { + if (o instanceof Map || o instanceof Iterable) { + return o.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(o)); + } + try { + return Objects.toString(o); + } catch (RuntimeException ex) { + return o.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(o)); + } + } + private static Class findNonDecoratedClass(Class clazz) { while (clazz != Object.class && (clazz.getModifiers() & 0x1000 /* Modifiers.SYNTHETIC */) > 0) { clazz = clazz.getSuperclass(); @@ -930,7 +989,7 @@ private void inspectExtensions(String prefix, ExtensionContainer container) { Class c = findNonDecoratedClass(ext.getClass()); propertyTypes.put(prefix + extName, c.getName()); - inspectObjectAndValues(ext.getClass(), ext, prefix + extName + ".", globalTypes, propertyTypes, values); + startInspectObjectAndValues(ext.getClass(), ext, prefix + extName + ".", globalTypes, propertyTypes, values); if (ext instanceof ExtensionAware) { inspectExtensions(prefix + extName + ".", ((ExtensionAware)ext).getExtensions()); // NOI18N } @@ -986,7 +1045,8 @@ private void detectProjectMetadata(NbProjectInfoModel model) { try { model.getInfo().put("buildClassPath", storeSet(project.getBuildscript().getConfigurations().getByName("classpath").getFiles())); } catch (RuntimeException e) { - model.noteProblem(e); + // unexpected exception, build classpath should be available. + model.noteProblem(e, true); } Set tasks = new HashSet<>(); for (org.gradle.api.Task t : project.getTasks()) { @@ -1206,7 +1266,7 @@ private void detectSources(NbProjectInfoModel model) { } catch(Exception e) { convertOfflineException(e); // will not be reached - model.noteProblem(e); + model.noteProblem(e, false); } sinceGradle("4.6", () -> { try { @@ -1214,7 +1274,7 @@ private void detectSources(NbProjectInfoModel model) { } catch(Exception e) { convertOfflineException(e); // will not be reached - model.noteProblem(e); + model.noteProblem(e, false); } model.getInfo().put(propBase + "configuration_annotation", getProperty(sourceSet, "annotationProcessorConfigurationName")); }); @@ -1230,6 +1290,7 @@ private void detectSources(NbProjectInfoModel model) { } } else { model.getInfo().put("sourcesets", Collections.emptySet()); + // TODO: should be this converted to Problem, severity INFO ? model.noteProblem("No sourceSets found on this project. This project mightbe a Model/Rule based one which is not supported at the moment."); } } @@ -1246,12 +1307,14 @@ private void detectArtifacts(NbProjectInfoModel model) { try { model.getInfo().put("exploded_war_dir", getProperty(project, "explodedWar", "destinationDir")); } catch(Exception e) { - model.noteProblem(e); + // probably not an internal error, but misconfiguration, omit trace + model.noteProblem(e, false); } try { model.getInfo().put("web_classpath", getProperty(project, "war", "classpath", "files")); } catch(Exception e) { - model.noteProblem(e); + // probably not an internal error, but misconfiguration, omit trace + model.noteProblem(e, false); } } Map archives = new HashMap<>(); @@ -1540,7 +1603,7 @@ private void detectDependencies(NbProjectInfoModel model) { walker.walkResolutionResult(it.getIncoming().getResolutionResult().getRoot()); } catch (ResolveException ex) { - model.noteProblem(ex); + model.noteProblem(ex, false); } } else { unresolvedIds.addAll(componentIds); diff --git a/extide/gradle/netbeans-gradle-tooling/src/main/java/org/netbeans/modules/gradle/tooling/NbProjectInfoModel.java b/extide/gradle/netbeans-gradle-tooling/src/main/java/org/netbeans/modules/gradle/tooling/NbProjectInfoModel.java index b634992ea149..0094c611126c 100644 --- a/extide/gradle/netbeans-gradle-tooling/src/main/java/org/netbeans/modules/gradle/tooling/NbProjectInfoModel.java +++ b/extide/gradle/netbeans-gradle-tooling/src/main/java/org/netbeans/modules/gradle/tooling/NbProjectInfoModel.java @@ -20,12 +20,15 @@ package org.netbeans.modules.gradle.tooling; import java.io.Serializable; +import java.util.Arrays; import org.netbeans.modules.gradle.tooling.internal.NbProjectInfo; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; import org.gradle.internal.exceptions.LocationAwareException; import org.gradle.internal.exceptions.MultiCauseException; import org.gradle.internal.resolve.ModuleVersionResolveException; @@ -76,27 +79,35 @@ void noteProblem(String s) { * yet, but contains exception class as a data for possible matching. * @param e reported issue as a Throwable. */ - void noteProblem(Throwable e) { + void noteProblem(Throwable e, boolean unexpected) { if (e instanceof MultiCauseException && !(e instanceof ModuleVersionResolveException)) { // only handle wrapper multi-causes. They may appear in the middle of the cause chain, too, but // it's not yet obvious if the multi-cause errors are actually useful. MultiCauseException mce = (MultiCauseException)e; for (Throwable t : mce.getCauses()) { - Report r = createReport(t, false); + Report r = createReport(t, false, unexpected); if (r != null) { - ReportImpl outer = createReport(e, true); + ReportImpl outer = createReport(e, true, unexpected); outer.addCause(r); reports.add(outer); } } } else { - Report r = createReport(e, false); + Report r = createReport(e, false, unexpected); if (r != null) { reports.add(r); } } } + void noteProblem(Report.Severity severity, String message, String detail) { + if (message == null) { + return; + } + Report r = new ReportImpl(severity, message, detail); + reports.add(r); + } + /** * Replicates the Throwable into a Report. If shallow is false, replicates * the whole cause chain into a chain of Reports. @@ -104,27 +115,29 @@ void noteProblem(Throwable e) { * @param shallow if true, will encode just the throwable, not its chain * @return created Report */ - private ReportImpl createReport(Throwable e, boolean shallow) { + private ReportImpl createReport(Throwable e, boolean shallow, boolean exc) { if (e == null) { return null; } ReportImpl report; Throwable reported = e; - + StackTraceElement[] els = e.getStackTrace(); + String detail = Arrays.asList(els).stream().map(Objects::toString).collect(Collectors.joining("\n")); + Report.Severity s = exc ? Report.Severity.EXCEPTION : Report.Severity.ERROR; if (e instanceof LocationAwareException) { LocationAwareException lae = (LocationAwareException)e; reported = lae.getCause(); - report = new ReportImpl(reported.getClass().getName(), lae.getLocation(), lae.getLineNumber(), reported.getMessage()); + report = new ReportImpl(s, reported.getClass().getName(), lae.getLocation(), lae.getLineNumber(), reported.getMessage(), exc ? detail : null); } else { - report = new ReportImpl(reported.getClass().getName(), null, -1, e.getMessage()); + report = new ReportImpl(s, reported.getClass().getName(), null, -1, e.getMessage(), exc ? detail : null); reported = e; } if (shallow) { return report; } if (e.getCause() != null && e.getCause() != reported) { - Report nested = createReport(e.getCause(), false); + Report nested = createReport(e.getCause(), false, exc); if (nested != null) { report.addCause(nested); } @@ -150,13 +163,26 @@ static class ReportImpl implements Serializable, Report { private final String scriptLocation; private final int lineNumber; private final String message; + private final String detail; + private final Severity severity; private Report cause; + + public ReportImpl(Severity severity, String message, String detail) { + this.severity = severity; + this.detail = detail; + this.errorClass = null; + this.scriptLocation = null; + this.lineNumber = -1; + this.message = message; + } - public ReportImpl(String errorClass, String scriptLocation, int lineNumber, String message) { + public ReportImpl(Severity severity, String errorClass, String scriptLocation, int lineNumber, String message, String detail) { + this.severity = severity; this.errorClass = errorClass; this.scriptLocation = scriptLocation; this.lineNumber = lineNumber; this.message = message; + this.detail = detail; } public String getErrorClass() { @@ -182,5 +208,13 @@ public void addCause(Report cause) { public Report getCause() { return cause; } + + public Severity getSeverity() { + return severity; + } + + public String getDetail() { + return detail; + } } } diff --git a/extide/gradle/netbeans-gradle-tooling/src/main/java/org/netbeans/modules/gradle/tooling/NetBeansToolingPlugin.java b/extide/gradle/netbeans-gradle-tooling/src/main/java/org/netbeans/modules/gradle/tooling/NetBeansToolingPlugin.java index 0fe4a153339f..40d3d1b3bad3 100644 --- a/extide/gradle/netbeans-gradle-tooling/src/main/java/org/netbeans/modules/gradle/tooling/NetBeansToolingPlugin.java +++ b/extide/gradle/netbeans-gradle-tooling/src/main/java/org/netbeans/modules/gradle/tooling/NetBeansToolingPlugin.java @@ -106,7 +106,8 @@ public Object buildAll(String modelName, Project prj) { Throwable cause = ex; while ((cause != null) && (cause.getCause() != cause)) { if (cause instanceof GradleException) { - ret.noteProblem((GradleException) cause); + // unexpected exceptions at this level + ret.noteProblem((GradleException) cause, true); break; } cause = cause.getCause(); diff --git a/extide/gradle/netbeans-gradle-tooling/src/main/java/org/netbeans/modules/gradle/tooling/internal/NbProjectInfo.java b/extide/gradle/netbeans-gradle-tooling/src/main/java/org/netbeans/modules/gradle/tooling/internal/NbProjectInfo.java index 54016b47e72f..afceb0878207 100644 --- a/extide/gradle/netbeans-gradle-tooling/src/main/java/org/netbeans/modules/gradle/tooling/internal/NbProjectInfo.java +++ b/extide/gradle/netbeans-gradle-tooling/src/main/java/org/netbeans/modules/gradle/tooling/internal/NbProjectInfo.java @@ -53,10 +53,19 @@ public interface NbProjectInfo extends Model, org.gradle.tooling.model.Model { * @since 2.23 */ interface Report { + /** + * Severity of the report. + */ + enum Severity { + EXCEPTION, ERROR, WARNING, INFO + } + + public Severity getSeverity(); public String getErrorClass(); public String getScriptLocation(); public int getLineNumber(); public String getMessage(); + public String getDetail(); public Report getCause(); } } diff --git a/extide/gradle/src/org/netbeans/modules/gradle/GradleProject.java b/extide/gradle/src/org/netbeans/modules/gradle/GradleProject.java index 04fda6a7a209..fa21c50be5af 100644 --- a/extide/gradle/src/org/netbeans/modules/gradle/GradleProject.java +++ b/extide/gradle/src/org/netbeans/modules/gradle/GradleProject.java @@ -135,11 +135,12 @@ private static void setProblems(GradleBaseProject baseProject, Set NbGradleProjectImpl.ACCESSOR.setProblems(baseProject, problems); } - public static GradleReport createGradleReport(String errorClass, String location, int line, String message, GradleReport causedBy) { - return NbGradleProjectImpl.ACCESSOR.createReport(errorClass, location, line, message, causedBy); + public static GradleReport createGradleReport(GradleReport.Severity severity, String errorClass, String location, int line, String message, + GradleReport causedBy, String... traceLines) { + return NbGradleProjectImpl.ACCESSOR.createReport(severity, errorClass, location, line, message, causedBy, traceLines); } - public static GradleReport createGradleReport(Path script, String message) { - return createGradleReport(null, Objects.toString(script), -1, message, null); + public static GradleReport createGradleReport(Path script, String message, String... detail) { + return createGradleReport(GradleReport.Severity.ERROR, null, Objects.toString(script), -1, message, null, detail); } } diff --git a/extide/gradle/src/org/netbeans/modules/gradle/GradleProjectProblemProvider.java b/extide/gradle/src/org/netbeans/modules/gradle/GradleProjectProblemProvider.java index 98e96a3b2f6d..5112676e6b83 100644 --- a/extide/gradle/src/org/netbeans/modules/gradle/GradleProjectProblemProvider.java +++ b/extide/gradle/src/org/netbeans/modules/gradle/GradleProjectProblemProvider.java @@ -24,6 +24,7 @@ import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; +import java.util.Arrays; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -43,6 +44,11 @@ */ @ProjectServiceProvider(service = ProjectProblemsProvider.class, projectType = NbGradleProject.GRADLE_PROJECT_TYPE) public class GradleProjectProblemProvider implements ProjectProblemsProvider { + /** + * Maximum number of lines presented from a report. Prevents stacktrace errors to flood everything, but Gradle has deep + * stacks... + */ + private static final int MAX_REPORT_LINES = 100; private final PropertyChangeSupport support = new PropertyChangeSupport(this); private final Project project; @@ -90,8 +96,25 @@ public Collection getProblems() { } else { for (GradleReport report : gp.getProblems()) { String problem = formatReport(report); - String[] lines = problem.split("\\n"); //NOI18N - ret.add(ProjectProblem.createWarning(lines[0], problem.replaceAll("\\n", "
"), null)); //NOI18N + String m; + String d; + if (report.getDetails() == null || report.getDetails().length == 0) { + String[] lines = problem.split("\n"); //NOI18N + m = lines[0]; + d = problem.replaceAll("\n", "
"); + } else { + m = problem; + d = String.join("\n", Arrays.asList(report.getDetails()).subList(0, Math.min(report.getDetails().length, MAX_REPORT_LINES))); + } + switch (report.getSeverity()) { + case ERROR: + case EXCEPTION: + ret.add(ProjectProblem.createError(m, d, null)); //NOI18N + break; + case WARNING: + ret.add(ProjectProblem.createWarning(m, d, null)); //NOI18N + break; + } } } return ret; diff --git a/extide/gradle/src/org/netbeans/modules/gradle/NbGradleProjectImpl.java b/extide/gradle/src/org/netbeans/modules/gradle/NbGradleProjectImpl.java index b14d7073ae9f..442b9ef7b505 100644 --- a/extide/gradle/src/org/netbeans/modules/gradle/NbGradleProjectImpl.java +++ b/extide/gradle/src/org/netbeans/modules/gradle/NbGradleProjectImpl.java @@ -134,7 +134,8 @@ public abstract static class WatcherAccessor { public abstract void passivate(NbGradleProject watcher); - public abstract GradleReport createReport(String errorClass, String location, int line, String message, GradleReport causedBy); + public abstract GradleReport createReport(GradleReport.Severity severity, String errorClass, String location, int line, String message, + GradleReport causedBy, String[] traceLines); public abstract void setProblems(GradleBaseProject baseProject, Set problems); } diff --git a/extide/gradle/src/org/netbeans/modules/gradle/api/GradleReport.java b/extide/gradle/src/org/netbeans/modules/gradle/api/GradleReport.java index d3a587e9c8e6..77f45458cb8a 100644 --- a/extide/gradle/src/org/netbeans/modules/gradle/api/GradleReport.java +++ b/extide/gradle/src/org/netbeans/modules/gradle/api/GradleReport.java @@ -42,13 +42,50 @@ public final class GradleReport { private final int line; private final String message; private final GradleReport causedBy; - - GradleReport(String errorClass, String location, int line, String message, GradleReport causedBy) { + private final Severity severity; + private final String[] details; + + /** + * Severity of the report. + * + * @since 2.38 + */ + public enum Severity { + /** + * Unexpected exception during project read, most likely an error + * in the project model reading code. + */ + EXCEPTION, + + /** + * Project could not be read, essential project data is missing. + */ + ERROR, + + /** + * Warning that indicates some issue that happened during project model reading, but + * basic model properties are still read. + */ + WARNING, + + /** + * Notices and information messages. + */ + INFO, + } + + GradleReport(Severity severity, String errorClass, String location, int line, String message, GradleReport causedBy, String... details) { + this.severity = severity; this.errorClass = errorClass; this.location = location; this.line = line; this.message = message == null ? "" : message; this.causedBy = causedBy; + this.details = details != null && details.length > 0 ? details : null; + } + + GradleReport(String errorClass, String location, int line, String message, GradleReport causedBy) { + this(Severity.ERROR, errorClass, location, line, message, causedBy); } public @CheckForNull String getLocation() { @@ -77,6 +114,18 @@ public String getErrorClass() { return errorClass; } + /** + * @return the report's severity + * @since 2.38 + */ + public Severity getSeverity() { + return severity; + } + + public String[] getDetails() { + return details; + } + @Override public boolean equals(Object obj) { if (this == obj) { @@ -116,7 +165,10 @@ public String toString() { "FMT_MessageWithLocation={0} ({1}:{2})", "# {0} - the error message", "# {1} - the file", - "FMT_MessageWithLocationNoLine={0} ({1})" + "FMT_MessageWithLocationNoLine={0} ({1})", + "# {0} - the error message", + "# {1} - the stack trace, one line per stack frame", + "FMT_MessageWithTrace={0}\nError stack trace: {1}" }) /** @@ -136,7 +188,13 @@ public String formatReportForHintOrProblem(boolean includeLocation, FileObject r msg = Bundle.FMT_AppendMessage(msg, r2.getMessage()); } if (!includeLocation || getLocation() == null) { - return msg; + // exceptions from gradle contain quite descriptive messages, but runtime exceptions + // and exceptions from the wrapper do not. + if (severity == Severity.EXCEPTION && details != null && details.length >0) { + return Bundle.FMT_MessageWithTrace(msg, String.join("\n", details)); + } else { + return msg; + } } String locString; diff --git a/extide/gradle/src/org/netbeans/modules/gradle/api/NbGradleProject.java b/extide/gradle/src/org/netbeans/modules/gradle/api/NbGradleProject.java index b82bd8b2777a..1fa59878bc40 100644 --- a/extide/gradle/src/org/netbeans/modules/gradle/api/NbGradleProject.java +++ b/extide/gradle/src/org/netbeans/modules/gradle/api/NbGradleProject.java @@ -184,8 +184,9 @@ public void passivate(NbGradleProject watcher) { } @Override - public GradleReport createReport(String errorClass, String location, int line, String message, GradleReport causedBy) { - return new GradleReport(errorClass, location, line, message, causedBy); + public GradleReport createReport(GradleReport.Severity severity, String errorClass, String location, int line, String message, + GradleReport causedBy, String[] traceLines) { + return new GradleReport(severity, errorClass, location, line, message, causedBy, traceLines); } @Override diff --git a/extide/gradle/src/org/netbeans/modules/gradle/cache/ProjectInfoDiskCache.java b/extide/gradle/src/org/netbeans/modules/gradle/cache/ProjectInfoDiskCache.java index b28c2902dca1..5718378b2dd3 100644 --- a/extide/gradle/src/org/netbeans/modules/gradle/cache/ProjectInfoDiskCache.java +++ b/extide/gradle/src/org/netbeans/modules/gradle/cache/ProjectInfoDiskCache.java @@ -45,7 +45,7 @@ public final class ProjectInfoDiskCache extends AbstractDiskCache { // Increase this number if new info is gathered from the projects. - private static final int COMPATIBLE_CACHE_VERSION = 23; + private static final int COMPATIBLE_CACHE_VERSION = 24; private static final String INFO_CACHE_FILE_NAME = "project-info.ser"; //NOI18N private static final Map DISK_CACHES = Collections.synchronizedMap(new WeakHashMap<>()); @@ -104,7 +104,7 @@ private static CacheReport makeReport(Report r) { } else { nested = null; } - return new CacheReport(r.getErrorClass(), r.getScriptLocation(), r.getLineNumber(), r.getMessage(), nested); + return new CacheReport(r.getSeverity(), r.getErrorClass(), r.getScriptLocation(), r.getLineNumber(), r.getMessage(), r.getDetail(), nested); } private static Set makeReports(Collection reps) { @@ -120,16 +120,29 @@ final static class CacheReport implements Report, Serializable { private final String location; private final int line; private final String message; + private final String detail; private final Report causedBy; + private final Severity severity; - public CacheReport(String errorClass, String location, int line, String message, Report causedBy) { + public CacheReport(Severity severity, String errorClass, String location, int line, String message, String detail, Report causedBy) { + this.severity = severity; this.errorClass = errorClass; this.location = location; this.line = line; this.message = message; + this.detail = detail; this.causedBy = causedBy; } + public CacheReport(Severity severity, String message, String detail) { + this.severity = severity; + this.errorClass = null; + this.location = null; + this.line = -1; + this.message = message; + this.detail = detail; + this.causedBy = null; + } public String getErrorClass() { return errorClass; } @@ -151,6 +164,14 @@ public int getLineNumber() { return causedBy; } + public Severity getSeverity() { + return severity; + } + + public String getDetail() { + return detail; + } + @Override public int hashCode() { int hash = 3; @@ -178,6 +199,9 @@ public boolean equals(Object obj) { if (!Objects.equals(this.location, other.location)) { return false; } + if (!Objects.equals(this.severity, other.severity)) { + return false; + } return Objects.equals(this.message, other.message); } diff --git a/extide/gradle/src/org/netbeans/modules/gradle/loaders/LegacyProjectLoader.java b/extide/gradle/src/org/netbeans/modules/gradle/loaders/LegacyProjectLoader.java index d0d30f28e311..518aab7bd6b4 100644 --- a/extide/gradle/src/org/netbeans/modules/gradle/loaders/LegacyProjectLoader.java +++ b/extide/gradle/src/org/netbeans/modules/gradle/loaders/LegacyProjectLoader.java @@ -26,7 +26,9 @@ import java.io.OutputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; +import java.io.PrintWriter; import java.io.Serializable; +import java.io.StringWriter; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; @@ -214,14 +216,23 @@ private static GradleProject loadGradleProject(ReloadContext ctx, CancellationTo LOG.finer(String.format(" %-20s:%s", s, o)); } } + List errorReports = info.getReports().stream().filter(r -> r.getSeverity() == Report.Severity.ERROR).collect(Collectors.toList()); if (!info.getProblems().isEmpty()) { errors.openNotification( TIT_LOAD_ISSUES(base.getProjectDir().getName()), TIT_LOAD_ISSUES(base.getProjectDir().getName()), GradleProjectErrorNotifications.bulletedList(info.getProblems())); } + if (!info.getReports().isEmpty()) { + errors.openNotification( + TIT_LOAD_ISSUES(base.getProjectDir().getName()), + TIT_LOAD_ISSUES(base.getProjectDir().getName()), + GradleProjectErrorNotifications.bulletedList( + info.getReports().stream().map(r -> r.getMessage()).collect(Collectors.toList()) + )); + } if (!info.hasException()) { - if (!info.getProblems().isEmpty() || !info.getReports().isEmpty()) { + if (!info.getProblems().isEmpty() || !errorReports.isEmpty()) { if (LOG.isLoggable(Level.FINE)) { // If we do not have exception, but seen some problems the we mark the quality as SIMPLE Object o = new ArrayList(info.getReports().stream(). @@ -247,7 +258,7 @@ private static GradleProject loadGradleProject(ReloadContext ctx, CancellationTo quality = onlineResult.get() ? Quality.FULL_ONLINE : Quality.FULL; } } else { - if (info.getProblems().isEmpty() && info.getReports().isEmpty()) { + if (info.getProblems().isEmpty() && errorReports.isEmpty()) { String problem = info.getGradleException(); String[] lines = problem.split("\n"); LOG.log(INFO, "Failed to retrieve project information for: {0}\nReason: {1}", new Object[] {base.getProjectDir(), problem}); //NOI18N @@ -317,9 +328,27 @@ static GradleReport copyReport(Report orig) { loc = m.group(1); } } + GradleReport.Severity s; + if (orig.getSeverity() == null) { + s = GradleReport.Severity.ERROR; + } else switch (orig.getSeverity()) { + case INFO: + s = GradleReport.Severity.INFO; + break; + case WARNING: + s = GradleReport.Severity.WARNING; + break; + case EXCEPTION: + s = GradleReport.Severity.EXCEPTION; + break; + default: + s = GradleReport.Severity.ERROR; + break; + } - return GradleProject.createGradleReport(orig.getErrorClass(), loc, orig.getLineNumber(), orig.getMessage(), - orig.getCause() == null ? null : copyReport(orig.getCause())); + String[] lines = orig.getDetail() == null ? null : orig.getDetail().split("\n"); + return GradleProject.createGradleReport(s, orig.getErrorClass(), loc, orig.getLineNumber(), orig.getMessage(), + orig.getCause() == null ? null : copyReport(orig.getCause()), lines); } private static List causesToProblems(Throwable ex) { @@ -359,7 +388,7 @@ private static List exceptionsToProblems(File script, Throwable t) if (!(t instanceof GradleConnectionException)) { return causesToProblems(t); } - return Collections.singletonList(createReport(t.getCause())); + return Collections.singletonList(createReport(script, t.getCause(), new boolean[1])); } private static String getLocation(Throwable locationAwareEx) { @@ -399,7 +428,11 @@ private static int getLineNumber(Throwable locationAwareEx) { * @param e the throwable * @return head of {@link GradleRepor} chain. */ - private static GradleReport createReport(Throwable e) { + private static GradleReport createReport(File p, Throwable e, boolean[] user) { + return createReport(p, e, true, user); + } + + private static GradleReport createReport(File p, Throwable e, boolean top, boolean[] user) { if (e == null) { return null; } @@ -410,6 +443,7 @@ private static GradleReport createReport(Throwable e) { GradleReport nested = null; if (e.getClass().getName().endsWith("LocationAwareException")) { // NOI18N + user[0] = true; String rawLoc = getLocation(e); if (rawLoc != null) { Matcher m = FILE_PATH_FROM_LOCATION.matcher(rawLoc); @@ -420,10 +454,30 @@ private static GradleReport createReport(Throwable e) { } else { reported = e; } + String cn = e.getClass().getName(); + if (cn.contains("GradleScriptException") || cn.contains("ResolutionException")) { + user[0] = true; + } if (reported.getCause() != null && reported.getCause() != reported) { - nested = createReport(reported.getCause()); + nested = createReport(p, reported.getCause(), false, user); + } + String m = reported.getMessage(); + if (m == null) { + m = reported.getClass().getSimpleName(); + } + String[] traceLines = null; + if (top) { + LOG.log(Level.WARNING, "Loading of script {0} threw an exception {2}", new Object[] { p, reported.getClass().getName() } ); + // need to log the exception at severity INFO so it does not appear as a red problem in Notifications. + LOG.log(Level.INFO, "Stacktrace from gradle daemon:", reported); + StringWriter sw = new StringWriter(); + try (PrintWriter pw = new PrintWriter(sw)) { + reported.printStackTrace(pw); + } + String[] l = sw.toString().split("\n"); + traceLines = Arrays.copyOf(l, Math.min(l.length, 100)); } - return GradleProject.createGradleReport(reported.getClass().getName(), loc, line, reported.getMessage(), nested); + return GradleProject.createGradleReport(GradleReport.Severity.ERROR, reported.getClass().getName(), loc, line, m, nested, user[0] ? null : traceLines); } private static BuildActionExecuter createInfoAction(ProjectConnection pconn, GradleCommandLine cmd, CancellationToken token, ProgressListener pl) { diff --git a/extide/gradle/test/unit/data/projects/spotbugs/.gitignore b/extide/gradle/test/unit/data/projects/spotbugs/.gitignore new file mode 100644 index 000000000000..2dbe6b8b9a52 --- /dev/null +++ b/extide/gradle/test/unit/data/projects/spotbugs/.gitignore @@ -0,0 +1,2 @@ +/.gradle +/build/* \ No newline at end of file diff --git a/extide/gradle/test/unit/data/projects/spotbugs/build.gradle b/extide/gradle/test/unit/data/projects/spotbugs/build.gradle new file mode 100644 index 000000000000..831eb49f07b1 --- /dev/null +++ b/extide/gradle/test/unit/data/projects/spotbugs/build.gradle @@ -0,0 +1,46 @@ +plugins { + id("com.github.spotbugs") version "6.0.0-beta.4" + id("application") + id("java") +} + +description = "Test for a bug" +group = "com.example" +version = "1.0.0-2023.10.12" + +def artifactName = "testforbug" +def moduleName = "com.example.gradlespotbugs" + +def versionAsm = "9.5" +def versionJcip = "1.0-1" +def versionSpotBugs = "4.7.3" +def versionSlf4j = "2.0.9" + +dependencies { + // SpotBugs + plugins + spotbugs(group: "com.github.spotbugs", name: "spotbugs", version: versionSpotBugs) + spotbugs(group: "org.slf4j", name: "slf4j-api", version: versionSlf4j) + spotbugs(group: "org.slf4j", name: "slf4j-simple", version: versionSlf4j) + spotbugs(group: "org.ow2.asm", name: "asm", version: versionAsm) + compileOnly(group: "com.github.spotbugs", name: "spotbugs-annotations", version: versionSpotBugs) + compileOnly(group: "com.github.stephenc.jcip", name: "jcip-annotations", version: versionJcip) +} + +repositories { + mavenLocal() + mavenCentral() +} + +application { + mainClass = "com.example.gradlespotbugs.Hello" +} + +var recursiveObject = new HashMap(); +var intermediateMap = new HashMap(); + +recursiveObject.put("key1", recursiveObject); +recursiveObject.put("key2", intermediateMap); +intermediateMap.put("key3", recursiveObject); + +project.ext.set("recursiveProperty", recursiveObject); + diff --git a/extide/gradle/test/unit/data/projects/spotbugs/settings.gradle b/extide/gradle/test/unit/data/projects/spotbugs/settings.gradle new file mode 100644 index 000000000000..fb85df824f8d --- /dev/null +++ b/extide/gradle/test/unit/data/projects/spotbugs/settings.gradle @@ -0,0 +1 @@ +rootProject.name = "Hello World" \ No newline at end of file diff --git a/extide/gradle/test/unit/data/projects/spotbugs/src/main/java/com/example/gradlespotbugs/Hello.java b/extide/gradle/test/unit/data/projects/spotbugs/src/main/java/com/example/gradlespotbugs/Hello.java new file mode 100644 index 000000000000..e736a2d248fe --- /dev/null +++ b/extide/gradle/test/unit/data/projects/spotbugs/src/main/java/com/example/gradlespotbugs/Hello.java @@ -0,0 +1,7 @@ +package com.example.gradlespotbugs; + +public class Hello { + public static void main(String[] args) { + System.out.println("Hello World"); + } +} \ No newline at end of file diff --git a/extide/gradle/test/unit/data/projects/spotbugs/src/main/java/com/example/gradlespotbugs/package-info.java b/extide/gradle/test/unit/data/projects/spotbugs/src/main/java/com/example/gradlespotbugs/package-info.java new file mode 100644 index 000000000000..c1867caf0495 --- /dev/null +++ b/extide/gradle/test/unit/data/projects/spotbugs/src/main/java/com/example/gradlespotbugs/package-info.java @@ -0,0 +1 @@ +package com.example.gradlespotbugs; \ No newline at end of file diff --git a/extide/gradle/test/unit/data/projects/spotbugs/src/main/java/module-info.java b/extide/gradle/test/unit/data/projects/spotbugs/src/main/java/module-info.java new file mode 100644 index 000000000000..8d35ca7ca927 --- /dev/null +++ b/extide/gradle/test/unit/data/projects/spotbugs/src/main/java/module-info.java @@ -0,0 +1,4 @@ +@SuppressWarnings({ "requires-automatic", "requires-transitive-automatic" }) +module com.example.gradlespotbugs { + requires transitive static com.github.spotbugs.annotations; +} \ No newline at end of file diff --git a/extide/gradle/test/unit/src/org/netbeans/modules/gradle/NbGradleProjectImpl2Test.java b/extide/gradle/test/unit/src/org/netbeans/modules/gradle/NbGradleProjectImpl2Test.java index 221864b38877..a96ad76b0345 100644 --- a/extide/gradle/test/unit/src/org/netbeans/modules/gradle/NbGradleProjectImpl2Test.java +++ b/extide/gradle/test/unit/src/org/netbeans/modules/gradle/NbGradleProjectImpl2Test.java @@ -23,6 +23,9 @@ import org.netbeans.api.project.ProjectManager; import org.netbeans.api.project.ui.OpenProjects; import org.netbeans.api.project.ui.ProjectProblems; +import org.netbeans.modules.gradle.api.BuildPropertiesSupport; +import org.netbeans.modules.gradle.api.BuildPropertiesSupport.Property; +import org.netbeans.modules.gradle.api.BuildPropertiesSupport.PropertyKind; import org.netbeans.modules.gradle.api.GradleBaseProject; import org.netbeans.modules.gradle.api.NbGradleProject; import org.netbeans.modules.projectapi.nb.NbProjectManagerAccessor; @@ -110,4 +113,30 @@ public void testForceLoadBrokenProject() throws Exception { assertTrue("Project has still problems", ProjectProblems.isBroken(project)); } + + public void testSpotbugsPlugin() throws Exception { + copyProject("projects/spotbugs"); + ProjectTrust.getDefault().trustProject(project); + + NbGradleProject gp1 = NbGradleProject.get(project); + NbGradleProject loaded = gp1.toQuality("Load test project", NbGradleProject.Quality.FULL, false).toCompletableFuture().get(); + assertNotNull(loaded); + assertTrue(loaded.getQuality().atLeast(NbGradleProject.Quality.FULL)); + + BuildPropertiesSupport props = BuildPropertiesSupport.get(project); + assertNotNull(props); + Property rec = props.findExtensionProperty("", "recursiveProperty"); + assertNotNull(rec); + assertEquals(PropertyKind.MAP, rec.getKind()); + + Property k1 = props.get(rec, "key1", null); + assertNull("Avoid recursion to same object", k1); + + Property k2 = props.get(rec, "key2", null); + assertNotNull(k2); + + Property k3 = props.get(k2, "key3", null); + assertNotNull(k3); + assertEquals("Avoid loop references", PropertyKind.EXISTS, k3.getKind()); + } }