910e62b5创建于 1月15日历史提交
#!/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()