Skip to content

Implement analyzer for cars-assemble concept exercise #277

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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: 2 additions & 0 deletions src/main/java/analyzer/AnalyzerRoot.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import analyzer.comments.FeedbackRequest;
import analyzer.exercises.GlobalAnalyzer;
import analyzer.exercises.annalynsinfiltration.AnnalynsInfiltrationAnalyzer;
import analyzer.exercises.carsassemble.CarsAssembleAnalyzer;
import analyzer.exercises.hamming.HammingAnalyzer;
import analyzer.exercises.lasagna.LasagnaAnalyzer;
import analyzer.exercises.leap.LeapAnalyzer;
Expand Down Expand Up @@ -52,6 +53,7 @@ private static List<Analyzer> createAnalyzers(String slug) {

switch (slug) {
case "annalyns-infiltration" -> analyzers.add(new AnnalynsInfiltrationAnalyzer());
case "cars-assemble" -> analyzers.add(new CarsAssembleAnalyzer());
case "hamming" -> analyzers.add(new HammingAnalyzer());
case "lasagna" -> analyzers.add(new LasagnaAnalyzer());
case "leap" -> analyzers.add(new LeapAnalyzer());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package analyzer.exercises.carsassemble;

import analyzer.Comment;

/**
* @see <a href="https://github.com/exercism/website-copy/blob/main/analyzer-comments/java/cars-assemble/avoid_using_return_in_else_statement.md">Markdown Template</a>
*/
public class AvoidUsingReturnInElseStatement extends Comment {
@Override
public String getKey() {
return "java.cars-assemble.avoid_using_return_in_else_statement";
}

@Override
public Type getType() {
return Type.INFORMATIVE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package analyzer.exercises.carsassemble;

import analyzer.Analyzer;
import analyzer.OutputCollector;
import analyzer.Solution;
import analyzer.comments.ExemplarSolution;
import analyzer.comments.MethodTooLong;
import analyzer.comments.ReuseCode;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.ast.stmt.BlockStmt;
import com.github.javaparser.ast.stmt.IfStmt;
import com.github.javaparser.ast.stmt.Statement;
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;

import java.util.List;

/**
* The {@link CarsAssembleAnalyzer} is the analyzer implementation for the {@code cars-assemble} concept exercise.
*
* @see <a href="https://github.com/exercism/java/tree/main/exercises/concept/cars-assemble">The cars-assemble exercise on the Java track</a>
*/
public class CarsAssembleAnalyzer extends VoidVisitorAdapter<OutputCollector> implements Analyzer {
private static final String EXERCISE_NAME = "CarsAssemble";
private static final String MAGIC_NUMBER = "221";
private static final String PRODUCTION_RATE_PER_HOUR_METHOD = "productionRatePerHour";
private static final String WORKING_ITEMS_PER_MINUTE_METHOD = "workingItemsPerMinute";
private static final String RETURN = "return";

@Override
public void analyze(Solution solution, OutputCollector output) {

for (var compilationUnit : solution.getCompilationUnits()) {
compilationUnit.getClassByName(EXERCISE_NAME).ifPresent(c -> c.accept(this, output));
}

if (output.getComments().isEmpty()) {
output.addComment(new ExemplarSolution(EXERCISE_NAME));
}
}

@Override
public void visit(MethodDeclaration n, OutputCollector output) {

if(n.getNameAsString().equals(PRODUCTION_RATE_PER_HOUR_METHOD) && !hasHelperMethod(n)){
output.addComment(new MethodTooLong(PRODUCTION_RATE_PER_HOUR_METHOD));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm concerned that reusing the MethodTooLong could be confusing for the students. There's nothing in the comment that hints students should move the if statement. I think its really hard for the students to tell that they should move it a helper method because it only tells them to make the method smaller, but we actually them to specifically move the if statement into a separate method. The actionable level may be too high for this comment too.

}

if(n.getNameAsString().equals(WORKING_ITEMS_PER_MINUTE_METHOD) && !reuseMethod(n)){
output.addComment(new ReuseCode(WORKING_ITEMS_PER_MINUTE_METHOD, PRODUCTION_RATE_PER_HOUR_METHOD));
}

if(useMagicNumber(n)){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method seems to check if the number 221 appears anywhere, but the comment in the design.md seems to suggest that it should check if the number appears more than once in the code:

If the solution is repeatedly hard-coding the value 221, inform the student that they could store this value in a field to make the code easier to maintain.

An alternate solution may to set it to a final variable (within a method), especially if it isn't used anywhere else. If the value appears only once, I think that could be fine.

output.addComment(new PreferStoringConstantInField());
}

super.visit(n, output);

}

@Override
public void visit(IfStmt node, OutputCollector output){

if(node.getThenStmt().toString().contains(RETURN) && node.hasElseBlock()){
output.addComment(new AvoidUsingReturnInElseStatement());
}

super.visit(node, output);
}

private boolean hasHelperMethod(MethodDeclaration n){

if(n.getBody().isEmpty()){
return true;
}

BlockStmt stmt = n.getBody().get();
List<Statement> IfStmts = stmt.getStatements().stream().filter(Statement::isIfStmt).toList();
for(Statement s : IfStmts){
return ((IfStmt) s).hasElseBlock();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something here seems strange here. You get a list of IfStmts, start iterating, but return true / false depending on whether the first if statement in the list is has an else block? What about the other if statements?

Even if you really did mean to check just the first if statement, I think the not-using-helper-method test would fail if productionRatePerHour used multiple returns like this community solution:

public class CarsAssemble {

    public double productionRatePerHour(double speed) {
        double baseProductionRate = 221;

        if (speed >= 1 && speed <= 4) {
            return speed * baseProductionRate;
        } else if (speed >= 5 && speed <= 8) {
            return speed * baseProductionRate * 0.9;
        } else if (speed == 9) {
            return speed * baseProductionRate * 0.8;
        }
        return speed * baseProductionRate * 0.77;
    }

    public int workingItemsPerMinute(int speed) {
       return (int) productionRatePerHour(speed)/60;

       
    }
}

}
return true;
}

private boolean reuseMethod(MethodDeclaration n){
return !n.findAll(MethodCallExpr.class).stream().filter(m -> m.getNameAsString().equals(PRODUCTION_RATE_PER_HOUR_METHOD)).toList().isEmpty();
}

private boolean useMagicNumber(MethodDeclaration n){

if(n.getBody().isEmpty()){
return false;
}

BlockStmt stmt = n.getBody().get();
return stmt.toString().contains(MAGIC_NUMBER);

}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package analyzer.exercises.carsassemble;

import analyzer.Comment;

/**
* @see <a href="https://github.com/exercism/website-copy/blob/main/analyzer-comments/java/cars-assemble/prefer_storing_constant_in_field.md">Markdown Template</a>
*/
public class PreferStoringConstantInField extends Comment {
@Override
public String getKey() {
return "java.cars-assemble.prefer_storing_constant_in_field";
}

@Override
public Type getType() {
return Type.INFORMATIVE;
}
}
16 changes: 16 additions & 0 deletions src/test/java/analyzer/AnalyzerIntegrationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,22 @@ void global(String scenario) throws IOException {
Approvals.verify(serialize(output.analysis()), Approvals.NAMES.withParameters(scenario));
}

@ParameterizedTest
@ValueSource(strings = {
"ExemplarSolution",
"NotReusingMethod",
"NotUsingHelperMethod",
"UsingMagicNumber",
"UsingReturnInElseStatement",
})
void carsassemble(String scenario) throws IOException {
var path = Path.of("cars-assemble", scenario + ".java");
var solution = new SolutionFromFiles("cars-assemble", SCENARIOS.resolve(path));
var output = AnalyzerRoot.analyze(solution);

Approvals.verify(serialize(output.analysis()), Approvals.NAMES.withParameters(scenario));
}

@ParameterizedTest
@ValueSource(strings = {
"ConstructorTooLong",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"comments": [
{
"comment": "java.general.exemplar",
"params": {
"exerciseName": "CarsAssemble"
},
"type": "celebratory"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"comments": [
{
"comment": "java.general.reuse_code",
"params": {
"callingMethod": "workingItemsPerMinute",
"methodToCall": "productionRatePerHour"
},
"type": "actionable"
},
{
"comment": "java.general.feedback_request",
"params": {},
"type": "informative"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"comments": [
{
"comment": "java.general.method_too_long",
"params": {
"methodNames": "productionRatePerHour"
},
"type": "actionable"
},
{
"comment": "java.general.feedback_request",
"params": {},
"type": "informative"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"comments": [
{
"comment": "java.cars-assemble.prefer_storing_constant_in_field",
"params": {},
"type": "informative"
},
{
"comment": "java.general.feedback_request",
"params": {},
"type": "informative"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"comments": [
{
"comment": "java.cars-assemble.avoid_using_return_in_else_statement",
"params": {},
"type": "informative"
},
{
"comment": "java.general.feedback_request",
"params": {},
"type": "informative"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@

public class CarsAssemble {

private final int defaultProductionRate = 221;

public double productionRatePerHour(int speed) {
return defaultProductionRate * speed * successRate(speed);
}

public int workingItemsPerMinute(int speed) {
return (int) (productionRatePerHour(speed) / 60);
}

private double successRate(int speed) {
if (speed == 10) {
return 0.77;
}

if (speed == 9) {
return 0.8;
}

if (speed >= 5) {
return 0.9;
}

return 1;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@

public class CarsAssemble {

private final int defaultProductionRate = 221;

public double productionRatePerHour(int speed) {
return defaultProductionRate * speed * successRate(speed);
}

public int workingItemsPerMinute(int speed) {
return (int) defaultProductionRate * speed * successRate(speed) / 60;
}

private double successRate(int speed) {
if (speed == 10) {
return 0.77;
}

if (speed == 9) {
return 0.8;
}

if (speed >= 5) {
return 0.9;
}

return 1;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

public class CarsAssemble {

private final int defaultProductionRate = 221;

public double productionRatePerHour(int speed) {
double rate = null;

if (speed == 10) {
rate = 0.77;
}else if (speed == 9) {
rate = 0.8;
} else if (speed >= 5) {
rate = 0.9;
} else {
rate = 1.0;
}

return defaultProductionRate * speed * rate;
}

public int workingItemsPerMinute(int speed) {
return (int) productionRatePerHour(speed) / 60;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@

public class CarsAssemble {

public double productionRatePerHour(int speed) {
return 221 * speed * successRate(speed);
}

public int workingItemsPerMinute(int speed) {
return (int) productionRatePerHour(speed) / 60;
}

private double successRate(int speed) {
if (speed == 10) {
return 0.77;
}

if (speed == 9) {
return 0.8;
}

if (speed >= 5) {
return 0.9;
}

return 1;
}
}
Loading