Skip to content
Closed
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# ---- Maven
target/
*.class
Copy link
Collaborator

Choose a reason for hiding this comment

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

There's no reason for this, as target folders are already ignored.


# ---- IntelliJ IDEA
*.iws
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,64 @@ <h2>Why is this an issue?</h2>
The cyclomatic complexity of routines should not be excessive, as complex code will be difficult
to understand and maintain.
</p>
<p>
High cyclomatic complexity can lead to:
</p>
<ul>
<li>Increased difficulty in understanding the code</li>
<li>Higher chance of introducing bugs</li>
<li>More complex testing requirements</li>
<li>Reduced code maintainability</li>
</ul>
<h3>Noncompliant Code Example</h3>
<pre>
function ProcessValue(Value: Integer; Mode: String): Integer;
begin
if Value > 0 then // +1
begin
if Mode = 'ADD' then // +1
Result := Value + 10
else if Mode = 'MUL' then // +1
Result := Value * 2
else if Mode = 'DIV' then // +1
Result := Value div 2
else
Result := Value;
end
else if Value < 0 then // +1
Result := -Value
else
Result := 0;
end;
// Cyclomatic Complexity = 6
</pre>
<h2>How to fix it</h2>
<p>
Refactor this routine so it runs more linearly, for example by reducing the number of times
the control flow splits.
the control flow splits. Consider using polymorphism, strategy pattern, or breaking the routine
into smaller, more focused methods.
</p>
<h3>Compliant Solution</h3>
<pre>
function ProcessValue(Value: Integer; Mode: String): Integer;
begin
if Value <= 0 then
Exit(Abs(Value));

Result := ApplyMode(Value, Mode);
end;

function ApplyMode(Value: Integer; Mode: String): Integer;
begin
case Mode of
'ADD': Result := Value + 10;
'MUL': Result := Value * 2;
'DIV': Result := Value div 2;
else
Result := Value;
end;
end;
</pre>
<h2>Resources</h2>
<ul>
<li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,35 @@
import org.sonar.plugins.communitydelphi.api.ast.StatementNode;
import org.sonar.plugins.communitydelphi.api.token.DelphiToken;

