Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Within a class table the methods keep the sort order, and the class tables thems
Use `--debug` to print the time of each stage (`config`, `discovery`, `analysis`, `sorting`, `output`) and the counters `filesFound`, `filesAnalysed`, `filesFailed`, `methodsAnalysed`, `cacheHits` and `cacheMisses` after the run.
On a terminal, a progress line written to the terminal itself shows how many files are analysed; it never reaches redirected output.

Write an HTML, CSV or JSON report instead:
Write a JSON, CSV, HTML or Markdown report instead, see [Reports](./docs/Reports.md):

```bash
java -jar target/cognitive-analysis.jar analyse src/main/java --report-type html --report-file report.html
Expand Down
2 changes: 1 addition & 1 deletion docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Options:
## Settings

`metrics`, `excludeFilePatterns`, `excludePatterns` and `cache` affect the analysis.
`scoreThreshold`, `show*` and `groupByClass` shape the console output, see [Console output](#console-output).
`scoreThreshold`, `show*` and `groupByClass` shape the console output, see [Console output](#console-output), and the HTML and Markdown reports, see [Reports](./Reports.md).
`customReporters` is parsed and validated, and is applied from a later release.

```yaml
Expand Down
53 changes: 53 additions & 0 deletions docs/Reports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Reports

`analyse` writes a report with `--report-type` (`-r`) and `--report-file` (`-f`).
Both must be given together.
The type is `json`, `csv`, `html` or `markdown`, in any letter case.
The directory of the file must exist, and the file is always written as UTF-8.
Numbers use a dot and three decimals, whatever the locale is.
A value that is not a finite number is `N/A` in the text formats and `null` in JSON.
The file is written to a temporary file next to it and moved into place, so a failed write leaves an existing report intact.

| Setting | json | csv | html | markdown |
| --- | --- | --- | --- | --- |
| `groupByClass` | always grouped | one row per method | yes | yes |
| `showOnlyMethodsExceedingThreshold` | all methods | all methods | yes | yes |
| `scoreThreshold` | no | no | highlights the score | note and filter |
| `showDetailedCognitiveMetrics`, `showHalsteadComplexity`, `showCyclomaticComplexity`, `showUnderstandability` | all | all | all | selects columns |

## JSON

```json
{
"pkg.Class": {
"file": "src/pkg/Class.java",
"methods": {
"method": { "class": "...", "method": "...", "file": "...", "line": 12, "lineCount": 3, "lineCountWeight": 0.0, "score": 1.5, "...": "..." }
}
}
}
```

Classes are sorted by name, methods keep the order of the analysis.
Weights, score and Halstead values are rounded to three decimals.
`deltas` holds the change of each weight against a baseline, and is left out when there is none.
When the same class name occurs in more than one file, for example in the default package, every occurrence is keyed `<class>@<file>`, with the file as shown in the reports, so nothing is lost.
Overloaded methods have distinct keys, as their parameter types are part of the name.

## CSV

One header row, then one row per method in the order of the analysis.
Per metric there are the columns `<Metric> Count`, `<Metric> Weight` and `<Metric> Weight Delta`, the last one empty without a baseline.

## HTML

A page that loads Bootstrap 5.3.3 from a CDN, so it needs a network connection to look as intended.
One table per class, or one table `All Methods (N total)` with a class column.
Scores above `scoreThreshold` are highlighted.
Weight changes against a baseline are badges, red for an increase and green for a decrease.

## Markdown

