Skip to content

Commit

Permalink
Refactor analyzer (#82)
Browse files Browse the repository at this point in the history
* Refactor JSON writing behavior

* Preserve order of added comments and tags

* Rename Exercise to Analyzer

* Add javadocs referencing the Exercism documentation

* Refactor Analyzer base class into interface

* Rename JsonSerializerTest.java to OutputWriterTest.java

* Remove unused general comments

The `FailedParse` comment was removed because it doesn't make much sense to have the analyzer comment on a submission that does not even compile.

The `FileNotFound` comment was removed because the analyzer now analyzes all files in the source root, so there is no need to search for a specific file anymore.

* Add golden test verifying that the analyzer doesn't crash on unknown exercises

* Add .gitattributes
  • Loading branch information
sanderploegsma authored Jan 19, 2024
1 parent 4387cac commit c7c64e1
Show file tree
Hide file tree
Showing 48 changed files with 956 additions and 944 deletions.
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* text=auto

*.bat text eol=crlf
1 change: 0 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ repositories {
dependencies {
implementation "org.json:json:20231013"
implementation "com.github.javaparser:javaparser-core:3.25.7"
implementation "com.google.guava:guava:33.0.0-jre"

testImplementation platform("org.junit:junit-bom:5.10.0")
testImplementation "org.junit.jupiter:junit-jupiter"
Expand Down
38 changes: 38 additions & 0 deletions src/main/java/analyzer/Analysis.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package analyzer;

import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

/**
* @see <a href="https://exercism.org/docs/building/tooling/analyzers/interface">The analyzer interface in the Exercism documentation</a>
*/
public class Analysis {
private String summary;
private final Set<Comment> comments = new LinkedHashSet<>();
private final Set<String> tags = new LinkedHashSet<>();

public String getSummary() {
return summary;
}

public void setSummary(String summary) {
this.summary = summary;
}

public List<Comment> getComments() {
return List.copyOf(comments);
}

public List<String> getTags() {
return List.copyOf(tags);
}

public void addComment(Comment comment) {
comments.add(comment);
}

public void addTag(String tag) {
tags.add(tag);
}
}
9 changes: 9 additions & 0 deletions src/main/java/analyzer/Analyzer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package analyzer;

import com.github.javaparser.ast.CompilationUnit;

import java.util.List;

public interface Analyzer {
void analyze(List<CompilationUnit> compilationUnits, Analysis analysis);
}
30 changes: 30 additions & 0 deletions src/main/java/analyzer/AnalyzerRoot.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package analyzer;

import analyzer.exercises.hamming.HammingAnalyzer;
import analyzer.exercises.twofer.TwoferAnalyzer;
import com.github.javaparser.ast.CompilationUnit;

import java.util.ArrayList;
import java.util.List;

public class AnalyzerRoot {

public static Analysis analyze(String slug, List<CompilationUnit> compilationUnits) {
var analysis = new Analysis();
for (Analyzer analyzer : createAnalyzers(slug)) {
analyzer.analyze(compilationUnits, analysis);
}
return analysis;
}

private static List<Analyzer> createAnalyzers(String slug) {
var analyzers = new ArrayList<Analyzer>();

switch (slug) {
case "hamming" -> analyzers.add(new HammingAnalyzer());
case "two-fer" -> analyzers.add(new TwoferAnalyzer());
}

return List.copyOf(analyzers);
}
}
46 changes: 46 additions & 0 deletions src/main/java/analyzer/Comment.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package analyzer;

import java.util.Map;
import java.util.Objects;

/**
* @see <a href="https://exercism.org/docs/building/tooling/analyzers/interface">The analyzer interface in the Exercism documentation</a>
*/
public abstract class Comment {

public abstract String getKey();

public Map<String, String> getParameters() {
return Map.of();
}

public CommentType getType() {
return null;
}

@Override
public boolean equals(Object obj) {
return (obj instanceof Comment other) && equals(other);
}

public boolean equals(Comment other) {
if (!getKey().equals(other.getKey()) || getType() != other.getType()) {
return false;
}

var params = this.getParameters().entrySet();
var otherParams = other.getParameters().entrySet();

return params.containsAll(otherParams) && otherParams.containsAll(params);
}

@Override
public int hashCode() {
return Objects.hash(getKey(), getType(), getParameters());
}

@Override
public String toString() {
return String.format("Comment{key=%s,params=%s,type=%s}", getKey(), getParameters(), getType());
}
}
11 changes: 11 additions & 0 deletions src/main/java/analyzer/CommentType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package analyzer;

/**
* @see <a href="https://exercism.org/docs/building/tooling/analyzers/interface">The analyzer interface in the Exercism documentation</a>
*/
public enum CommentType {
ESSENTIAL,
ACTIONABLE,
INFORMATIVE,
CELEBRATORY
}
54 changes: 36 additions & 18 deletions src/main/java/analyzer/Main.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
package analyzer;

import analyzer.exercises.Exercise;
import analyzer.exercises.twofer.Twofer;
import analyzer.exercises.hamming.Hamming;
import com.github.javaparser.ParseResult;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.utils.SourceRoot;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

public class Main {

private static boolean isNotValidDirectory(String p) {
return !p.endsWith("/") || !new File(p).isDirectory();
}

public static void main(String... args) {
private static Options validateOptions(String... args) {
if (args.length < 3) {
System.err.println("Invalid arguments. Usage: java-analyzer <exercise slug> <exercise directory> <output directory>");
System.exit(-1);
Expand All @@ -30,21 +36,33 @@ public static void main(String... args) {
System.exit(-1);
}

Exercise ex = null;
switch (slug) {
case "two-fer":
ex = new Twofer(inputDirectory, outputDirectory);
break;
case "hamming":
ex = new Hamming(inputDirectory, outputDirectory);
break;
default:
System.err.println("Exercise not found");
System.exit(-1);
return new Options(slug, inputDirectory, outputDirectory);
}

private static List<CompilationUnit> parseInput(Options options) throws IOException {
var sourceRoot = new SourceRoot(Path.of(options.inputDirectory, "src/main/java"));
var compilationUnits = new ArrayList<CompilationUnit>();
for (ParseResult<CompilationUnit> parseResult : sourceRoot.tryToParse()) {
compilationUnits.add(parseResult.getResult().get());
}

ex.parse();
ex.writeAnalysisToFile();
System.out.println("Analysis completed successfully");
return List.copyOf(compilationUnits);
}

private static void writeOutput(Analysis analysis, Options options) throws IOException {
try (var analysisWriter = new FileWriter(options.outputDirectory + "analysis.json");
var tagsWriter = new FileWriter(options.outputDirectory + "tags.json")) {
var output = new OutputWriter(analysisWriter, tagsWriter);
output.write(analysis);
}
}

public static void main(String... args) throws IOException {
var options = validateOptions(args);
var input = parseInput(options);
var analysis = AnalyzerRoot.analyze(options.slug, input);
writeOutput(analysis, options);
}

private record Options(String slug, String inputDirectory, String outputDirectory){}
}
67 changes: 67 additions & 0 deletions src/main/java/analyzer/OutputWriter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package analyzer;

import org.json.JSONObject;

import java.io.IOException;
import java.io.Writer;

/**
* @see <a href="https://exercism.org/docs/building/tooling/analyzers/interface">The analyzer interface in the Exercism documentation</a>
*/
public class OutputWriter {
private static final int JSON_INDENTATION = 2;

private final Writer analysisWriter;
private final Writer tagsWriter;

public OutputWriter(Writer analysisWriter, Writer tagsWriter) {
this.analysisWriter = analysisWriter;
this.tagsWriter = tagsWriter;
}

public void write(Analysis analysis) throws IOException {
writeAnalysis(analysis);
writeTags(analysis);
}

private void writeAnalysis(Analysis analysis) throws IOException {
var json = new JSONObject();

if (analysis.getSummary() != null) {
json.put("summary", analysis.getSummary());
}

for (Comment comment : analysis.getComments()) {
json.append("comments", serialize(comment));
}

this.analysisWriter.write(json.toString(JSON_INDENTATION));
}

private void writeTags(Analysis analysis) throws IOException {
var json = new JSONObject();
for (String tag : analysis.getTags()) {
json.append("tags", tag);
}

this.tagsWriter.write(json.toString(JSON_INDENTATION));
}

private static JSONObject serialize(Comment comment) {
var json = new JSONObject();
json.put("comment", comment.getKey());

if (comment.getType() != null) {
json.put("type", comment.getType().name().toLowerCase());
}

if (comment.getParameters().isEmpty()) {
return json;
}

var paramsJson = new JSONObject();
comment.getParameters().forEach(paramsJson::put);
json.put("params", paramsJson);
return json;
}
}
13 changes: 13 additions & 0 deletions src/main/java/analyzer/comments/AvoidHardCodedTestCases.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package analyzer.comments;

import analyzer.Comment;

/**
* @see <a href="https://github.com/exercism/website-copy/blob/main/analyzer-comments/java/general/avoid_hard_coded_test_cases.md">Markdown Template</a>
*/
public class AvoidHardCodedTestCases extends Comment {
@Override
public String getKey() {
return "java.general.avoid_hard_coded_test_cases";
}
}
32 changes: 32 additions & 0 deletions src/main/java/analyzer/comments/ConstructorTooLong.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package analyzer.comments;

import analyzer.Comment;

import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
* @see <a href="https://github.com/exercism/website-copy/blob/main/analyzer-comments/java/general/constructor_too_long.md">Markdown Template</a>
*/
public class ConstructorTooLong extends Comment {
private final Collection<String> constructorNames;

public ConstructorTooLong(Collection<String> constructorNames) {
this.constructorNames = constructorNames;
}

public ConstructorTooLong(String constructorName) {
this(List.of(constructorName));
}

@Override
public String getKey() {
return "java.general.constructor_too_long";
}

@Override
public Map<String, String> getParameters() {
return Map.of("constructorNames", String.join(", ", this.constructorNames));
}
}
32 changes: 32 additions & 0 deletions src/main/java/analyzer/comments/MethodTooLong.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package analyzer.comments;

import analyzer.Comment;

import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
* @see <a href="https://github.com/exercism/website-copy/blob/main/analyzer-comments/java/general/method_too_long.md">Markdown Template</a>
*/
public class MethodTooLong extends Comment {
private final Collection<String> methodNames;

public MethodTooLong(Collection<String> methodNames) {
this.methodNames = methodNames;
}

public MethodTooLong(String methodName) {
this(List.of(methodName));
}

@Override
public String getKey() {
return "java.general.method_too_long";
}

@Override
public Map<String, String> getParameters() {
return Map.of("methodNames", String.join(", ", this.methodNames));
}
}
Loading

0 comments on commit c7c64e1

Please sign in to comment.