Skip to content

Commit be07d4a

Browse files
committed
Implement ParsingError check
1 parent 76eec4d commit be07d4a

File tree

10 files changed

+190
-12
lines changed

10 files changed

+190
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- `ParsingError` analysis rule, which flags files where parsing failures occurred.
1213
- Support for the `WEAK_NATIVEINT` symbol, which is defined from Delphi 12 onward.
1314
- Support for undocumented intrinsics usable within `varargs` routines:
1415
- `VarArgStart`

delphi-checks/src/main/java/au/com/integradev/delphi/checks/CheckList.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ public final class CheckList {
115115
NoSonarCheck.class,
116116
NonLinearCastCheck.class,
117117
ObjectTypeCheck.class,
118+
ParsingErrorCheck.class,
118119
PascalStyleResultCheck.class,
119120
PlatformDependentCastCheck.class,
120121
PlatformDependentTruncationCheck.class,
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Sonar Delphi Plugin
3+
* Copyright (C) 2025 Integrated Application Development
4+
*
5+
* This program is free software; you can redistribute it and/or
6+
* modify it under the terms of the GNU Lesser General Public
7+
* License as published by the Free Software Foundation; either
8+
* version 3 of the License, or (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
* Lesser General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Lesser General Public
16+
* License along with this program; if not, write to the Free Software
17+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
18+
*/
19+
package au.com.integradev.delphi.checks;
20+
21+
import org.sonar.check.Rule;
22+
import org.sonar.plugins.communitydelphi.api.ast.DelphiAst;
23+
import org.sonar.plugins.communitydelphi.api.check.DelphiCheck;
24+
import org.sonar.plugins.communitydelphi.api.check.DelphiCheckContext;
25+
26+
@Rule(key = "ParsingError")
27+
public class ParsingErrorCheck extends DelphiCheck {
28+
@Override
29+
public DelphiCheckContext visit(DelphiAst ast, DelphiCheckContext context) {
30+
// Dummy implementation to satisfy the check registrar.
31+
// See DelphiSensor::handleParsingError.
32+
return context;
33+
}
34+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<h2>Why is this an issue?</h2>
2+
<p>
3+
When the Delphi parser fails, it is possible to record the failure as a violation on the file.
4+
This way, not only it is possible to track the number of files that do not parse but also to
5+
easily find out why they do not parse.
6+
</p>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"title": "Delphi parser failure",
3+
"type": "CODE_SMELL",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant/Issue",
7+
"constantCost": "30min"
8+
},
9+
"code": {
10+
"attribute": "CONVENTIONAL",
11+
"impacts": {
12+
"MAINTAINABILITY": "MEDIUM"
13+
}
14+
},
15+
"tags": ["suspicious"],
16+
"defaultSeverity": "Major",
17+
"scope": "ALL",
18+
"quickfix": "unknown"
19+
}

delphi-checks/src/test/java/au/com/integradev/delphi/checks/CheckTestNameTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,8 @@ void testChecksHaveAnAssociatedTest() {
208208
.areAssignableTo(DelphiCheck.class)
209209
.and()
210210
.doNotHaveModifier(JavaModifier.ABSTRACT)
211+
.and()
212+
.doNotBelongToAnyOf(ParsingErrorCheck.class)
211213
.should(HAVE_ASSOCIATED_TEST)
212214
.allowEmptyShould(true)
213215
.check(CHECKS_PACKAGE);

delphi-frontend/src/main/antlr3/au/com/integradev/delphi/antlr/Delphi.g

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ import org.apache.commons.lang3.StringUtils;
143143
public void reportError(RecognitionException e) {
144144
String hdr = this.getErrorHeader(e);
145145
String msg = this.getErrorMessage(e, this.getTokenNames());
146-
throw new LexerException(hdr + " " + msg, e);
146+
throw new LexerException(hdr + " " + msg, e.line, e);
147147
}
148148

149149
@Override
@@ -152,12 +152,20 @@ import org.apache.commons.lang3.StringUtils;
152152
}
153153

154154
public static class LexerException extends RuntimeException {
155-
public LexerException(String message) {
155+
private final int line;
156+
157+
public LexerException(String message, int line) {
156158
super(message);
159+
this.line = line;
157160
}
158161

159-
public LexerException(String message, Throwable cause) {
162+
public LexerException(String message, int line, Throwable cause) {
160163
super(message, cause);
164+
this.line = line;
165+
}
166+
167+
public int getLine() {
168+
return line;
161169
}
162170
}
163171

@@ -234,7 +242,8 @@ import org.apache.commons.lang3.StringUtils;
234242
+ state.tokenStartLine
235243
+ ":"
236244
+ state.tokenStartCharPositionInLine
237-
+ " unterminated multi-line comment");
245+
+ " unterminated multi-line comment",
246+
state.tokenStartLine);
238247

239248
default:
240249
// do nothing
@@ -350,7 +359,7 @@ import org.apache.commons.lang3.StringUtils;
350359
public void reportError(RecognitionException e) {
351360
String hdr = this.getErrorHeader(e);
352361
String msg = this.getErrorMessage(e, this.getTokenNames());
353-
throw new ParserException(hdr + " " + msg, e);
362+
throw new ParserException(hdr + " " + msg, e.line, e);
354363
}
355364

356365
@Override
@@ -373,8 +382,15 @@ import org.apache.commons.lang3.StringUtils;
373382
}
374383