Tables per class, or one table of all methods.
In the grouped form a class that occurs in several files gets one section per file, titled `<class>@<file>`, as in the JSON keys.
`|`, `\`, backticks, `<` and `>` are escaped, line breaks become spaces, and weight changes are shown as `↑ 0.500` and `↓ 0.500`.
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@
<param>net.floriankraemer.cognitive_analysis.domain.*</param>
<param>net.floriankraemer.cognitive_analysis.config.*</param>
<param>net.floriankraemer.cognitive_analysis.churn.*</param>
<param>net.floriankraemer.cognitive_analysis.application.report.*</param>
</targetClasses>
<mutationThreshold>80</mutationThreshold>
<timestampedReports>false</timestampedReports>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,16 @@

import java.io.PrintWriter;
import java.nio.file.Path;
import java.time.Clock;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Callable;
import java.util.function.Supplier;
import java.util.function.LongSupplier;
import net.floriankraemer.cognitive_analysis.CognitiveAnalysisException;
import net.floriankraemer.cognitive_analysis.application.report.CsvReportBuilder;
import net.floriankraemer.cognitive_analysis.application.report.HtmlReportBuilder;
import net.floriankraemer.cognitive_analysis.application.report.JsonReportBuilder;
import net.floriankraemer.cognitive_analysis.application.report.ReportBuilder;
import net.floriankraemer.cognitive_analysis.application.report.ReportBuilder.ReportType;
import net.floriankraemer.cognitive_analysis.application.report.ReportType;
import net.floriankraemer.cognitive_analysis.application.report.ReportWriter;
import net.floriankraemer.cognitive_analysis.config.CognitiveConfig;
import net.floriankraemer.cognitive_analysis.config.ConfigLoader;
import net.floriankraemer.cognitive_analysis.domain.CognitiveMetricsFacade;
Expand All @@ -41,12 +38,7 @@ public class AnalyseCommand implements Callable<Integer> {
static final class ReportTypeConverter implements ITypeConverter<ReportType> {
@Override
public ReportType convert(final String value) {
try {
return ReportType.valueOf(value.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
"Invalid report type '" + value + "'. Expected one of: html, csv, json.", e);
}
return ReportType.parse(value);
}
}

Expand Down Expand Up @@ -74,7 +66,7 @@ public Iterator<String> iterator() {
@Option(
names = {"-r", "--report-type"},
converter = ReportTypeConverter.class,
description = "Report type: html, csv or json. Requires --report-file."
description = "Report type: json, csv, html or markdown. Requires --report-file."
)
ReportType reportType;

Expand Down Expand Up @@ -106,8 +98,7 @@ public Iterator<String> iterator() {
/** The terminal the progress goes to, or null when the process is not interactive. */
Supplier<PrintWriter> terminalWriter = () -> Terminal.isAttached() ? System.console().writer() : null;

private final ReportBuilder reportBuilder = new ReportBuilder(
new HtmlReportBuilder(), new CsvReportBuilder(), new JsonReportBuilder());
private final ReportWriter reportWriter = new ReportWriter(Clock.systemDefaultZone());

@Override
public Integer call() {
Expand Down Expand Up @@ -144,7 +135,7 @@ public Integer call() {
summary.endStage("sorting");

if (reportType != null) {
reportBuilder.build(reportType, reportFile, MetricsCollection.of(methods));
reportWriter.write(reportType, reportFile, config, methods);
out.println("Report generated at: " + reportFile);
} else {
console.renderMethods(out, methods);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package net.floriankraemer.cognitive_analysis.application.report;

import java.io.IOException;
import java.io.StringWriter;
import java.time.Clock;
import java.util.ArrayList;
import java.util.List;
import net.floriankraemer.cognitive_analysis.CognitiveAnalysisException;
import net.floriankraemer.cognitive_analysis.application.report.ReportSupport.Row;
import net.floriankraemer.cognitive_analysis.config.CognitiveConfig;
import net.floriankraemer.cognitive_analysis.config.MetricName;
import net.floriankraemer.cognitive_analysis.domain.CognitiveMetrics;
import net.floriankraemer.cognitive_analysis.domain.ScoredMethod;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;

/**
* One row per method in collection order, not grouped. Contains every method, whatever
* {@code showOnlyMethodsExceedingThreshold} says.
*/
final class CsvReport implements Report {

private static final CSVFormat FORMAT = CSVFormat.DEFAULT.builder().setRecordSeparator("\n").get();

CsvReport(final CognitiveConfig config, final Clock clock) {
// The CSV report has no use for either.
}

@Override
public String render(final List<ScoredMethod> metrics) throws CognitiveAnalysisException {
final StringWriter out = new StringWriter();
try (CSVPrinter printer = new CSVPrinter(out, FORMAT)) {
printer.printRecord(header());
for (final Row row : ReportSupport.rows(metrics)) {
printer.printRecord(record(row));
}
} catch (IOException e) {
throw new CognitiveAnalysisException("Failed to build the CSV report", e);
}
return out.toString();
}

private static List<String> header() {
final List<String> header = new ArrayList<>(List.of("Class", "Method", "File", "Line"));
for (final MetricName name : MetricName.values()) {
final String label = ReportSupport.longLabel(name);
header.addAll(List.of(label, label + " Weight", label + " Weight Delta"));
}
header.addAll(List.of("Score", "Cyclomatic Complexity", "Halstead Volume", "Halstead Difficulty",
"Halstead Effort", "Understandability"));
return header;
}

private static List<Object> record(final Row row) {
final CognitiveMetrics metrics = row.metrics();
final List<Object> record = new ArrayList<>(
List.of(metrics.getClassName(), metrics.getMethodName(), row.file(), metrics.getLine()));
for (final MetricName name : MetricName.values()) {
record.add(metrics.value(name));
record.add(ReportSupport.fixed(metrics.weight(name)));
record.add(metrics.delta(name).map(delta -> ReportSupport.fixed(delta.getValue())).orElse(""));
}
record.add(ReportSupport.fixed(metrics.getCognitiveComplexity()));
record.add(metrics.getCyclomatic().complexity());
record.add(ReportSupport.fixed(metrics.getHalstead().volume()));
record.add(ReportSupport.fixed(metrics.getHalstead().difficulty()));
record.add(ReportSupport.fixed(metrics.getHalstead().effort()));
record.add(metrics.getUnderstandability().complexity());
return record;
}
}

This file was deleted.

Loading
Loading