Skip to content
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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What if the value appears only once, but there is no final variable declared within the method?
I think this is still a "magic number" and therefore incorrect. Because we do not understand what is the meaning of 221 solely.

Something like:

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

Copy link
Member

Choose a reason for hiding this comment

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

The current version correctly flags magic numbers. Since this method (useMagicNumber(MethodDeclaration n)) analyzes only a single MethodDeclaration, which represents one method and not the entire CompilationUnit, any occurrence of the number within that method is rightly flagged as a magic number. Therefore, no change is needed!

Copy link

Choose a reason for hiding this comment

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

Are you saying that this is still magic?

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

Copy link
Member

Choose a reason for hiding this comment

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

Makes sense! Apologies, I completely missed this angle. Indeed, this is not a magic number situation!

Copy link
Member

@jagdish-15 jagdish-15 Oct 16, 2025

Choose a reason for hiding this comment

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

What do you think of something like this?

private boolean useMagicNumber(CompilationUnit node) {
    List<VariableDeclarator> declarations = node.findAll(VariableDeclarator.class);

    long declarationCount = declarations.stream()
            .filter(vd -> vd.getInitializer().isPresent()
                    && vd.getInitializer().get().toString().equals(MAGIC_NUMBER))
            .count();

    long literalUsages = node.findAll(IntegerLiteralExpr.class).stream()
            .filter(lit -> lit.asString().equals(MAGIC_NUMBER))
            .count();

    return declarationCount == 1 && literalUsages == 1;
}

Copy link

Choose a reason for hiding this comment

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

I'm not familiar with how the java analyzer works. I read this as saying, for some node in the AST, if the magic number appears it must be part of a variable declaration. Is that right?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, and it should be declared only once, with the number not used anywhere else except in that declaration! Right, @kahgoh?

Thanks for catching the above, though!

Copy link
Member

Choose a reason for hiding this comment

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

Yes, and it should be declared only once, with the number not used anywhere else except in that declaration! Right, @kahgoh?

Uh ... if I go back to what was written in the design.md, it would be no, I don't think that's quite right. The design.md doesn't mention anything about magic number at all - just that the number 221 shouldn't appear in the code more than once in the code.

To be honest, I thought this solution looked ok (assuming this is the only place where 221 appears):

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

If 221 should only be in the declaration, what about the other numbers? The reference solution shows there are also other numbers in the solution like 10, 9, 5, 0.77, etc.

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