375384
public static class ParserException extends RuntimeException {
376-
public ParserException(String message, Throwable cause) {
385+
private final int line;
386+
387+
public ParserException(String message, int line, Throwable cause) {
377388
super(message, cause);
389+
this.line = line;
390+
}
391+
392+
public int getLine() {
393+
return line;
378394
}
379395
}
380396
}

delphi-frontend/src/main/java/au/com/integradev/delphi/file/DelphiFile.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ private static DelphiAst createAST(
183183
token.getChannel() == Token.HIDDEN_CHANNEL || token.getType() == Token.EOF);
184184

185185
if (isEmptyFile) {
186-
throw new EmptyDelphiFileException("Empty files are not allowed.");
186+
throw new EmptyDelphiFileException("Empty files are not allowed");
187187
}
188188

189189
DelphiParser parser = new DelphiParser(tokenStream);

sonar-delphi-plugin/src/main/java/au/com/integradev/delphi/DelphiSensor.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import static au.com.integradev.delphi.utils.DelphiUtils.inputFilesToPaths;
2626
import static au.com.integradev.delphi.utils.DelphiUtils.stopProgressReport;
2727

28+
import au.com.integradev.delphi.antlr.DelphiLexer.LexerException;
29+
import au.com.integradev.delphi.antlr.DelphiParser.ParserException;
2830
import au.com.integradev.delphi.compiler.CompilerVersion;
2931
import au.com.integradev.delphi.compiler.Toolchain;
3032
import au.com.integradev.delphi.core.Delphi;
@@ -33,6 +35,7 @@
3335
import au.com.integradev.delphi.file.DelphiFile;
3436
import au.com.integradev.delphi.file.DelphiFile.DelphiFileConstructionException;
3537
import au.com.integradev.delphi.file.DelphiFile.DelphiInputFile;
38+
import au.com.integradev.delphi.file.DelphiFile.EmptyDelphiFileException;
3639
import au.com.integradev.delphi.file.DelphiFileConfig;
3740
import au.com.integradev.delphi.msbuild.DelphiProjectHelper;
3841
import au.com.integradev.delphi.preprocessor.DelphiPreprocessorFactory;
@@ -51,6 +54,9 @@
5154
import org.sonar.api.batch.sensor.Sensor;
5255
import org.sonar.api.batch.sensor.SensorContext;
5356
import org.sonar.api.batch.sensor.SensorDescriptor;
57+
import org.sonar.api.batch.sensor.issue.NewIssue;
58+
import org.sonar.api.batch.sensor.issue.NewIssueLocation;
59+
import org.sonar.api.rule.RuleKey;
5460
import org.sonarsource.analyzer.commons.ProgressReport;
5561

