Skip to content

Commit 92f9927

Browse files
cirrasfourls
authored andcommitted
Implement FormDfm check
1 parent 919566b commit 92f9927

File tree

7 files changed

+373
-0
lines changed

7 files changed

+373
-0
lines changed

CHANGELOG.md

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

1212
- Support for the `LLVM` symbol, which is defined on LLVM-based toolchains from Delphi 12 onward.
1313
- Support for the `IOSSIMULATOR` symbol, which is defined on the `DCCIOSSIMARM64` toolchain.
14+
- `FormDfm` analysis rule, which flags VCL forms/frames that lack a `.dfm` resource.
1415
- **API:** `CompilerDirectiveParser` now returns a new `ResourceDirective` type when parsing
1516
[resource directives](https://docwiki.embarcadero.com/RADStudio/en/Resource_file_(Delphi)).
1617

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Sonar Delphi Plugin
3+
* Copyright (C) 2024 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 java.util.Optional;
22+
import org.apache.commons.lang3.StringUtils;
23+
import org.sonar.plugins.communitydelphi.api.ast.DelphiAst;
24+
import org.sonar.plugins.communitydelphi.api.ast.DelphiNode;
25+
import org.sonar.plugins.communitydelphi.api.ast.TypeDeclarationNode;
26+
import org.sonar.plugins.communitydelphi.api.check.DelphiCheck;
27+
import org.sonar.plugins.communitydelphi.api.check.DelphiCheckContext;
28+
import org.sonar.plugins.communitydelphi.api.directive.CompilerDirective;
29+
import org.sonar.plugins.communitydelphi.api.directive.ResourceDirective;
30+
import org.sonar.plugins.communitydelphi.api.token.DelphiToken;
31+
import org.sonar.plugins.communitydelphi.api.type.Type;
32+
33+
abstract class AbstractFormResourceCheck extends DelphiCheck {
34+
protected abstract String getFrameworkName();
35+
36+
protected abstract String getFormTypeImage();
37+
38+
protected abstract String getFrameTypeImage();
39+
40+
protected abstract String getResourceFileExtension();
41+
42+
@Override
43+
public DelphiCheckContext visit(DelphiAst ast, DelphiCheckContext context) {
44+
if (context.getTokens().stream()
45+
.filter(DelphiToken::isCompilerDirective)
46+
.map(token -> context.getCompilerDirectiveParser().parse(token))
47+
.flatMap(Optional::stream)
48+
.anyMatch(this::isFormResource)) {
49+
return context;
50+
}
51+
return super.visit(ast, context);
52+
}
53+
54+
@Override
55+
public DelphiCheckContext visit(TypeDeclarationNode declaration, DelphiCheckContext context) {
56+
DelphiNode location = declaration.getTypeNameNode();
57+
58+
Type type = declaration.getType();
59+
if (!type.isAlias()) {
60+
if (type.isDescendantOf(getFormTypeImage())) {
61+
reportIssue(context, location, getMessage("form"));
62+
} else if (type.isDescendantOf(getFrameTypeImage())) {
63+
reportIssue(context, location, getMessage("frame"));
64+
}
65+
}
66+
67+
return context;
68+
}
69+
70+
private boolean isFormResource(CompilerDirective directive) {
71+
return directive instanceof ResourceDirective
72+
&& StringUtils.endsWithIgnoreCase(
73+
((ResourceDirective) directive).getResourceFile(), "." + getResourceFileExtension());
74+
}
75+
76+
private String getMessage(String componentKind) {
77+
return String.format(
78+
"Add a '.%s' resource for this %s %s.",
79+
getResourceFileExtension(), getFrameworkName(), componentKind);
80+
}
81+
}

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
@@ -77,6 +77,7 @@ public final class CheckList {
7777
ForbiddenPropertyCheck.class,
7878
ForbiddenRoutineCheck.class,
7979
ForbiddenTypeCheck.class,
80+
FormDfmCheck.class,
8081
FreeAndNilTObjectCheck.class,
8182
GotoStatementCheck.class,
8283
GroupedFieldDeclarationCheck.class,
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Sonar Delphi Plugin
3+
* Copyright (C) 2024 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+
23+
@Rule(key = "FormDfm")
24+
public class FormDfmCheck extends AbstractFormResourceCheck {
25+
26+
@Override
27+
protected String getFrameworkName() {
28+
return "VCL";
29+
}
30+
31+
@Override
32+
protected String getFormTypeImage() {
33+
return "Vcl.Forms.TForm";
34+
}
35+
36+
@Override
37+
protected String getFrameTypeImage() {
38+
return "Vcl.Forms.TFrame";
39+
}
40+
41+
@Override
42+
protected String getResourceFileExtension() {
43+
return "dfm";
44+
}
45+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<h2>Why is this an issue?</h2>
2+
<p>
3+
Omitting the corresponding <code>.dfm</code> resource for a VCL form or frame is typically a
4+
programming error. While there are some cases that won't cause problems at runtime, it remains
5+
unintuitive and confusing.
6+
</p>
7+
<p>
8+
Additionally, a unit that doesn't contain a <code>.dfm</code> resource will lack design-time form
9+
editing functionality.
10+
</p>
11+
<p>
12+
This rule flags any <code>TForm</code> or <code>TFrame</code> descendant in a unit that doesn't
13+
include a <code>.dfm</code> resource.
14+
</p>
15+
<h2>How to fix it</h2>
16+
<p>Add a <code>.dfm</code> resource to the unit:</p>
17+
<pre data-diff-id="1" data-diff-type="noncompliant">
18+
unit Foo;
19+
20+
interface
21+
22+
uses
23+
Vcl.Forms;
24+
25+
type
26+
TFoo = class(TForm)
27+
// ...
28+
end;
29+
30+
implementation
31+
32+
end.
33+
</pre>
34+
<pre data-diff-id="1" data-diff-type="compliant">
35+
unit Foo;
36+
37+
interface
38+
39+
uses
40+
Vcl.Forms;
41+
42+
type
43+
TFoo = class(TForm)
44+
// ...
45+
end;
46+
47+
implementation
48+
49+
{$R *.dfm}
50+
51+
end.
52+
</pre>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"title": "VCL forms and frames should have a corresponding .dfm form file",
3+
"type": "CODE_SMELL",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant/Issue",
7+
"constantCost": "5min"
8+
},
9+
"code": {
10+
"attribute": "COMPLETE",
11+
"impacts": {
12+
"MAINTAINABILITY": "MEDIUM"
13+
}
14+
},
15+
"tags": ["bad-practice", "confusing"],
16+
"defaultSeverity": "Major",
17+
"scope": "ALL",
18+
"quickfix": "unknown"
19+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* Sonar Delphi Plugin
3+
* Copyright (C) 2024 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 au.com.integradev.delphi.builders.DelphiTestUnitBuilder;
22+
import au.com.integradev.delphi.checks.verifier.CheckVerifier;
23+
import org.junit.jupiter.api.Test;
24+
25+
class FormDfmCheckTest {
26+
@Test
27+
void testNormalClassWithoutDfmShouldNotAddIssue() {
28+
CheckVerifier.newVerifier()
29+
.withCheck(new FormDfmCheck())
30+
.onFile(
31+
new DelphiTestUnitBuilder()
32+
.appendDecl("type")
33+
.appendDecl(" TFoo = class(TObject)")
34+
.appendDecl(" end;"))
35+
.verifyNoIssues();
36+
}
37+
38+
@Test
39+
void testAliasesWithoutDfmShouldNotAddIssue() {
40+
CheckVerifier.newVerifier()
41+
.withCheck(new FormDfmCheck())
42+
.withSearchPathUnit(createVclForms())
43+
.onFile(
44+
new DelphiTestUnitBuilder()
45+
.appendDecl("uses")
46+
.appendDecl(" Vcl.Forms;")
47+
.appendDecl("type")
48+
.appendDecl(" TFormWeakAlias = TForm;")
49+
.appendDecl(" TFrameWeakAlias = TFrame;")
50+
.appendDecl(" TFormStrongAlias = type TForm;")
51+
.appendDecl(" TFrameStrongAlias = type TFrame;"))
52+
.verifyNoIssues();
53+
}
54+
55+
@Test
56+
void testFormWithDfmShouldNotAddIssue() {
57+
CheckVerifier.newVerifier()
58+
.withCheck(new FormDfmCheck())
59+
.withSearchPathUnit(createVclForms())
60+
.onFile(
61+
new DelphiTestUnitBuilder()
62+
.appendDecl("uses")
63+
.appendDecl(" Vcl.Forms;")
64+
.appendDecl("type")
65+
.appendDecl(" TFoo = class(TForm)")
66+
.appendDecl(" end;")
67+
.appendImpl("{$R Foo.dfm}"))
68+
.verifyNoIssues();
69+
}
70+
71+
@Test
72+
void testFrameWithDfmShouldNotAddIssue() {
73+
CheckVerifier.newVerifier()
74+
.withCheck(new FormDfmCheck())
75+
.withSearchPathUnit(createVclForms())
76+
.onFile(
77+
new DelphiTestUnitBuilder()
78+
.appendDecl("uses")
79+
.appendDecl(" Vcl.Forms;")
80+
.appendDecl("type")
81+
.appendDecl(" TFoo = class(TFrame)")
82+
.appendDecl(" end;")
83+
.appendImpl("{$R Foo.dfm}"))
84+
.verifyNoIssues();
85+
}
86+
87+
@Test
88+
void testFormWithoutDfmShouldAddIssue() {
89+
CheckVerifier.newVerifier()
90+
.withCheck(new FormDfmCheck())
91+
.withSearchPathUnit(createVclForms())
92+
.onFile(
93+
new DelphiTestUnitBuilder()
94+
.appendDecl("uses")
95+
.appendDecl(" Vcl.Forms;")
96+
.appendDecl("type")
97+
.appendDecl(" TFoo = class(TForm) // Noncompliant")
98+
.appendDecl(" end;"))
99+
.verifyIssues();
100+
}
101+
102+
@Test
103+
void testFrameWithoutDfmShouldAddIssue() {
104+
CheckVerifier.newVerifier()
105+
.withCheck(new FormDfmCheck())
106+
.withSearchPathUnit(createVclForms())
107+
.onFile(
108+
new DelphiTestUnitBuilder()
109+
.appendDecl("uses")
110+
.appendDecl(" Vcl.Forms;")
111+
.appendDecl("type")
112+
.appendDecl(" TFoo = class(TFrame) // Noncompliant")
113+
.appendDecl(" end;"))
114+
.verifyIssues();
115+
}
116+
117+
@Test
118+
void testNonDfmResourceShouldAddIssue() {
119+
CheckVerifier.newVerifier()
120+
.withCheck(new FormDfmCheck())
121+
.withSearchPathUnit(createVclForms())
122+
.onFile(
123+
new DelphiTestUnitBuilder()
124+
.appendDecl("uses")
125+
.appendDecl(" Vcl.Forms;")
126+
.appendDecl("type")
127+
.appendDecl(" TFoo = class(TForm) // Noncompliant")
128+
.appendDecl(" end;")
129+
.appendImpl("{$R Foo.bar}"))
130+
.verifyIssues();
131+
}
132+
133+
@Test
134+
void testNonDfmResourceContainingDotDfmShouldAddIssue() {
135+
CheckVerifier.newVerifier()
136+
.withCheck(new FormDfmCheck())
137+
.withSearchPathUnit(createVclForms())
138+
.onFile(
139+
new DelphiTestUnitBuilder()
140+
.appendDecl("uses")
141+
.appendDecl(" Vcl.Forms;")
142+
.appendDecl("type")
143+
.appendDecl(" TFoo = class(TForm) // Noncompliant")
144+
.appendDecl(" end;")
145+
.appendImpl("{$R Foo.dfm.bar}"))
146+
.verifyIssues();
147+
}
148+
149+
@Test
150+
void testIncludeDfmShouldAddIssue() {
151+
CheckVerifier.newVerifier()
152+
.withCheck(new FormDfmCheck())
153+
.withSearchPathUnit(createVclForms())
154+
.onFile(
155+
new DelphiTestUnitBuilder()
156+
.appendDecl("uses")
157+
.appendDecl(" Vcl.Forms;")
158+
.appendDecl("type")
159+
.appendDecl(" TFoo = class(TForm) // Noncompliant")
160+
.appendDecl(" end;")
161+
.appendImpl("{$I Foo.dfm}"))
162+
.verifyIssues();
163+
}
164+
165+
private static DelphiTestUnitBuilder createVclForms() {
166+
return new DelphiTestUnitBuilder()
167+
.unitName("Vcl.Forms")
168+
.appendDecl("type")
169+
.appendDecl(" TForm = class")
170+
.appendDecl(" end;")
171+
.appendDecl(" TFrame = class")
172+
.appendDecl(" end;");
173+
}
174+
}

0 commit comments

Comments
 (0)