/* * Copyright 2023-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package rife.bld.extension; import rife.bld.BaseProject; import rife.bld.operations.AbstractProcessOperation; import rife.bld.operations.exceptions.ExitStatusException; import rife.tools.exceptions.FileUtilsErrorException; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; /** * Performs static code analysis with Detekt. * * @author Erik C. Thauvin * @since 1.0 */ public class DetektOperation extends AbstractProcessOperation { private static final List DETEKT_JARS = List.of( "annotations-", "contester-breakpoint-", "detekt-", "jcommander-", "kotlin-compiler-embeddable-", "kotlin-daemon-embeddable-", "kotlin-reflect-", "kotlin-script-runtime-", "kotlin-stdlib-", "kotlin-stdlib-jdk7-", "kotlin-stdlib-jdk8-", "kotlinx-html-jvm-", "kotlinx-serialization-", "sarif4k-jvm-", "snakeyaml-engine-", "trove4j-"); private static final Logger LOGGER = Logger.getLogger(DetektReport.class.getName()); private final Collection classpath_ = new ArrayList<>(); private final Collection config_ = new ArrayList<>(); private final Collection input_ = new ArrayList<>(); private final Collection plugins_ = new ArrayList<>(); private final Collection report_ = new ArrayList<>(); private boolean allRules_; private boolean autoCorrect_; private String basePath_; private String baseline_; private boolean buildUponDefaultConfig_; private String configResource_; private boolean createBaseline_; private boolean debug_; private boolean disableDefaultRuleSets_; private String excludes_; private boolean generateConfig_; private String includes_; private String jdkHome_; private String jvmTarget_; private String languageVersion_; private int maxIssues_; private boolean parallel_; private BaseProject project_; /** * Activates all available (even unstable) rules. * * @param allRules {@code true} or {@code false} * @return this operation instance */ public DetektOperation allRules(boolean allRules) { allRules_ = allRules; return this; } /** * Allow rules to autocorrect code if they support it. The default rule * sets do NOT support autocorrecting and won't change any line in the * users code base. However, custom rules can be written to support * autocorrecting. The additional 'formatting' rule set, added with * {@link #plugins(String...) Plugins}, does support it and needs this flag. * * @param autoCorrect {@code true} or {@code false} * @return this operation instance */ public DetektOperation autoCorrect(boolean autoCorrect) { autoCorrect_ = autoCorrect; return this; } /** * Specifies a directory as the base path. Currently, it impacts all file * paths in the formatted reports. File paths in console output and txt * report are not affected and remain as absolute paths. * * @param path the directory path * @return this operation instance */ public DetektOperation basePath(String path) { basePath_ = path; return this; } /** * If a baseline xml file is passed in, only new code smells not in the * baseline are printed in the console. * * @param baseline the baseline xml file * @return this operation instance */ public DetektOperation baseline(String baseline) { baseline_ = baseline; return this; } /** * Preconfigures detekt with a bunch of rules and some opinionated defaults * for you. Allows additional provided configurations to override the * defaults. * * @param buildUponDefaultConfig {@code true} or {@code false} * @return this operation instance */ public DetektOperation buildUponDefaultConfig(boolean buildUponDefaultConfig) { buildUponDefaultConfig_ = buildUponDefaultConfig; return this; } /** * EXPERIMENTAL: Paths where to find user class files and depending jar files. * Used for type resolution. * * @param paths one or more files * @return this operation instance */ public DetektOperation classPath(String... paths) { classpath_.addAll(List.of(paths)); return this; } /** * EXPERIMENTAL: Paths where to find user class files and depending jar files. * Used for type resolution. * * @param paths the list of files * @return this operation instance */ public DetektOperation classPath(Collection paths) { classpath_.addAll(paths); return this; } /** * Path to the config file ({@code path/to/config.yml}). * * @param configs one or more config files * @return this operation instance */ public DetektOperation config(String... configs) { config_.addAll(List.of(configs)); return this; } /** * Path to the config file ({@code path/to/config.yml}). * * @param configs the list pf config files * @return this operation instance */ public DetektOperation config(Collection configs) { config_.addAll(configs); return this; } /** * Path to the config resource on detekt's classpath ({@code path/to/config.yml}). * * @param resource the config resource path * @return this operation instance */ public DetektOperation configResource(String resource) { configResource_ = resource; return this; } /** * Treats current analysis findings as a smell baseline for future detekt * runs. * * @param createBaseline {@code true} or {@code false} * @return this operation instance */ public DetektOperation createBaseline(boolean createBaseline) { createBaseline_ = createBaseline; return this; } /** * Prints extra information about configurations and extensions. * * @param debug {@code true} or {@code false} * @return this operation instance */ public DetektOperation debug(boolean debug) { debug_ = debug; return this; } /** * Disables default rule sets. * * @param disable {@code true} or {@code false} * @return this operation instance */ public DetektOperation disableDefaultRuleSets(boolean disable) { disableDefaultRuleSets_ = disable; return this; } /** * Globbing patterns describing paths to exclude from the analysis. * * @param patterns the patterns * @return this operation instance */ public DetektOperation excludes(String patterns) { excludes_ = patterns; return this; } /** * Performs the operation. * * @throws InterruptedException when the operation was interrupted * @throws IOException when an exception occurred during the execution of the process * @throws FileUtilsErrorException when an exception occurred during the retrieval of the operation output * @throws ExitStatusException when the exit status was changed during the operation */ @Override public void execute() throws IOException, FileUtilsErrorException, InterruptedException, ExitStatusException { super.execute(); if (successful_ && LOGGER.isLoggable(Level.INFO)) { if (createBaseline_) { LOGGER.info("Detekt baseline successfully generated: " + "file://" + new File(baseline_).toURI().getPath()); } else { LOGGER.info("Detekt executed successfully."); } } } /** * Part of the {@link #execute} operation, constructs the command list * to use for building the process. */ @Override protected List executeConstructProcessCommandList() { if (project_ == null) { LOGGER.severe("A project must be specified."); } final List args = new ArrayList<>(); args.add(javaTool()); args.add("-cp"); args.add(getDetektJarList(project_.libBldDirectory())); args.add("io.gitlab.arturbosch.detekt.cli.Main"); // all-rules if (allRules_) { args.add("--all-rules"); } // auto-correct if (autoCorrect_) { args.add("--auto-correct"); } // base-path if (isNotBlank(basePath_)) { args.add("--base-path"); args.add(basePath_); } // baseline if (isNotBlank(baseline_)) { args.add("--baseline"); args.add(baseline_); } // build-upon-default-config if (buildUponDefaultConfig_) { args.add("--build-upon-default-config"); } // classpath if (!classpath_.isEmpty()) { args.add("--classpath"); args.add(String.join(File.pathSeparator, classpath_.stream().filter(this::isNotBlank).toList())); } // config if (!config_.isEmpty()) { args.add("-config"); args.add(String.join(";", config_.stream().filter(this::isNotBlank).toList())); } // config-resource if (isNotBlank(configResource_)) { args.add("--config-resource"); args.add(configResource_); } // create-baseline if (createBaseline_) { args.add("--create-baseline"); } // debug if (debug_) { args.add("--debug"); } // disable-default-rulesets if (disableDefaultRuleSets_) { args.add("--disable-default-rulesets"); } // excludes if (isNotBlank(excludes_)) { args.add("--excludes"); args.add(excludes_); } // generate-config if (generateConfig_) { args.add("--generate-config"); } // includes if (isNotBlank(includes_)) { args.add("--includes"); args.add(includes_); } // input if (!input_.isEmpty()) { args.add("--input"); args.add(String.join(",", input_.stream().filter(this::isNotBlank).toList())); } // jdk-home if (isNotBlank(jdkHome_)) { args.add("--jdk-home"); args.add(jdkHome_); } // jvm-target if (isNotBlank(jvmTarget_)) { args.add("--jvm-target"); args.add(jvmTarget_); } // language-version if (isNotBlank(languageVersion_)) { args.add("--language-version"); args.add(languageVersion_); } // max-issues if (maxIssues_ > 0) { args.add("--max-issues"); args.add(String.valueOf(maxIssues_)); } // parallel if (parallel_) { args.add("--parallel"); } // plugins if (!plugins_.isEmpty()) { args.add("--plugins"); args.add(String.join(",", plugins_.stream().filter(this::isNotBlank).toList())); } // report if (!report_.isEmpty()) { report_.forEach(it -> { args.add("-r"); args.add(it.id().name().toLowerCase() + ":" + it.path()); }); } if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine(String.join(" ", args.stream().filter(this::isNotBlank).toList())); } return args; } /** * Configures the operation from a {@link BaseProject}. *

* Sets the following: *

    *
  • {@link #baseline baseline} to {@code detekt-baseline.xml}, if it exists
  • *
  • {@link #excludes excludes} to exclude {@code build} and {@code resources} directories
  • *
* * @param project the project to configure the operation from * @return this operation instance */ @Override public DetektOperation fromProject(BaseProject project) { project_ = project; var baseline = new File(project.workDirectory(), "detekt-baseline.xml"); if (baseline.exists()) { baseline_ = baseline.getAbsolutePath(); } excludes(".*/build/.*,.*/resources/.*"); return this; } /** * Export default config. Path can be specified with {@link #config config} option. *

* Default path: {@code default-detekt-config.yml} * * @param generate {@code true} or {@code false} * @return this operation instance */ public DetektOperation generateConfig(boolean generate) { generateConfig_ = generate; return this; } /* * Retrieves the matching JARs files from the given directory. */ private String getDetektJarList(File directory) { var jars = new ArrayList(); if (directory.isDirectory()) { var files = directory.listFiles(); if (files != null) { for (var f : files) { if (!f.getName().endsWith("-sources.jar") && !f.getName().endsWith("-javadoc.jar")) { for (var m : DETEKT_JARS) { if (f.getName().startsWith(m)) { jars.add(f.getAbsolutePath()); break; } } } } } } return String.join(":", jars); } /** * Globbing patterns describing paths to include in the analysis. Useful in * combination with {@link #excludes(String) excludes} patterns. * * @param patterns the patterns * @return this operation instance */ public DetektOperation includes(String patterns) { includes_ = patterns; return this; } /** * Input paths to analyze. If not specified the current working directory is used. * * @param paths the list of paths * @return this operation instance */ public DetektOperation input(Collection paths) { input_.addAll(paths); return this; } /** * Input paths to analyze. If not specified the current working directory is used. * * @param paths one or more paths * @return this operation instance */ public DetektOperation input(String... paths) { input_.addAll(List.of(paths)); return this; } /** * Returns the input paths to analyze. * * @return the input paths */ public Collection input() { return input_; } /* * Determines if a string is not blank. */ private boolean isNotBlank(String s) { return s != null && !s.isBlank(); } /** * EXPERIMENTAL: Use a custom JDK home directory to include into the * classpath. * * @param path the JDK home directory path * @return this operation instance */ public DetektOperation jdkHome(String path) { jdkHome_ = path; return this; } /** * EXPERIMENTAL: Target version of the generated JVM bytecode that was * generated during compilation and is now being used for type resolution *

* Default: 1.8 * * @param target the target version * @return this operation instance */ public DetektOperation jvmTarget(String target) { jvmTarget_ = target; return this; } /** * EXPERIMENTAL: Compatibility mode for Kotlin language version X.Y, * reports errors for all language features that came out later. *

* Possible Values: [1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, 2.1] * * @param version the version * @return this operation instance */ public DetektOperation languageVersion(String version) { languageVersion_ = version; return this; } /** * Return exit code 0 only when found issues count does not exceed * specified issues count. * * @param max the issues code * @return this operation instance */ public DetektOperation maxIssues(int max) { maxIssues_ = max; return this; } /** * Enables parallel compilation and analysis of source files. Do some * benchmarks first before enabling this flag. Heuristics show performance * benefits starting from 2000 lines of Kotlin code. * * @param parallel {@code true} or {@code false} * @return this operation instance */ public DetektOperation parallel(boolean parallel) { parallel_ = parallel; return this; } /** * Extra paths to plugin jars. * * @param jars one or more jars * @return this operation instance */ public DetektOperation plugins(String... jars) { plugins_.addAll(List.of(jars)); return this; } /** * Extra paths to plugin jars. * * @param jars the list of jars * @return this operation instance */ public DetektOperation plugins(Collection jars) { plugins_.addAll(jars); return this; } /** * Generates a report for given {@link DetektReportId report-id} and stores it on given 'path'. * * @param reports one or more reports * @return this operation instance */ public DetektOperation report(DetektReport... reports) { report_.addAll(List.of(reports)); return this; } }