5662
public class DelphiSensor implements Sensor {
@@ -135,13 +141,14 @@ private void executeOnFiles(SensorContext sensorContext) {
135141
try {
136142
for (Path sourceFile : sourceFiles) {
137143
String absolutePath = sourceFile.toAbsolutePath().toString();
144+
InputFile inputFile = delphiProjectHelper.getFile(absolutePath);
138145
try {
139-
InputFile inputFile = delphiProjectHelper.getFile(absolutePath);
140146
DelphiInputFile delphiFile = DelphiInputFile.from(inputFile, config);
141147
executor.execute(executorContext, delphiFile);
142148
progressReport.nextFile();
143149
} catch (DelphiFileConstructionException e) {
144150
LOG.error("Error while analyzing {}", absolutePath, e);
151+
handleParsingError(sensorContext, inputFile, e);
145152
}
146153
}
147154
success = true;
@@ -150,6 +157,37 @@ private void executeOnFiles(SensorContext sensorContext) {
150157
}
151158
}
152159

160+
private static void handleParsingError(
161+
SensorContext context, InputFile inputFile, DelphiFileConstructionException e) {
162+
Throwable cause = e.getCause();
163+
if (cause instanceof LexerException
164+
|| cause instanceof ParserException
165+
|| cause instanceof EmptyDelphiFileException) {
166+
NewIssue newIssue =
167+
context.newIssue().forRule(RuleKey.of("community-delphi", "ParsingError"));
168+
169+
NewIssueLocation primaryLocation =
170+
newIssue
171+
.newLocation()
172+
.on(inputFile)
173+
.message(String.format("Parse error (%s)", cause.getMessage()));
174+
175+
int line = 0;
176+
if (cause instanceof ParserException) {
177+
line = ((ParserException) cause).getLine();
178+
} else if (cause instanceof LexerException) {
179+
line = ((LexerException) cause).getLine();
180+
}
181+
182+
if (line != 0) {
183+
primaryLocation.at(inputFile.selectLine(line));
184+
}
185+
186+
newIssue.at(primaryLocation);
187+
newIssue.save();
188+
}
189+
}
190+
153191
private SearchPath createSearchPath() {
154192
/*
155193
CodeGear.Delphi.Targets appends the library paths to DCC_UnitSearchPath to create a new

sonar-delphi-plugin/src/test/java/au/com/integradev/delphi/DelphiSensorTest.java

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import au.com.integradev.delphi.executor.DelphiMasterExecutor;
3535
import au.com.integradev.delphi.msbuild.DelphiProjectHelper;
3636
import java.io.IOException;
37+
import java.io.UncheckedIOException;
3738
import java.nio.file.Files;
3839
import java.nio.file.Path;
3940
import java.util.List;
@@ -42,7 +43,10 @@
4243
import org.junit.jupiter.api.BeforeEach;
4344
import org.junit.jupiter.api.Test;
4445
import org.sonar.api.batch.fs.InputFile;
46+
import org.sonar.api.batch.fs.TextRange;
47+
import org.sonar.api.batch.fs.internal.TestInputFileBuilder;
4548
import org.sonar.api.batch.sensor.SensorDescriptor;
49+
import org.sonar.api.batch.sensor.internal.SensorContextTester;
4650

4751
class DelphiSensorTest {
4852
private final DelphiMasterExecutor executor = mock(DelphiMasterExecutor.class);
@@ -84,15 +88,53 @@ void setup() throws IOException {
8488
+ "implementation\n"
8589
+ "end.");
8690

91+
setupFile("unit SourceFile;\ninterface\nimplementation\nend.");
92+
when(delphiProjectHelper.standardLibraryPath()).thenReturn(standardLibraryPath);
93+
}
94+
95+
private void setupFile(String content) {
8796
Path sourceFilePath = baseDir.resolve("SourceFile.pas");
88-
Files.writeString(sourceFilePath, "unit SourceFile;\ninterface\nimplementation\nend.");
8997

90-
InputFile inputFile = mock(InputFile.class);
91-
when(inputFile.uri()).thenReturn(sourceFilePath.toUri());
98+
try {
99+
Files.writeString(sourceFilePath, content);
100+
} catch (IOException e) {
101+
throw new UncheckedIOException(e);
102+
}
103+
104+
InputFile inputFile =
105+
TestInputFileBuilder.create("moduleKey", baseDir.toFile(), sourceFilePath.toFile())
106+
.setContents(content)
107+
.setLanguage(Delphi.KEY)
108+
.setType(InputFile.Type.MAIN)
109+
.build();
92110

93111
when(delphiProjectHelper.inputFiles()).thenReturn(List.of(inputFile));
94112
when(delphiProjectHelper.getFile(anyString())).thenReturn(inputFile);
95-
when(delphiProjectHelper.standardLibraryPath()).thenReturn(standardLibraryPath);
113+
}
114+
115+
private void assertParsingErrorIssue(int expectedLine, String expectedMessage) {
116+
SensorContextTester context = SensorContextTester.create(baseDir);
117+
118+
sensor.execute(context);
119+
120+
assertThat(context.allIssues())
121+
.hasSize(1)
122+
.element(0)
123+
.satisfies(
124+
issue -> {
125+
assertThat(issue.ruleKey().repository()).isEqualTo("community-delphi");
126+
assertThat(issue.ruleKey().rule()).isEqualTo("ParsingError");
127+
assertThat(issue.primaryLocation().message()).isEqualTo(expectedMessage);
128+
129+
TextRange position = issue.primaryLocation().textRange();
130+
if (expectedLine == 0) {
131+
assertThat(position).isNull();
132+
} else {
133+
assertThat(position).isNotNull();
134+
assertThat(position.start().line()).isEqualTo(expectedLine);
135+
assertThat(position.end().line()).isEqualTo(expectedLine);
136+
}
137+
});
96138
}
97139

98140
@AfterEach
@@ -143,4 +185,23 @@ void testWhenShouldExecuteOnProjectReturnsTrueThenExecutorIsCalled() {
143185

144186
verify(executor, times(1)).execute(any(), any());
145187
}
188+
189+
@Test
190+
void testFileWithLexerErrorRaisesParsingErrorIssue() {
191+
setupFile("\n\n'unterminated string literal");
192+
assertParsingErrorIssue(
193+
3, "Parse error (line 3:28 mismatched character '<EOF>' expecting ''')");
194+
}
195+
196+
@Test
197+
void testFileWithParserErrorRaisesParsingErrorIssue() {
198+
setupFile("\n\n\n\n;");
199+
assertParsingErrorIssue(5, "Parse error (line 5:0 no viable alternative at input ';')");
200+
}
201+
202+
@Test
203+
void testEmptyFileRaisesParsingErrorIssue() {
204+
setupFile("");
205+
assertParsingErrorIssue(0, "Parse error (Empty files are not allowed)");
206+
}
146207
}

0 commit comments

Comments
 (0)