Skip to content

Commit ff771a7

Browse files
committedFeb 7, 2019
For #1007: Prohibit non final classes.
1 parent 558b197 commit ff771a7

File tree

3 files changed

+222
-15
lines changed

3 files changed

+222
-15
lines changed
 

‎qulice-checkstyle/src/main/java/com/qulice/checkstyle/ProhibitNonFinalClassesCheck.java

+208-15
Original file line numberDiff line numberDiff line change
@@ -30,36 +30,229 @@
3030
package com.qulice.checkstyle;
3131

3232
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
33+
import com.puppycrawl.tools.checkstyle.api.DetailAST;
34+
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
35+
import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
36+
import java.util.ArrayDeque;
37+
import java.util.Deque;
38+
import org.cactoos.text.JoinedText;
39+
import org.cactoos.text.UncheckedText;
3340

3441
/**
35-
* Checks if the classes in the file are final.
42+
* Checks that classes are declared as final. Doesn't check for classes nested
43+
* in interfaces or annotations, as they are always {@code final} there.
44+
* <p>
45+
* An example of how to configure the check is:
46+
* </p>
47+
* <pre>
48+
* &lt;module name="ProhibitNonFinalClassesCheck"/&gt;
49+
* </pre>
3650
*
37-
* @since 0.18
38-
* @todo #920:30min Qulice should prohibit any non-final class. Tests for
39-
* this check have already been implemented in ProhibitNonFinalClassesCheck
40-
* folder. After the implementation of this class add config.xml to folder
41-
* pointing to ProhibitNonFinalClassesCheck class, add check into ChecksTest
42-
* and add it to custom checks in checks.xml
51+
* @since 0.19
4352
*/
4453
public final class ProhibitNonFinalClassesCheck extends AbstractCheck {
54+
55+
/**
56+
* Character separate package names in qualified name of java class.
57+
*/
58+
private static final String PACKAGE_SEPARATOR = ".";
59+
60+
/**
61+
* Keeps ClassDesc objects for stack of declared classes.
62+
*/
63+
private Deque<ClassDesc> classes = new ArrayDeque<>();
64+
65+
/**
66+
* Full qualified name of the package.
67+
*/
68+
private String pack;
69+
4570
@Override
4671
public int[] getDefaultTokens() {
47-
throw new UnsupportedOperationException(
48-
"getDefaultTokens() not implemented"
49-
);
72+
return this.getRequiredTokens();
5073
}
5174

5275
@Override
5376
public int[] getAcceptableTokens() {
54-
throw new UnsupportedOperationException(
55-
"getAcceptableTokens() not implemented"
56-
);
77+
return this.getRequiredTokens();
5778
}
5879

5980
@Override
6081
public int[] getRequiredTokens() {
61-
throw new UnsupportedOperationException(
62-
"getRequiredTokens() not implemented"
82+
return new int[] {TokenTypes.CLASS_DEF};
83+
}
84+
85+
@Override
86+
public void beginTree(final DetailAST root) {
87+
this.classes = new ArrayDeque<>();
88+
this.pack = "";
89+
}
90+
91+
@Override
92+
public void visitToken(final DetailAST ast) {
93+
final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
94+
if (ast.getType() == TokenTypes.CLASS_DEF) {
95+
final boolean isfinal =
96+
modifiers.findFirstToken(TokenTypes.FINAL) != null;
97+
final boolean isabstract =
98+
modifiers.findFirstToken(TokenTypes.ABSTRACT) != null;
99+
final String qualified = this.getQualifiedClassName(ast);
100+
this.classes.push(
101+
new ClassDesc(qualified, isfinal, isabstract)
102+
);
103+
}
104+
}
105+
106+
@Override
107+
public void leaveToken(final DetailAST ast) {
108+
if (ast.getType() == TokenTypes.CLASS_DEF) {
109+
final ClassDesc desc = this.classes.pop();
110+
if (!desc.isDeclaredAsAbstract()
111+
&& !desc.isAsfinal()
112+
&& !ScopeUtil.isInInterfaceOrAnnotationBlock(ast)) {
113+
final String qualified = desc.getQualified();
114+
final String name =
115+
ProhibitNonFinalClassesCheck.getClassNameFromQualifiedName(
116+
qualified
117+
);
118+
log(ast.getLineNo(), "Classes should be final", name);
119+
}
120+
}
121+
}
122+
123+
/**
124+
* Get qualified class name from given class Ast.
125+
* @param classast Class to get qualified class name
126+
* @return Qualified class name of a class
127+
*/
128+
private String getQualifiedClassName(final DetailAST classast) {
129+
final String name = classast.findFirstToken(
130+
TokenTypes.IDENT
131+
).getText();
132+
String outer = null;
133+
if (!this.classes.isEmpty()) {
134+
outer = this.classes.peek().getQualified();
135+
}
136+
return ProhibitNonFinalClassesCheck.getQualifiedClassName(
137+
this.pack,
138+
outer,
139+
name
140+
);
141+
}
142+
143+
/**
144+
* Calculate qualified class name(package + class name) laying inside given
145+
* outer class.
146+
* @param pack Package name, empty string on default package
147+
* @param outer Qualified name(package + class) of outer
148+
* class, null if doesn't exist
149+
* @param name Class name
150+
* @return Qualified class name(package + class name)
151+
*/
152+
private static String getQualifiedClassName(
153+
final String pack,
154+
final String outer,
155+
final String name) {
156+
final String qualified;
157+
if (outer == null) {
158+
if (pack.isEmpty()) {
159+
qualified = name;
160+
} else {
161+
qualified =
162+
new UncheckedText(
163+
new JoinedText(
164+
ProhibitNonFinalClassesCheck.PACKAGE_SEPARATOR,
165+
pack,
166+
name
167+
)
168+
).asString();
169+
}
170+
} else {
171+
qualified =
172+
new UncheckedText(
173+
new JoinedText(
174+
ProhibitNonFinalClassesCheck.PACKAGE_SEPARATOR,
175+
outer,
176+
name
177+
)
178+
).asString();
179+
}
180+
return qualified;
181+
}
182+
183+
/**
184+
* Get class name from qualified name.
185+
* @param qualified Qualified class name
186+
* @return Class Name
187+
*/
188+
private static String getClassNameFromQualifiedName(
189+
final String qualified
190+
) {
191+
return qualified.substring(
192+
qualified.lastIndexOf(
193+
ProhibitNonFinalClassesCheck.PACKAGE_SEPARATOR
194+
) + 1
63195
);
64196
}
197+
198+
/**
199+
* Maintains information about class' ctors.
200+
*/
201+
private static final class ClassDesc {
202+
203+
/**
204+
* Qualified class name(with package).
205+
*/
206+
private final String qualified;
207+
208+
/**
209+
* Is class declared as final.
210+
*/
211+
private final boolean asfinal;
212+
213+
/**
214+
* Is class declared as abstract.
215+
*/
216+
private final boolean asabstract;
217+
218+
/**
219+
* Create a new ClassDesc instance.
220+
*
221+
* @param qualified Qualified class name(with package)
222+
* @param asfinal Indicates if the class declared as final
223+
* @param asabstract Indicates if the class declared as
224+
* abstract
225+
*/
226+
ClassDesc(final String qualified, final boolean asfinal,
227+
final boolean asabstract
228+
) {
229+
this.qualified = qualified;
230+
this.asfinal = asfinal;
231+
this.asabstract = asabstract;
232+
}
233+
234+
/**
235+
* Get qualified class name.
236+
* @return Qualified class name
237+
*/
238+
private String getQualified() {
239+
return this.qualified;
240+
}
241+
242+
/**
243+
* Is class declared as final.
244+
* @return True if class is declared as final
245+
*/
246+
private boolean isAsfinal() {
247+
return this.asfinal;
248+
}
249+
250+
/**
251+
* Is class declared as abstract.
252+
* @return True if class is declared as final
253+
*/
254+
private boolean isDeclaredAsAbstract() {
255+
return this.asabstract;
256+
}
257+
}
65258
}

‎qulice-checkstyle/src/test/java/com/qulice/checkstyle/ChecksTest.java

+1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ public final class ChecksTest {
8181
"JavadocEmptyLineCheck",
8282
"JavadocParameterOrderCheck",
8383
"JavadocTagsCheck",
84+
"ProhibitNonFinalClassesCheck",
8485
};
8586

8687
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0"?>
2+
<!DOCTYPE module PUBLIC
3+
"-//Puppy Crawl//DTD Check Configuration 1.2//EN"
4+
"http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
5+
<!--
6+
* This is not a real Checkstyle config. It is used
7+
* only as a text resource in integration.ChecksIT.
8+
-->
9+
<module name="Checker">
10+
<module name="TreeWalker">
11+
<module name="com.qulice.checkstyle.ProhibitNonFinalClassesCheck"/>
12+
</module>
13+
</module>

0 commit comments

Comments
 (0)
Please sign in to comment.