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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `ParsingError` analysis rule, which flags files where parsing failures occurred.
- Support for the `WEAK_NATIVEINT` symbol, which is defined from Delphi 12 onward.
- Support for undocumented intrinsics usable within `varargs` routines:
- `VarArgStart`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ public final class CheckList {
NoSonarCheck.class,
NonLinearCastCheck.class,
ObjectTypeCheck.class,
ParsingErrorCheck.class,
PascalStyleResultCheck.class,
PlatformDependentCastCheck.class,
PlatformDependentTruncationCheck.class,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Sonar Delphi Plugin
* Copyright (C) 2025 Integrated Application Development
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
*/
package au.com.integradev.delphi.checks;

import org.sonar.check.Rule;
import org.sonar.plugins.communitydelphi.api.ast.DelphiAst;
import org.sonar.plugins.communitydelphi.api.check.DelphiCheck;
import org.sonar.plugins.communitydelphi.api.check.DelphiCheckContext;

@Rule(key = "ParsingError")
public class ParsingErrorCheck extends DelphiCheck {
@Override
public DelphiCheckContext visit(DelphiAst ast, DelphiCheckContext context) {
// Dummy implementation to satisfy the check registrar.
// See DelphiSensor::handleParsingError.
return context;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<h2>Why is this an issue?</h2>
<p>
When the Delphi parser fails, it is possible to record the failure as a violation on the file.
This way, not only it is possible to track the number of files that do not parse but also to
easily find out why they do not parse.
</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"title": "Delphi parser failure",
"type": "CODE_SMELL",
"status": "ready",
"remediation": {
"func": "Constant/Issue",
"constantCost": "30min"
},
"code": {
"attribute": "CONVENTIONAL",
"impacts": {
"MAINTAINABILITY": "MEDIUM"
}
},
"tags": ["suspicious"],
"defaultSeverity": "Major",
"scope": "ALL",
"quickfix": "unknown"
}
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ void testChecksHaveAnAssociatedTest() {
.areAssignableTo(DelphiCheck.class)
.and()
.doNotHaveModifier(JavaModifier.ABSTRACT)
.and()
.doNotBelongToAnyOf(ParsingErrorCheck.class)
.should(HAVE_ASSOCIATED_TEST)
.allowEmptyShould(true)
.check(CHECKS_PACKAGE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ import org.apache.commons.lang3.StringUtils;
public void reportError(RecognitionException e) {
String hdr = this.getErrorHeader(e);
String msg = this.getErrorMessage(e, this.getTokenNames());
throw new LexerException(hdr + " " + msg, e);
throw new LexerException(hdr + " " + msg, e.line, e);
}

@Override
Expand All @@ -152,12 +152,20 @@ import org.apache.commons.lang3.StringUtils;
}

public static class LexerException extends RuntimeException {
public LexerException(String message) {
private final int line;

public LexerException(String message, int line) {
super(message);
this.line = line;
}

public LexerException(String message, Throwable cause) {
public LexerException(String message, int line, Throwable cause) {
super(message, cause);
this.line = line;
}

public int getLine() {
return line;
}
}

Expand Down Expand Up @@ -234,7 +242,8 @@ import org.apache.commons.lang3.StringUtils;
+ state.tokenStartLine
+ ":"
+ state.tokenStartCharPositionInLine
+ " unterminated multi-line comment");
+ " unterminated multi-line comment",
state.tokenStartLine);

default:
// do nothing
Expand Down Expand Up @@ -350,7 +359,7 @@ import org.apache.commons.lang3.StringUtils;
public void reportError(RecognitionException e) {
String hdr = this.getErrorHeader(e);
String msg = this.getErrorMessage(e, this.getTokenNames());
throw new ParserException(hdr + " " + msg, e);
throw new ParserException(hdr + " " + msg, e.line, e);
}

@Override
Expand All @@ -373,8 +382,15 @@ import org.apache.commons.lang3.StringUtils;
}

public static class ParserException extends RuntimeException {
public ParserException(String message, Throwable cause) {
private final int line;

public ParserException(String message, int line, Throwable cause) {
super(message, cause);
this.line = line;
}

public int getLine() {
return line;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ private static DelphiAst createAST(
token.getChannel() == Token.HIDDEN_CHANNEL || token.getType() == Token.EOF);

if (isEmptyFile) {
throw new EmptyDelphiFileException("Empty files are not allowed.");
throw new EmptyDelphiFileException("Empty files are not allowed");
}

DelphiParser parser = new DelphiParser(tokenStream);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import static au.com.integradev.delphi.utils.DelphiUtils.inputFilesToPaths;
import static au.com.integradev.delphi.utils.DelphiUtils.stopProgressReport;

import au.com.integradev.delphi.antlr.DelphiLexer.LexerException;
import au.com.integradev.delphi.antlr.DelphiParser.ParserException;
import au.com.integradev.delphi.compiler.CompilerVersion;
import au.com.integradev.delphi.compiler.Toolchain;
import au.com.integradev.delphi.core.Delphi;
Expand All @@ -33,6 +35,7 @@
import au.com.integradev.delphi.file.DelphiFile;
import au.com.integradev.delphi.file.DelphiFile.DelphiFileConstructionException;
import au.com.integradev.delphi.file.DelphiFile.DelphiInputFile;
import au.com.integradev.delphi.file.DelphiFile.EmptyDelphiFileException;
import au.com.integradev.delphi.file.DelphiFileConfig;
import au.com.integradev.delphi.msbuild.DelphiProjectHelper;
import au.com.integradev.delphi.preprocessor.DelphiPreprocessorFactory;
Expand All @@ -51,6 +54,9 @@
import org.sonar.api.batch.sensor.Sensor;
import org.sonar.api.batch.sensor.SensorContext;
import org.sonar.api.batch.sensor.SensorDescriptor;
import org.sonar.api.batch.sensor.issue.NewIssue;
import org.sonar.api.batch.sensor.issue.NewIssueLocation;
import org.sonar.api.rule.RuleKey;
import org.sonarsource.analyzer.commons.ProgressReport;

public class DelphiSensor implements Sensor {
Expand Down Expand Up @@ -135,13 +141,14 @@ private void executeOnFiles(SensorContext sensorContext) {
try {
for (Path sourceFile : sourceFiles) {
String absolutePath = sourceFile.toAbsolutePath().toString();
InputFile inputFile = delphiProjectHelper.getFile(absolutePath);
try {
InputFile inputFile = delphiProjectHelper.getFile(absolutePath);
DelphiInputFile delphiFile = DelphiInputFile.from(inputFile, config);
executor.execute(executorContext, delphiFile);
progressReport.nextFile();
} catch (DelphiFileConstructionException e) {
LOG.error("Error while analyzing {}", absolutePath, e);
handleParsingError(sensorContext, inputFile, e);
}
}
success = true;
Expand All @@ -150,6 +157,37 @@ private void executeOnFiles(SensorContext sensorContext) {
}
}

private static void handleParsingError(
SensorContext context, InputFile inputFile, DelphiFileConstructionException e) {
Throwable cause = e.getCause();
if (cause instanceof LexerException
|| cause instanceof ParserException
|| cause instanceof EmptyDelphiFileException) {
NewIssue newIssue =
context.newIssue().forRule(RuleKey.of("community-delphi", "ParsingError"));

NewIssueLocation primaryLocation =
newIssue
.newLocation()
.on(inputFile)
.message(String.format("Parse error (%s)", cause.getMessage()));

int line = 0;
if (cause instanceof ParserException) {
line = ((ParserException) cause).getLine();
} else if (cause instanceof LexerException) {
line = ((LexerException) cause).getLine();
}

if (line != 0) {
primaryLocation.at(inputFile.selectLine(line));
}

newIssue.at(primaryLocation);
newIssue.save();
}
}

private SearchPath createSearchPath() {
/*
CodeGear.Delphi.Targets appends the library paths to DCC_UnitSearchPath to create a new
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import au.com.integradev.delphi.executor.DelphiMasterExecutor;
import au.com.integradev.delphi.msbuild.DelphiProjectHelper;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
Expand All @@ -42,7 +43,10 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.fs.TextRange;
import org.sonar.api.batch.fs.internal.TestInputFileBuilder;
import org.sonar.api.batch.sensor.SensorDescriptor;
import org.sonar.api.batch.sensor.internal.SensorContextTester;

class DelphiSensorTest {
private final DelphiMasterExecutor executor = mock(DelphiMasterExecutor.class);
Expand Down Expand Up @@ -84,15 +88,53 @@ void setup() throws IOException {
+ "implementation\n"
+ "end.");

setupFile("unit SourceFile;\ninterface\nimplementation\nend.");
when(delphiProjectHelper.standardLibraryPath()).thenReturn(standardLibraryPath);
}

private void setupFile(String content) {
Path sourceFilePath = baseDir.resolve("SourceFile.pas");
Files.writeString(sourceFilePath, "unit SourceFile;\ninterface\nimplementation\nend.");

InputFile inputFile = mock(InputFile.class);
when(inputFile.uri()).thenReturn(sourceFilePath.toUri());
try {
Files.writeString(sourceFilePath, content);
} catch (IOException e) {
throw new UncheckedIOException(e);
}

InputFile inputFile =
TestInputFileBuilder.create("moduleKey", baseDir.toFile(), sourceFilePath.toFile())
.setContents(content)
.setLanguage(Delphi.KEY)
.setType(InputFile.Type.MAIN)
.build();

when(delphiProjectHelper.inputFiles()).thenReturn(List.of(inputFile));
when(delphiProjectHelper.getFile(anyString())).thenReturn(inputFile);
when(delphiProjectHelper.standardLibraryPath()).thenReturn(standardLibraryPath);
}

private void assertParsingErrorIssue(int expectedLine, String expectedMessage) {
SensorContextTester context = SensorContextTester.create(baseDir);

sensor.execute(context);

assertThat(context.allIssues())
.hasSize(1)
.element(0)
.satisfies(
issue -> {
assertThat(issue.ruleKey().repository()).isEqualTo("community-delphi");
assertThat(issue.ruleKey().rule()).isEqualTo("ParsingError");
assertThat(issue.primaryLocation().message()).isEqualTo(expectedMessage);

TextRange position = issue.primaryLocation().textRange();
if (expectedLine == 0) {
assertThat(position).isNull();
} else {
assertThat(position).isNotNull();
assertThat(position.start().line()).isEqualTo(expectedLine);
assertThat(position.end().line()).isEqualTo(expectedLine);
}
});
}

@AfterEach
Expand Down Expand Up @@ -143,4 +185,23 @@ void testWhenShouldExecuteOnProjectReturnsTrueThenExecutorIsCalled() {

verify(executor, times(1)).execute(any(), any());
}

@Test
void testFileWithLexerErrorRaisesParsingErrorIssue() {
setupFile("\n\n'unterminated string literal");
assertParsingErrorIssue(
3, "Parse error (line 3:28 mismatched character '<EOF>' expecting ''')");
}

@Test
void testFileWithParserErrorRaisesParsingErrorIssue() {
setupFile("\n\n\n\n;");
assertParsingErrorIssue(5, "Parse error (line 5:0 no viable alternative at input ';')");
}

@Test
void testEmptyFileRaisesParsingErrorIssue() {
setupFile("");
assertParsingErrorIssue(0, "Parse error (Empty files are not allowed)");
}
}