Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Created a new detector for rule MET04-J #1994

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Expand Up @@ -9,6 +9,9 @@ Currently the versioning policy of this project follows [Semantic Versioning v2.
### Fixed
- Fix FP `SING_SINGLETON_GETTER_NOT_SYNCHRONIZED` with eager instances ([#2932]https://github.com/spotbugs/spotbugs/issues/2932)

### Added
- New detector `FindIncreasedAccessibilityOfMethods` for new bug type `IAOM_DO_NOT_INCREASE_METHOD_ACCESSIBILITY`. This detector reports a bug if a class increases the accessibility of overridden or hidden methods. (See [SEI CERT rule MET04-J](https://wiki.sei.cmu.edu/confluence/display/java/MET04-J.+Do+not+increase+the+accessibility+of+overridden+or+hidden+methods))

## 4.8.4 - 2024-04-07
### Fixed
- Fix FP in SE_PREVENT_EXT_OBJ_OVERWRITE when the if statement checking for null value, checking multiple variables or the method exiting in the if branch with an exception. ([#2750](https://github.com/spotbugs/spotbugs/issues/2750))
Expand Down
@@ -0,0 +1,120 @@
package edu.umd.cs.findbugs.detect;

import edu.umd.cs.findbugs.AbstractIntegrationTest;
import edu.umd.cs.findbugs.test.matcher.BugInstanceMatcher;
import edu.umd.cs.findbugs.test.matcher.BugInstanceMatcherBuilder;
import org.junit.jupiter.api.Test;

import static edu.umd.cs.findbugs.test.CountMatcher.containsExactly;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.not;

class FindIncreasedAccessibilityOfMethodsTest extends AbstractIntegrationTest {
@Test
void findIAOMBugInClass_IncreasedAccessibilityOfMethods_SubClassFromSamePackage() {
performAnalysis(
"increasedAccessibilityOfMethods/SuperClassOfSuperClass.class",
"increasedAccessibilityOfMethods/SuperClass.class",
"increasedAccessibilityOfMethods/SubClassFromSamePackage.class");

assertContainsCorrectNumberOfIAOMBugs(6);

assertContainsIAOMBug("SubClassFromSamePackage", "protectedMethodToPublic");
assertContainsIAOMBug("SubClassFromSamePackage", "packagePrivateMethodToPublic");
assertContainsIAOMBug("SubClassFromSamePackage", "packagePrivateMethodToProtected");
assertContainsIAOMBug("SubClassFromSamePackage", "superProtectedMethodToPublic");
assertContainsIAOMBug("SubClassFromSamePackage", "superPackagePrivateMethodToPublic");
assertContainsIAOMBug("SubClassFromSamePackage", "superPackagePrivateMethodToProtected");
}

@Test
void findIAOMBugInClass_IncreasedAccessibilityOfMethods_SubClassFromAnotherPackage() {
performAnalysis(
"increasedAccessibilityOfMethods/SuperClassOfSuperClass.class",
"increasedAccessibilityOfMethods/SuperClass.class",
"increasedAccessibilityOfMethods/anotherPackage/SubClassFromAnotherPackage.class");

assertContainsCorrectNumberOfIAOMBugs(1);

assertContainsIAOMBug("SubClassFromAnotherPackage", "protectedMethodToPublic");
}

@Test
void findNoIAOMBugInClass_IncreasedAccessibilityOfMethods_CorrectSubClass() {
performAnalysis(
"increasedAccessibilityOfMethods/SuperClassOfSuperClass.class",
"increasedAccessibilityOfMethods/anotherPackage/CorrectSubClass.class");

assertContainsCorrectNumberOfIAOMBugs(0);
}

@Test
void findNoIAOMBugInClass_IncreasedAccessibilityOfMethods_CloneableImplementation() {
performAnalysis(
"increasedAccessibilityOfMethods/CloneableImplementation.class");

assertContainsCorrectNumberOfIAOMBugs(0);
}

@Test
void findNoIAOMBugInClass_IncreasedAccessibilityOfMethods_InterfaceImplementation() {
performAnalysis(
"increasedAccessibilityOfMethods/Interface.class",
"increasedAccessibilityOfMethods/InterfaceImplementation.class");
sajtizsolt marked this conversation as resolved.
Show resolved Hide resolved

assertContainsCorrectNumberOfIAOMBugs(0);
JuditKnoll marked this conversation as resolved.
Show resolved Hide resolved
}

@Test
void findNoIAOMBugInClass_IncreasedAccessibilityOfMethods_GenericImplementation() {
performAnalysis(
"increasedAccessibilityOfMethods/GenericSuperClass.class",
"increasedAccessibilityOfMethods/GenericSubClass.class");

assertContainsCorrectNumberOfIAOMBugs(3);

assertContainsIAOMBug("GenericSubClass", "protectedMethodToPublicWithGenericParameter");
assertContainsIAOMBug("GenericSubClass", "packagePrivateMethodToPublicWithGenericParameter");
assertContainsIAOMBug("GenericSubClass", "packagePrivateMethodToProtectedWithGenericParameter");
}

@Test
void findNoIAOMBugInClass_IncreasedAccessibilityOfMethods_SubClassWithRepeatedOverride() {
performAnalysis(
"increasedAccessibilityOfMethods/SuperClassOfSuperClass.class",
"increasedAccessibilityOfMethods/SuperClassWithOverride.class",
"increasedAccessibilityOfMethods/SubClassWithRepeatedOverride.class");

assertContainsCorrectNumberOfIAOMBugs(3);

assertContainsIAOMBug("SuperClassWithOverride", "superProtectedMethodToPublicOverriddenInSuperClass");
assertContainsIAOMBug("SuperClassWithOverride", "superPackagePrivateMethodToProtectedOverriddenInSuperClass");
assertContainsIAOMBug("SubClassWithRepeatedOverride", "superPackagePrivateMethodToProtectedOverriddenInSuperClass");
assertNotContainsIAOMBug("SubClassWithRepeatedOverride", "superProtectedMethodToPublicOverriddenInSuperClass");
}

private void assertContainsCorrectNumberOfIAOMBugs(int numberOfBugs) {
BugInstanceMatcher bugTypeMatcher = new BugInstanceMatcherBuilder()
.bugType("IAOM_DO_NOT_INCREASE_METHOD_ACCESSIBILITY").build();
assertThat(getBugCollection(), containsExactly(numberOfBugs, bugTypeMatcher));
}

private void assertContainsIAOMBug(String className, String methodName) {
BugInstanceMatcher bugInstanceMatcher = new BugInstanceMatcherBuilder()
.bugType("IAOM_DO_NOT_INCREASE_METHOD_ACCESSIBILITY")
.inClass(className)
.inMethod(methodName)
.build();
assertThat(getBugCollection(), hasItem(bugInstanceMatcher));
}

private void assertNotContainsIAOMBug(String className, String methodName) {
BugInstanceMatcher bugInstanceMatcher = new BugInstanceMatcherBuilder()
.bugType("IAOM_DO_NOT_INCREASE_METHOD_ACCESSIBILITY")
.inClass(className)
.inMethod(methodName)
.build();
assertThat(getBugCollection(), not(hasItem(bugInstanceMatcher)));
}
}
6 changes: 5 additions & 1 deletion spotbugs/etc/findbugs.xml
Expand Up @@ -686,7 +686,10 @@
reports="ENV_USE_PROPERTY_INSTEAD_OF_ENV"/>
<Detector class="edu.umd.cs.findbugs.detect.MultipleInstantiationsOfSingletons" speed="fast"
reports="SING_SINGLETON_IMPLEMENTS_CLONEABLE,SING_SINGLETON_INDIRECTLY_IMPLEMENTS_CLONEABLE,SING_SINGLETON_IMPLEMENTS_CLONE_METHOD,SING_SINGLETON_HAS_NONPRIVATE_CONSTRUCTOR,SING_SINGLETON_IMPLEMENTS_SERIALIZABLE,SING_SINGLETON_GETTER_NOT_SYNCHRONIZED" />
<!-- Bug Categories -->
<Detector class="edu.umd.cs.findbugs.detect.FindIncreasedAccessibilityOfMethods" speed="fast"
reports="IAOM_DO_NOT_INCREASE_METHOD_ACCESSIBILITY"/>

<!-- Bug Categories -->
<BugCategory category="NOISE" hidden="true"/>

<!-- Bug Codes -->
Expand Down Expand Up @@ -1331,4 +1334,5 @@
<BugPattern abbrev="SING" type="SING_SINGLETON_IMPLEMENTS_SERIALIZABLE" category="CORRECTNESS" />
<BugPattern abbrev="SING" type="SING_SINGLETON_GETTER_NOT_SYNCHRONIZED" category="CORRECTNESS" />
<BugPattern abbrev="ENV" type="ENV_USE_PROPERTY_INSTEAD_OF_ENV" category="BAD_PRACTICE" />
<BugPattern abbrev="IAOM" type="IAOM_DO_NOT_INCREASE_METHOD_ACCESSIBILITY" category="CORRECTNESS" />
</FindbugsPlugin>
26 changes: 26 additions & 0 deletions spotbugs/etc/messages.xml
Expand Up @@ -1839,6 +1839,14 @@ factory pattern to create these objects.</p>
]]>
</Details>
</Detector>
<Detector class="edu.umd.cs.findbugs.detect.FindIncreasedAccessibilityOfMethods">
<Details>
<![CDATA[
<p>Finds classes where the accessibility of the overridden or hidden method was increased.</p>
]]>
</Details>
</Detector>

<!--
**********************************************************************
BugPatterns
Expand Down Expand Up @@ -9269,11 +9277,28 @@ Using floating-point variables should not be used as loop counters, as they are
</p>]]>
</Details>
</BugPattern>
<BugPattern type="IAOM_DO_NOT_INCREASE_METHOD_ACCESSIBILITY">
<ShortDescription>Accessibility of the overridden or hidden method was increased.</ShortDescription>
<LongDescription>Accessibility of the overridden or hidden method {1} was increased from {2} to {3}.</LongDescription>
sajtizsolt marked this conversation as resolved.
Show resolved Hide resolved
<Details>
<![CDATA[<p>
Increasing the accessibility of overridden or hidden methods permits a malicious subclass to offer wider
sajtizsolt marked this conversation as resolved.
Show resolved Hide resolved
access to the restricted method than was originally intended. Consequently, programs must override methods
only when necessary and must declare methods final whenever possible to prevent malicious subclassing. When
methods cannot be declared final, programs must refrain from increasing the accessibility of overridden methods.
</p>
<p>
See SEI CERT rule <a href="https://wiki.sei.cmu.edu/confluence/display/java/MET04-J.+Do+not+increase+the+accessibility+of+overridden+or+hidden+methods">MET04-J. Do not increase the accessibility of overridden or hidden methods</a>.
</p>]]>
</Details>
</BugPattern>

<!--
**********************************************************************
BugCodes
**********************************************************************
-->

<BugCode abbrev="FS">Format string problem</BugCode>
<BugCode abbrev="SKIPPED">Analysis skipped</BugCode>
<BugCode abbrev="IL">Infinite Loop</BugCode>
Expand Down Expand Up @@ -9430,4 +9455,5 @@ Using floating-point variables should not be used as loop counters, as they are
<BugCode abbrev="AA">Misuse of assertions for checking arguments of public methods</BugCode>
<BugCode abbrev="PI">Do not reuse public identifiers from Java Standard Library</BugCode>
<BugCode abbrev="ENV">Environment variable is used instead of the corresponding Java property</BugCode>
<BugCode abbrev="IAOM">Do not increase the accessibility of overridden or hidden methods</BugCode>
</MessageCollection>
@@ -0,0 +1,160 @@
/*
* SpotBugs - Find bugs in Java programs
*
* This library 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 2.1 of the License, or (at your option) any later version.
*
* This library 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 library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/

package edu.umd.cs.findbugs.detect;

import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.Detector;
import edu.umd.cs.findbugs.ba.AnalysisContext;
import edu.umd.cs.findbugs.ba.ClassContext;
import org.apache.bcel.Const;
import org.apache.bcel.Repository;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

public class FindIncreasedAccessibilityOfMethods implements Detector {
private static final String BUG_TYPE = "IAOM_DO_NOT_INCREASE_METHOD_ACCESSIBILITY";
private static final String PACKAGE_PRIVATE = "package private";
private static final String PROTECTED = "protected";
private static final String PUBLIC = "public";

private final BugReporter bugReporter;

public FindIncreasedAccessibilityOfMethods(BugReporter bugReporter) {
this.bugReporter = bugReporter;
}

@Override
public void visitClassContext(ClassContext classContext) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

In the current implementation there is a hit type, which may cause problems for the users, so we may need filter it out: we have three classes: SuperClass, Class, and SubClass, SuperClass is parent of Class, which is parent of SubClass. We have a method, which is declared in SuperClass as protected, overriden in both Class and SubClass as public. There will be a hit in Class, which is great, but there will be also a hit in SubClass, about which I'm not so sure. If the user have no write access to SuperClass and Class, then can't really do anything about the problem.

Copy link
Author

Choose a reason for hiding this comment

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

I'm not sure, if this can occur in real life. Do SpotBugs analyze classes to which the user has no write access? For example in dependencies? I see your point, can you maybe show me an example of it where I can reproduce the hit?

Copy link
Collaborator

Choose a reason for hiding this comment

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

If the user provided the necessary compiled files for the analysis, even if doesn't have write access to them, cause e.g. they are inside a jar file. Checked every hit of the tests on the bigger projects, but didn't find any test like the one, I mentioned. (Otherwise I would have referred it.) You can create a test by simply creating a class, which descends from one of the hits and overrides the appropriate function, or reusing your already existing test classes, using SuperClassOfSuperClass as SuperClass in my example, SuperClass as Class, and SubClassFromSamePackage as SubClass. If you simply add an override of superPackagePrivateMethodToPublic() to SuperClass, then you have the test:

@Override
public String superPackagePrivateMethodToPublic() {
    return "SuperClass.superPackagePrivateMethodToPublic";
}

IMO in this case, the error should be only reported in SuperClass, and not in SubClassFromSamePackage.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@sajtizsolt Can you please create a testcase like this and modify the code to not to bury our users in hits they can't do anything about?

Copy link
Author

Choose a reason for hiding this comment

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

I will, I'm just a littly busy nowadays. I will tag you if it happens!

Copy link
Author

Choose a reason for hiding this comment

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

@JuditKnoll Now I thought about this problem again. To achieve the desired behaviour, we should be able to filter out potential bugs based on the bugs already found while we analyzed the super classes of the current class. I don't know, if there is some logic implemented in SpotBugs which enables us to query this list of bugs based on a class, but if there is not, then we should analyze every single super class again. I don't know, I feel that this would increase the complexity of the detector by a huge scale. Do you feel, that the added value of this solution complies with the amount of extra computational demand of it?

Copy link
Author

Choose a reason for hiding this comment

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

Anyway, I've added the requested changes in another commit - take a look at them. Not terribly slow if there are few super classes.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, I think having a more precise detector is worth the increased complexity. Usually there is less than 10 super classes, so it's not as much of an overhead.

Copy link
Collaborator

Choose a reason for hiding this comment

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

However, IMO there is a solution which only needs to check every super class only once, if we take advantage of the order of visiting, and maintain a collection of the correct methods of the examined class. If we phrase the problem like this, that may help: an IAOM bug should be reported, when there is an increase of accessibility of a method from the closest parent declaring/overriding the given method.
When the bug is reported, there is an accessibility increase, then we can remove the current buggy method from the methods of the examined class (subClass) collection. This way, we don't need to check at every parent whether the same bug got reported or not, also, the number of methods to check against decreases.
Removing a method won't cause a problem, since a method can only have one IAOM bug in one class, and the closer parent will be visited first, since JavaClass.getSuperClasses() contains the list of super classes in ascending order.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Answering your question about filtering out bugs based on other bugs, there is a solution using BugAccumulator, but it's a bit different to what you need. As far as I know, that one only works inside one class, not across classes.

JavaClass subClass = classContext.getJavaClass();
for (IAOMBug bug : findIAOMBugs(subClass)) {
bugReporter.reportBug(new BugInstance(this, BUG_TYPE, LOW_PRIORITY)
.addClassAndMethod(bug.clazz, bug.method)
.addString(bug.oldAccessibility)
.addString(bug.newAccessibility));
}
}

@Override
public void report() {
}

private List<IAOMBug> findIAOMBugs(JavaClass subClass) {
List<IAOMBug> iaomBugs = new ArrayList<>();
JavaClass[] superClasses;
try {
superClasses = subClass.getSuperClasses();
} catch (ClassNotFoundException e) {
AnalysisContext.reportMissingClass(e);
return iaomBugs;
}

List<Method> subClassMethods = getMethodsExceptConstructors(subClass);
List<IAOMBug> superClassBugs = new ArrayList<>();
for (JavaClass superClass : superClasses) {
superClassBugs.addAll(findIAOMBugs(superClass));
Copy link
Collaborator

Choose a reason for hiding this comment

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

It does work, but it could be a bit more efficient, see my comment at the visitClassContext() method.

for (Method superClassMethod : getMethodsExceptConstructors(superClass)) {
if (isAccessibleFromSubClass(superClassMethod, subClass, superClass)) {
for (Method subClassMethod : subClassMethods) {
if (isOverridable(superClassMethod, subClassMethod) && !isCloneMethodFromCloneable(subClass, superClassMethod)) {
if (superClassMethod.isProtected() && subClassMethod.isPublic()) {
iaomBugs.add(new IAOMBug(subClass, subClassMethod, PROTECTED, PUBLIC));
}
if (isPackagePrivate(superClassMethod)) {
if (subClassMethod.isPublic()) {
iaomBugs.add(new IAOMBug(subClass, subClassMethod, PACKAGE_PRIVATE, PUBLIC));
} else if (subClassMethod.isProtected()) {
iaomBugs.add(new IAOMBug(subClass, subClassMethod, PACKAGE_PRIVATE, PROTECTED));
}
}
}
}
}
}
}

return iaomBugs.stream().filter(bug -> !containsBug(superClassBugs, bug)).collect(Collectors.toList());
}

private List<Method> getMethodsExceptConstructors(JavaClass javaClass) {
ArrayList<Method> methods = new ArrayList<>();
for (Method method : javaClass.getMethods()) {
if (!Const.CONSTRUCTOR_NAME.equals(method.getName())) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think you can also filter out the static initializer.

methods.add(method);
}
}
return methods;
}

private boolean isAccessibleFromSubClass(Method superClassMethod, JavaClass subClass, JavaClass superClass) {
return superClassMethod.isProtected() ||
(subClass.getPackageName().equals(superClass.getPackageName()) && isPackagePrivate(superClassMethod));
}

private boolean isOverridable(Method original, Method overrider) {
return Objects.equals(original, overrider) && !original.isFinal();
}

private boolean isPackagePrivate(Method method) {
return !(method.isPublic() || method.isProtected() || method.isPrivate());
}

private boolean isCloneMethodFromCloneable(JavaClass javaClass, Method method) {
try {
return javaClass.implementationOf(Repository.lookupClass(Cloneable.class)) && "clone".equals(method.getName());
} catch (ClassNotFoundException e) {
AnalysisContext.reportMissingClass(e);
return false;
KengoTODA marked this conversation as resolved.
Show resolved Hide resolved
}
}

private boolean containsBug(List<IAOMBug> bugs, IAOMBug bug) {
for (IAOMBug other : bugs) {
if (bug.isSameMethodAndAccessibility(other)) {
return true;
}
}
return false;
}

private static class IAOMBug {
final JavaClass clazz;
final Method method;
final String oldAccessibility;
final String newAccessibility;

IAOMBug(JavaClass clazz, Method method, String oldAccessibility, String newAccessibility) {
this.clazz = clazz;
this.method = method;
this.oldAccessibility = oldAccessibility;
this.newAccessibility = newAccessibility;
}

boolean isSameMethodAndAccessibility(IAOMBug other) {
return Objects.equals(method.getSignature(), other.method.getSignature())
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are you sure, that checking the signatures is enough to check two methods are the same?

Copy link
Collaborator

Choose a reason for hiding this comment

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

The Method class itself has an equals, which checks both the name of the method and the signature. Is there any reason, why you don't use that equals?

&& Objects.equals(oldAccessibility, other.oldAccessibility)
&& Objects.equals(newAccessibility, other.newAccessibility);
}
}
}
@@ -0,0 +1,12 @@
package increasedAccessibilityOfMethods;

public class CloneableImplementation implements Cloneable {
@Override
public CloneableImplementation clone() {
try {
return (CloneableImplementation) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}