/**
* Visitor that collects various code metrics from Delphi source code.
*
* <p>This visitor traverses the AST and collects metrics including:
*
* <ul>
* <li>Number of classes, routines, and statements
* <li>Cyclomatic and cognitive complexity
* <li>Code and comment line counts
* </ul>
*/
public class MetricsVisitor implements DelphiParserVisitor<Data> {
private static final Pattern NEW_LINE_PATTERN = Pattern.compile("\r\n|\n|\r");

/**
* Container class for metrics collected from Delphi source code.
*
* <p>This class holds various metrics gathered during AST traversal, including:
* <ul>
* <li>Number of classes ({@code classes})</li>
* <li>Number of routines ({@code routines})</li>
* <li>Cyclomatic complexity ({@code complexity})</li>
* <li>Cognitive complexity ({@code cognitiveComplexity})</li>
* <li>Set of code line numbers ({@code codeLines})</li>
* <li>Set of comment line numbers ({@code commentLines})</li>
* <li>Number of statements ({@code statements})</li>
* </ul>
* <p>These metrics are used to assess code quality and complexity.
*/
Comment on lines +47 to +61
Copy link
Preview

Copilot AI Sep 22, 2025

Choose a reason for hiding this comment

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

The JavaDoc for the Data class is placed before the MetricsVisitor class instead of before the Data class declaration. Move this documentation to line 62 where the Data class is actually declared.

Copilot uses AI. Check for mistakes.

Comment on lines +47 to +61
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't find this comment valuable.

public static class Data {
private int classes;
private int routines;
Expand Down Expand Up @@ -73,15 +99,24 @@ public Set<Integer> getCodeLines() {

@Override
public Data visit(DelphiAst ast, Data data) {
calculateComplexityMetrics(ast, data);
return DelphiParserVisitor.super.visit(ast, data);
}

/**
* Calculates both cyclomatic and cognitive complexity metrics for the given AST.
*
* @param ast the AST to analyze
* @param data the data container to populate with complexity metrics
*/
private void calculateComplexityMetrics(DelphiAst ast, Data data) {
var cyclomaticVisitor = new CyclomaticComplexityVisitor();
var cyclomaticComplexity = cyclomaticVisitor.visit(ast, new CyclomaticComplexityVisitor.Data());
data.complexity = cyclomaticComplexity.getComplexity();

var cognitiveVisitor = new CognitiveComplexityVisitor();
var cognitiveComplexity = cognitiveVisitor.visit(ast, new CognitiveComplexityVisitor.Data());
data.cognitiveComplexity = cognitiveComplexity.getComplexity();

return DelphiParserVisitor.super.visit(ast, data);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,26 @@ public TokenType getType() {
public String getText() {
return text;
}

@Override
public String toString() {
return "Token{type=" + type + ", text='" + text + "'}";
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Token token = (Token) obj;
return type == token.type && java.util.Objects.equals(text, token.text);
}

@Override
public int hashCode() {
return java.util.Objects.hash(type, text);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,26 +26,59 @@
import org.sonar.api.utils.AnnotationUtils;
import org.sonar.check.Rule;

/**
* Utility class for reading and processing rule template annotations.
*
* <p>This class handles the configuration of SonarQube rules based on annotations found on rule
* classes, particularly {@link RuleTemplate} annotations.
*/
public final class RuleTemplateAnnotationReader {

/**
* Updates rule definitions in the repository based on rule template annotations.
*
* @param repository the rules repository to update
* @param ruleClasses the list of rule classes to process
* @throws IllegalArgumentException if repository or ruleClasses is null
*/
public void updateRulesByAnnotatedClass(
RulesDefinition.NewRepository repository, List<Class<?>> ruleClasses) {
ruleClasses.forEach(ruleClass -> handleTemplateRule(repository, ruleClass));
}
if (repository == null) {
throw new IllegalArgumentException("Repository cannot be null");
}
if (ruleClasses == null) {
throw new IllegalArgumentException("Rule classes cannot be null");
}

private static String ruleKey(Class<?> ruleClass) {
return AnnotationUtils.getAnnotation(ruleClass, Rule.class).key();
ruleClasses.forEach(ruleClass -> processTemplateRule(repository, ruleClass));
}

private static boolean isTemplateRule(Class<?> ruleClass) {
return AnnotationUtils.getAnnotation(ruleClass, RuleTemplate.class) != null;
}
private static void processTemplateRule(NewRepository repository, Class<?> ruleClass) {
String key = extractRuleKey(ruleClass);
NewRule rule = Objects.requireNonNull(repository.rule(key), "Rule not found for key: " + key);

private static void handleTemplateRule(NewRepository repository, Class<?> ruleClass) {
NewRule rule = Objects.requireNonNull(repository.rule(ruleKey(ruleClass)));
if (isTemplateRule(ruleClass)) {
rule.setTemplate(true);
} else {
rule.params().removeIf(param -> param.key().equals("scope"));
// Remove scope parameter for non-template rules.
// The "scope" parameter is only relevant for template rules, as it allows users to configure
// the scope when instantiating a rule from a template. For non-template rules, the scope
// parameter is not applicable and should be removed to prevent confusion and ensure correct
// rule configuration in SonarQube.
Comment on lines +63 to +67
Copy link
Preview

Copilot AI Sep 22, 2025

Choose a reason for hiding this comment

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

[nitpick] The comment is overly verbose for a simple parameter removal operation. Consider condensing it to a single line explaining why the scope parameter is removed for non-template rules.

Suggested change
// Remove scope parameter for non-template rules.
// The "scope" parameter is only relevant for template rules, as it allows users to configure
// the scope when instantiating a rule from a template. For non-template rules, the scope
// parameter is not applicable and should be removed to prevent confusion and ensure correct
// rule configuration in SonarQube.
// Remove "scope" parameter, which is only relevant for template rules.

Copilot uses AI. Check for mistakes.

Copy link
Collaborator

Choose a reason for hiding this comment

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

How about this?

// Configurable rule scopes are only exposed for template rules.

rule.params().removeIf(param -> "scope".equals(param.key()));
}
}

private static String extractRuleKey(Class<?> ruleClass) {
Rule ruleAnnotation = AnnotationUtils.getAnnotation(ruleClass, Rule.class);
if (ruleAnnotation == null) {
throw new IllegalArgumentException(
"Rule class must have @Rule annotation: " + ruleClass.getName());
}
return ruleAnnotation.key();
}

private static boolean isTemplateRule(Class<?> ruleClass) {
return AnnotationUtils.getAnnotation(ruleClass, RuleTemplate.class) != null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,52 @@
import java.util.Collections;
import org.apache.commons.lang3.StringUtils;

/**
* Utility class for creating and configuring Delphi files for testing purposes.
*
* <p>This class provides convenient methods for parsing Delphi source code and creating mock
* configurations commonly used in unit tests.
*/
public final class DelphiFileUtils {
private DelphiFileUtils() {
// Utility class
}

/**
* Parses the given lines of Delphi source code into a DelphiFile.
*
* <p>This method creates a temporary file with the provided content and parses it using a mock
* configuration. The temporary file is automatically deleted when the JVM exits.
*
* @param lines the lines of Delphi source code to parse
* @return a parsed DelphiFile instance
* @throws UncheckedIOException if an I/O error occurs during file creation or writing
*/
public static DelphiFile parse(String... lines) {
try {
Path path = Files.createTempFile(null, ".pas");
Path path = Files.createTempFile("delphi-test-", ".pas");
Files.writeString(path, "\uFEFF" + StringUtils.join(lines, '\n'), StandardCharsets.UTF_8);
path.toFile().deleteOnExit();
return DelphiFile.from(path.toFile(), mockConfig());
} catch (IOException e) {
throw new UncheckedIOException(e);
throw new UncheckedIOException("Failed to create temporary Delphi file for parsing", e);
}
}

/**
* Creates a mock DelphiFileConfig with default settings suitable for testing.
*
* <p>The mock configuration includes:
*
* <ul>
* <li>UTF-8 encoding
* <li>Default compiler version and Windows platform
* <li>Default type factory
* <li>Empty search path and definitions
* </ul>
*
* @return a mock DelphiFileConfig instance
*/
public static DelphiFileConfig mockConfig() {
DelphiFileConfig mock = mock(DelphiFileConfig.class);
when(mock.getEncoding()).thenReturn(StandardCharsets.UTF_8.name());
Expand Down
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is an arbitrary change, revert it.

Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
<h2>Why is this an issue?</h2>
<p>My stupid rule to avoid routine declaration</p>
<p>This rule detects routine declarations that should be avoided according to coding standards.
Routine declarations in certain contexts can lead to maintenance issues and reduced code readability.</p>
<h3>Noncompliant Code Example</h3>
<pre>
TO DO
procedure MyProcedure(); forward; // Noncompliant - forward declaration usage

function Calculate(X: Integer): Integer; external; // Noncompliant - external routine
</pre>
<h3>Compliant Solution</h3>
<pre>
TO DO
function Calculate(X: Integer): Integer; // Compliant - proper implementation
begin
Result := X * 2;
end;
</pre>