blob: 8ea80cc7c6c8072b02ce66594bd52cfc5cba63ec [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2025 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Tests for checkstyle.py."""
import inspect
import os
import re
import subprocess
import sys
import tempfile
import unittest
from concurrent.futures import ThreadPoolExecutor
_THIS_DIR = os.path.dirname(__file__)
_SRC_ROOT = os.path.normpath(os.path.join(_THIS_DIR, '..', '..', '..'))
_CHECKSTYLE_PATH = os.path.abspath(os.path.join(_THIS_DIR, 'checkstyle.py'))
def _run_checkstyle_in_worker(content):
with tempfile.NamedTemporaryFile(mode='w', suffix='.java') as f:
f.write(content)
f.flush()
result = subprocess.run([sys.executable, _CHECKSTYLE_PATH, f.name],
capture_output=True,
text=True,
cwd=_SRC_ROOT)
return result.stdout + result.stderr
# Decorator to associate the test .java with each method so it can be retrieved
# in order to run checkstyle concurrently.
def java(content):
def decorator(func):
setattr(func, '_java_content', content)
return func
return decorator
class CheckstyleTest(unittest.TestCase):
_executor = None
_results = {}
@classmethod
def setUpClass(cls):
cls._executor = ThreadPoolExecutor()
# Run checkstyle on all tests at once to not be too slow.
for name, method in inspect.getmembers(cls, inspect.isfunction):
if hasattr(method, '_java_content'):
content = method._java_content
cls._results[name] = cls._executor.submit(
_run_checkstyle_in_worker, content)
@classmethod
def tearDownClass(cls):
cls._executor.shutdown()
def _result(self):
return self._results[self._testMethodName].result()
def _check(self, value=None):
result = self._result()
if value:
self.assertIn(value, result)
else:
self.assertEqual('', result)
@java("""
class A {
public static void main(String args[]) {
}
}
""")
def test_ArrayTypeStyle(self):
self._check('Array brackets at illegal position')
@java("""
import java.util.HashSet;
class A {
void test() {
new HashSet<String>() {{
add("foo");
}};
}
}
""")
def test_AvoidDoubleBraceInitialization(self):
self._check('Avoid double brace initialization')
@java("""
class A {
void test(int a) {
switch (a) {
default:
break;
case 1:
break;
}
}
}
""")
def test_DefaultComesLast(self):
self._check('Default should be last label in the switch')
@java("""
class A {
void test() {
;
}
}
""")
def test_EmptyStatement(self):
self._check('Empty statement')
@java("""
class A {
void test() {
int a;
int b = a = 1;
}
}
""")
def test_InnerAssignment(self):
self._check('Inner assignments should be avoided')
@java("""
class A {
void test() {
int a; int b;
}
}
""")
def test_OneStatementPerLine(self):
self._check('Only one statement per line allowed')
@java("""
interface A {
public void test();
}
""")
def test_RedundantModifier(self):
self._check("Redundant 'public' modifier")
@java("""
class A {
void test(String s) {
if (s == "foo") {
}
}
}
""")
def test_StringLiteralEquality(self):
self._check('Literal Strings should be compared using equals()')
@java("""
import java.util.*;
class A {
}
""")
def test_AvoidStarImport(self):
self._check("Using the '.*' form of import should be avoided")
@java("""
class A {
void test() {
try {
} catch (Exception e) {
}
}
}
""")
def test_IllegalCatch(self):
self._check()
@java("""
class A {
static public void main(String[] args) {
}
}
""")
def test_ModifierOrder(self):
self._check('modifier out of order')
@java("""
import java.lang.String;
class A {
String s;
}
""")
def test_RedundantImport(self):
self._check('Redundant import from the java.lang package')
@java("""
import java.util.List;
class A {
}
""")
def test_UnusedImports(self):
self._check('Unused import')
@java("""
public class A {
}
""")
def test_JavadocType(self):
self._check()
@java("""
/** A class */
public class A {
public void test() {
}
}
""")
def test_JavadocMethod(self):
self._check()
@java("""
package MyPackage;
class A {
}
""")
def test_PackageName(self):
self._check("Name 'MyPackage' must match pattern")
@java("""
class A {
boolean test(boolean a) {
return a == true;
}
}
""")
def test_SimplifyBooleanExpression(self):
self._check('Expression can be simplified')
@java("""
class A {
boolean test(boolean a) {
if (a) {
return true;
} else {
return false;
}
}
}
""")
def test_SimplifyBooleanReturn(self):
self._check('Conditional logic can be removed')
@java("""
class a {
}
""")
def test_TypeName(self):
self._check("Name 'a' must match pattern")
@java("""
class A {
static final int my_const = 1;
}
""")
def test_ConstantName(self):
self._check('Static final field names must either be all caps')
@java("""
class A {
private int myMember;
}
""")
def test_MemberName(self):
self._check('Non-public, non-static field names start with m')
@java("""
class A {
static int myStatic;
}
""")
def test_StaticVariableName(self):
self._check('Static field names start with s')
@java("""
class A {
void MyMethod() {
}
}
""")
def test_MethodName(self):
self._check('Method names should start with a lower case letter')
@java("""
class A {
void test(int MyParam) {
}
}
""")
def test_ParameterName(self):
self._check("Name 'MyParam' must match pattern")
@java("""
class A {
void test() {
final int MY_VAR = 1; // bad
}
}
""")
def test_LocalFinalVariableName_bad(self):
self._check('Local variables should be camel-cased')
@java("""
class A {
void test() {
final int myVar2 = 1; // good
}
}
""")
def test_LocalFinalVariableName_good(self):
self._check()
@java("""
class A {
void test() {
int mMyVar = 1;
}
}
""")
def test_LocalVariableName_bad(self):
self._check('Local variables should be camel-cased')
@java("""
class A {
void test() {
int myVar = 1;
}
}
""")
def test_LocalVariableName_good(self):
self._check()
@java("""
class A {
void test(int a) {
switch (a) {
case 1:
a++;
case 2:
a++;
}
}
}
""")
def test_FallThrough(self):
self._check('Fall through from previous branch')
@java("""
class A {
protected void finalize() {
}
}
""")
def test_NoFinalizer(self):
self._check('finalize() is banned')
@java("""
class A {
void test(int a) {
if (a == 1)
a++;
}
}
""")
def test_NeedBraces(self):
self._check("'if' construct must use '{}'s")
@java("""
class A {
void test() {
StringBuffer sb = new StringBuffer();
}
}
""")
def test_StringBufferCheck(self):
self._check('Avoid StringBuffer; use StringBuilder instead')
@java("""
import android.app.AlertDialog;
class A {
void test() {
new AlertDialog.Builder(null);
}
}
""")
def test_AlertDialogCheck(self):
self._check('Avoid android.app.AlertDialog')
@java("""
import android.content.Context;
import android.preference.PreferenceManager;
class A {
void test(Context context) {
PreferenceManager.getDefaultSharedPreferences(context);
}
}
""")
def test_UseSharedPreferencesManagerFromChromeCheck(self):
self._check('Use SharedPreferencesManager instead to access app-wide')
@java("""
import android.content.Context;
import android.preference.PreferenceManager;
class A {
void test(Context context) {
PreferenceManager.getDefaultSharedPreferences(context);
}
}
""")
def test_DefaultSharedPreferencesCheck(self):
self._check('Use ContextUtils.getAppSharedPreferences() instead')
@java("""
import java.util.concurrent.ConcurrentHashMap;
class A {
void test() {
new ConcurrentHashMap<String, String>();
}
}
""")
def test_ConcurrentHashMapCheck(self):
self._check('ConcurrentHashMap has a bug on some Android versions')
@java("""
import android.os.StrictMode;
class A {
void test() {
try (StrictModeContext x = StrictModeContext.allowDiskWrites()) {
}
}
}
""")
def test_StrictModeContextIgnoredCheck(self):
self._check('Please name the StrictModeContext variable "ignored"')
@java("""
class A {
void test() {
System.exit(1);
}
}
""")
def test_SystemExitCheck(self):
self._check('Throw an exception instead of calling System#exit')
@java("""
import android.widget.Button;
class MyButton extends Button {
public MyButton(android.content.Context c) { super(c); }
}
""")
def test_AndroidButtonWidgetCheck(self):
self._check('Use org.chromium.ui.widget.ButtonCompat instead')
@java("""
import android.widget.TextView;
class A {
void test(TextView tv) {
tv.setTextColor(0);
tv.setTextSize(1);
}
}
""")
def test_SetTextColorAndSetTextSizeCheck(self):
self._check('please use #setTextAppearance')
@java("""
import androidx.test.core.app.ActivityScenario;
import android.app.Activity;
class A {
void test() {
ActivityScenario.launch(Activity.class);
}
}
""")
def test_ActivityScenarioLaunch(self):
self._check('Launching an ActivityScenario manually is error prone')
@java("""
import com.google.common.base.Optional;
class A {
Optional<String> s;
}
""")
def test_GuavaOptional(self):
self._check('Use java.util.Optional instead')
@java("""
import java.util.HashSet;
class A {
void test() {
new HashSet<String>() {};
}
}
""")
def test_CollectionSubclass(self):
self._check('Subclassing collections is uncommon')
@java("""
import android.view.accessibility.AccessibilityManager;
class A {
AccessibilityManager am;
}
""")
def test_AccessibilityManagerUsage(self):
self._check('Use org.chromium.ui.accessibility.AccessibilityState')
@java("""
class A {
int style = R.style.ThemeOverlay_BrowserUI_Fullscreen;
}
class R { public static class style {
public static final int ThemeOverlay_BrowserUI_Fullscreen = 1;
}}
""")
def test_FullscreenDialogs(self):
self._check('Fullscreen AlertDialogs must use FullscreenAlertDialog')
@java("""
class A {
// dummy
}
""")
def test_InclusiveLanguageCheck(self):
self._check('Please use inclusive language where possible')
@java("""
public class MyPreferenceFragment extends PreferenceFragment {}
""")
def test_SettingsFragmentCheck(self):
self._check('Top level settings fragment should be named with suffix')
@java("""
class A {
void test(/* comment */ int p) {}
}
""")
def test_ParamComments_1(self):
self._check(
'Parameter comments should use the ErrorProne-aware syntax')
@java("""
class A {
void test(int p, /* comment */ int b) {}
}
""")
def test_ParamComments_2(self):
self._check(
'Parameter comments should use the ErrorProne-aware syntax')
@java("""
class A {
void test(int p, int b /* comment */) {}
}
""")
def test_ParamComments_3(self):
self._check(
'Parameter comments should use the ErrorProne-aware syntax')
@java("""
class A {
void test(int p, int b /* comment */, int x) {}
}
""")
def test_ParamComments_4(self):
self._check(
'Parameter comments should use the ErrorProne-aware syntax')
@java("""
/**
* comment * */
class A {}
""")
def test_ClosingJavadocs(self):
self._check("Closing a javadoc only takes a single")
@java("""
class A {
interface Observer { void onAction(); }
void addObserver(Observer o) {}
void removeObserver(Observer o) {}
void test() {
removeObserver(this::onAction);
}
void onAction() {}
}
""")
def test_RemoveObserverMethodReference(self):
self._check('Each method reference creates a new instance')
@java("""
import org.mockito.MockitoAnnotations;
class A {
void test() {
MockitoAnnotations.initMocks(this);
}
}
""")
def test_MockitoInitMocks(self):
self._check('Prefer to use MockitoJUnit.Rule for consistency')
@java("""
import android.view.Window;
class A {
void test(Window w) {
WindowCompat.setDecorFitsSystemWindows(w, false);
}
}
""")
def test_SetDecorFitsSystemWindowsCheck(self):
self._check('setDecorFitsSystemWindows will change the window')
@java("""
import com.google.android.material.elevation.ElevationOverlayProvider;
class A {
ElevationOverlayProvider p;
}
""")
def test_ElevationOverlayProvider(self):
self._check('Elevation based surface color is being deprecated')
@java("""
import android.view.ContextMenu;
class A {
ContextMenu m;
}
""")
def test_AndroidContextMenu(self):
self._check("Android context menu has been proven not compatible")
@java("""
enum MyEnum { A, B }
""")
def test_IntDefsNotEnums(self):
self._check('Use an @IntDef / @StringDef / @LongDef')
@java("""
import androidx.annotation.VisibleForTesting;
class A {
@VisibleForTesting
void testForTesting() {}
}
""")
def test_VisibleForTestingForTesting(self):
self._check(
'There is no need to add @VisibleForTesting to test-only methods')
@java("""
import android.content.Context;
class A {
void test(Context c) {
c.getResources().getString(0);
}
}
""")
def test_ContextGetString_bad(self):
self._check('Use Context.getString()')
@java("""
import android.content.Context;
class A {
void test(Context c) {
c.getString(0);
thing.getContext().getString(0);
}
}
""")
def test_ContextGetString_good(self):
self._check()
@java("""
import org.robolectric.annotation.Config;
@Config(minSdk = 21)
class A {}
""")
def test_RobolecticMinSdk(self):
self._check(
'@Config(minSdk=...) parameterizes tests across every SDK level')
@java("""
import androidx.annotation.Nullable;
import org.chromium.build.annotations.NullMarked;
@NullMarked class MarkImportsAsUsed<T extends @Nullable Object> {}
""")
def test_WrongNullable_AndroidX(self):
self._check('Use org.chromium.build.annotations.Nullable instead of ')
@java("""
import javax.annotation.Nullable;
import org.chromium.build.annotations.NullMarked;
@NullMarked class MarkImportsAsUsed<T extends @Nullable Object> {}
""")
def test_WrongNullable_JavaX(self):
self._check('Use org.chromium.build.annotations.Nullable instead of ')
@java("""
import androidx.annotation.Nullable;
class MarkImportsAsUsed<T extends @Nullable Object> {}
""")
def test_WrongNullable_NonNullMarked(self):
self._check()
@java("""
class MarkImportsAsUsed<T extends @NonNull Object> {}
""")
def test_NonNull(self):
self._check('Values are @NonNull by default. Use @NonNull')
if __name__ == '__main__':
# Only Chromium Linux checkouts have a Java runtime.
if sys.platform == 'linux':
unittest.main()