├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── wp │ │ └── codingconstraint │ │ ├── ValidActivityAndFragment.kt │ │ └── ValidNameDemo.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ └── valid_layout.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── lint-coding-constraint-arr ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── wp │ │ └── lint_coding_constraint_arr │ │ └── ExampleInstrumentedTest.kt │ ├── main │ └── AndroidManifest.xml │ └── test │ └── java │ └── com │ └── wp │ └── lint_coding_constraint_arr │ └── ExampleUnitTest.kt ├── lint-coding-constraint-jar ├── .gitignore ├── build.gradle └── src │ └── main │ └── java │ ├── com │ └── wp │ │ └── lint_coding_constraint_jar │ │ ├── IssueRegister.kt │ │ ├── detector │ │ ├── ActivityAndFragmentDetector.kt │ │ ├── NameDetector.kt │ │ └── element │ │ │ ├── ElementIdDetector.kt │ │ │ ├── InvisibleElementDetector.kt │ │ │ └── NoValueCanNotPreviewElementDetector.kt │ │ └── util │ │ ├── EnglishDictionary.kt │ │ └── StringExt.kt │ └── data │ └── words.txt ├── pic ├── 1.png ├── 2.png ├── 3.png └── 4.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | /build 7 | /captures 8 | gradlew 9 | gradlew.bat 10 | 11 | #built application files 12 | *.apk 13 | *.ap_ 14 | 15 | 16 | # files for the dex VM 17 | *.dex 18 | 19 | 20 | # Java class files 21 | *.class 22 | 23 | 24 | # generated files 25 | bin/ 26 | gen/ 27 | 28 | 29 | # Local configuration file (sdk path, etc) 30 | local.properties 31 | 32 | 33 | # Windows thumbnail db 34 | Thumbs.db 35 | 36 | 37 | # OSX files 38 | .DS_Store 39 | 40 | 41 | # Eclipse project files 42 | .classpath 43 | .project 44 | 45 | 46 | # Android Studio 47 | .idea 48 | #.idea/workspace.xml - remove # and delete .idea if it better suit your needs. 49 | build/ 50 | app/build/ 51 | 52 | # Signing files 53 | .signing/ 54 | 55 | 56 | # User-specific configurations 57 | .idea/libraries/ 58 | .idea/workspace.xml 59 | .idea/tasks.xml 60 | .idea/.name 61 | .idea/compiler.xml 62 | .idea/copyright/profiles_settings.xml 63 | .idea/encodings.xml 64 | .idea/misc.xml 65 | .idea/modules.xml 66 | .idea/scopes/scope_settings.xml 67 | .idea/vcs.xml 68 | *.json 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 wp529 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### CodingConstraint 2 | 3 | 是否还在为项目命名风格不一致而头痛?是否还在为预览布局文件一片空白而捶胸顿足?Android编码规范约束工具,使用**自定义lint规则**对代码进行编码规范扫描,对不符合编码规范的代码进行**error告警**,强制统一项目组编码规范,解决你的烦恼。 4 | 5 | ![img](https://raw.githubusercontent.com/wp529/CodingConstraint/master/pic/1.png) 6 | 7 | ![img](https://raw.githubusercontent.com/wp529/CodingConstraint/master/pic/2.png) 8 | 9 | ![img](https://raw.githubusercontent.com/wp529/CodingConstraint/master/pic/3.png) 10 | 11 | ![img](https://raw.githubusercontent.com/wp529/CodingConstraint/master/pic/4.png) 12 | 13 | ##### 使用方式 14 | 15 | 将lint-coding-constraint-arr和lint-coding-constraint-jar导入项目中,然后在需要进行编码规范扫描的module添加implementation project(path: ':lint-coding-constraint-arr')然后rebuild,稍等片刻即可生效 16 | 17 | ##### 包含的自定义规则有 18 | 19 | 1. 命名相关 20 | * 所有的单词命名必须使用正确的英文单词 21 | * 类命名必须使用大驼峰 22 | * 成员变量命名必须使用小驼峰 23 | * 局部变量命名必须使用小驼峰 24 | * 方法命名必须使用小驼峰 25 | * 静态变量、常量命名必须使用全大写+下划线 26 | * 控件id命名必须使用小驼峰且必须以控件类型开头。eg:TextView的id必须为android:id="@+id/tvTest" 27 | 28 | 2. Activity和Fargment规范 29 | 30 | * Activity类必须包含静态startActivity()方法 31 | * Fragment类必须包含静态newInstance()方法 32 | 33 | 3. 布局编写规范 34 | 35 | * 控件必须使用相对应的tools属性,限定这条规则是不使用tools预览效果很差。eg:TextView必须要有tools:text="XXX"或者android:text="XXX"属性,具体详见下表 36 | 37 | | 控件 | 检查的属性 | 38 | | :----------- | -----------------------------------------------------------: | 39 | | 所有控件 | 若使用了android:visibility="gone"或者android:visibility="invisible"那么必须添加tools:visibility="visible" | 40 | | TextView | 必须有android:text="XXX"或者tools:text="XXX" | 41 | | ImageView | 必须有android:src="XXX"或者tools:src="XXX" | 42 | | EditText | 必须有android:text="XXX"或者tools:text="XXX"或者android:hint="XXX"或者android:hint="XXX" | 43 | | RecyclerView | 必须有tools:itemCount="XXX"和tools:listitem="XXX" | 44 | | ProgressBar | 必须有android:progress="XXX"或者tools:progress="XXX" | 45 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 29 7 | buildToolsVersion "29.0.3" 8 | 9 | defaultConfig { 10 | applicationId "com.wp.codingconstraint" 11 | minSdkVersion 21 12 | targetSdkVersion 29 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | } 26 | 27 | dependencies { 28 | implementation fileTree(dir: "libs", include: ["*.jar"]) 29 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 30 | implementation 'androidx.core:core-ktx:1.3.1' 31 | implementation 'androidx.appcompat:appcompat:1.2.0' 32 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 33 | implementation project(path: ':lint-coding-constraint-arr') 34 | testImplementation 'junit:junit:4.12' 35 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 36 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 37 | 38 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/wp/codingconstraint/ValidActivityAndFragment.kt: -------------------------------------------------------------------------------- 1 | package com.wp.codingconstraint 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.appcompat.app.AppCompatActivity 7 | 8 | //正确的Activity 9 | class ValidActivity : AppCompatActivity() { 10 | companion object { 11 | fun startActivity(context: Context) { 12 | context.startActivity(Intent(context, ValidActivity::class.java)) 13 | } 14 | } 15 | 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | } 19 | } 20 | 21 | //不符合规范的Activity 22 | class ErrorActivity : AppCompatActivity() { 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | super.onCreate(savedInstanceState) 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wp/codingconstraint/ValidNameDemo.kt: -------------------------------------------------------------------------------- 1 | package com.wp.codingconstraint 2 | 3 | //正确类名 4 | class CorrectClass { 5 | } 6 | 7 | //正确类名 8 | class Correct { 9 | } 10 | 11 | //类名没用大驼峰 12 | class errorClass { 13 | } 14 | 15 | //类名没用大驼峰 16 | class error_class { 17 | } 18 | 19 | //类名没用大驼峰 20 | class Error_class { 21 | } 22 | 23 | //类名拼写错误 24 | class ErrorClbss { 25 | } 26 | 27 | //成员变量命名验证 28 | class FieldValid { 29 | //正确变量名 30 | private val errorField = "" 31 | 32 | //正确变量名 33 | private val field = "" 34 | 35 | //变量名没使用小驼峰 36 | private val ErrorField = "" 37 | 38 | //变量名没使用小驼峰 39 | private val Error_Field = "" 40 | 41 | //变量名没使用小驼峰 42 | private val Error_field = "" 43 | 44 | //变量名拼写错误 45 | private val errorFiald = "" 46 | } 47 | 48 | //局部变量命名验证 49 | class LocalVariableValid { 50 | private fun testFunction() { 51 | //正确变量名 52 | val errorField = "" 53 | //正确变量名 54 | val field = "" 55 | //变量名没使用小驼峰 56 | val ErrorField = "" 57 | //变量名没使用小驼峰 58 | val Error_Field = "" 59 | //变量名没使用小驼峰 60 | val Error_field = "" 61 | //变量名拼写错误 62 | val errorFiald = "" 63 | } 64 | } 65 | 66 | //方法命名验证 67 | class MethodNameValid { 68 | //正确方法名 69 | private fun testFunction() { 70 | } 71 | 72 | //正确方法名 73 | private fun function() { 74 | } 75 | 76 | //方法名没使用小驼峰 77 | private fun ErrorFunction() { 78 | } 79 | 80 | //方法名没使用小驼峰 81 | private fun Error_Function() { 82 | } 83 | 84 | //方法名没使用小驼峰 85 | private fun Error_function() { 86 | } 87 | 88 | //方法名拼写错误 89 | private fun errorFanction() { 90 | } 91 | } 92 | 93 | class StaticFieldValid { 94 | companion object { 95 | //正确静态变量 96 | val TEST_FIELD = "" 97 | 98 | //正确静态变量 99 | val TEST = "" 100 | 101 | //静态变量不符合规范 102 | val test_FIELD = "" 103 | 104 | //静态变量拼写不正确 105 | val TEST_FIALD = "" 106 | } 107 | } 108 | 109 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/valid_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wp529/CodingConstraint/2f5bc86452cddf21b50e8ef479d9bca86b7fc83f/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wp529/CodingConstraint/2f5bc86452cddf21b50e8ef479d9bca86b7fc83f/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wp529/CodingConstraint/2f5bc86452cddf21b50e8ef479d9bca86b7fc83f/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wp529/CodingConstraint/2f5bc86452cddf21b50e8ef479d9bca86b7fc83f/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wp529/CodingConstraint/2f5bc86452cddf21b50e8ef479d9bca86b7fc83f/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wp529/CodingConstraint/2f5bc86452cddf21b50e8ef479d9bca86b7fc83f/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wp529/CodingConstraint/2f5bc86452cddf21b50e8ef479d9bca86b7fc83f/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wp529/CodingConstraint/2f5bc86452cddf21b50e8ef479d9bca86b7fc83f/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wp529/CodingConstraint/2f5bc86452cddf21b50e8ef479d9bca86b7fc83f/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wp529/CodingConstraint/2f5bc86452cddf21b50e8ef479d9bca86b7fc83f/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6200EE 4 | #3700B3 5 | #03DAC5 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | CodingConstraint 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext.kotlin_version = "1.3.72" 4 | repositories { 5 | google() 6 | jcenter() 7 | } 8 | dependencies { 9 | classpath "com.android.tools.build:gradle:4.0.1" 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | 12 | // NOTE: Do not place your application dependencies here; they belong 13 | // in the individual module build.gradle files 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | jcenter() 21 | } 22 | } 23 | 24 | task clean(type: Delete) { 25 | delete rootProject.buildDir 26 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wp529/CodingConstraint/2f5bc86452cddf21b50e8ef479d9bca86b7fc83f/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Aug 19 09:32:54 CST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip 7 | -------------------------------------------------------------------------------- /lint-coding-constraint-arr/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /lint-coding-constraint-arr/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 29 7 | buildToolsVersion "29.0.3" 8 | 9 | defaultConfig { 10 | minSdkVersion 21 11 | targetSdkVersion 29 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | consumerProguardFiles "consumer-rules.pro" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | lintOptions { 26 | checkReleaseBuilds false 27 | abortOnError true 28 | } 29 | } 30 | 31 | configurations { 32 | lintJarImport 33 | } 34 | 35 | dependencies { 36 | implementation fileTree(dir: "libs", include: ["*.jar"]) 37 | testImplementation 'junit:junit:4.12' 38 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 39 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 40 | lintJarImport project(path: ':lint-coding-constraint-jar', configuration: 'lintJarOutput') 41 | } 42 | 43 | task copyLintJar(type: Copy) { 44 | from (configurations.lintJarImport) { 45 | rename { 46 | String fileName -> 47 | 'lint.jar' 48 | } 49 | } 50 | into 'build/intermediates/lint/' 51 | } 52 | project.afterEvaluate { 53 | def compileLintTask = project.tasks.find{ it.name == 'preBuild'} 54 | compileLintTask.dependsOn(copyLintJar) 55 | } 56 | 57 | -------------------------------------------------------------------------------- /lint-coding-constraint-arr/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wp529/CodingConstraint/2f5bc86452cddf21b50e8ef479d9bca86b7fc83f/lint-coding-constraint-arr/consumer-rules.pro -------------------------------------------------------------------------------- /lint-coding-constraint-arr/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /lint-coding-constraint-arr/src/androidTest/java/com/wp/lint_coding_constraint_arr/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.wp.lint_coding_constraint_arr 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.wp.lint_coding_constraint_arr.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /lint-coding-constraint-arr/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lint-coding-constraint-arr/src/test/java/com/wp/lint_coding_constraint_arr/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.wp.lint_coding_constraint_arr 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /lint-coding-constraint-jar/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /lint-coding-constraint-jar/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | apply plugin: 'kotlin' 3 | jar { 4 | manifest{ 5 | //main 函数地址类 6 | attributes("Lint-Registry-v2": "com.wp.lint_coding_constraint_jar.IssueRegister") 7 | } 8 | from{ 9 | ["./src/main/java/data/words.txt"] 10 | } 11 | into('assets') { 12 | from 'assets' 13 | } 14 | } 15 | 16 | configurations { 17 | lintJarOutput 18 | } 19 | 20 | dependencies { 21 | lintJarOutput files(jar) 22 | compileOnly 'com.android.tools.lint:lint-api:27.0.1' 23 | compileOnly 'com.android.tools.lint:lint-checks:27.0.1' 24 | implementation fileTree(dir: 'libs', include: ['*.jar']) 25 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 26 | } -------------------------------------------------------------------------------- /lint-coding-constraint-jar/src/main/java/com/wp/lint_coding_constraint_jar/IssueRegister.kt: -------------------------------------------------------------------------------- 1 | package com.wp.lint_coding_constraint_jar 2 | 3 | import com.android.tools.lint.client.api.IssueRegistry 4 | import com.android.tools.lint.detector.api.CURRENT_API 5 | import com.android.tools.lint.detector.api.Issue 6 | import com.wp.lint_coding_constraint_jar.detector.ActivityAndFragmentDetector 7 | import com.wp.lint_coding_constraint_jar.detector.NameDetector 8 | import com.wp.lint_coding_constraint_jar.detector.element.ElementIdDetector 9 | import com.wp.lint_coding_constraint_jar.detector.element.InvisibleElementDetector 10 | import com.wp.lint_coding_constraint_jar.detector.element.NoValueCanNotPreviewElementDetector 11 | 12 | /** 13 | * 注册Issue 14 | * create by WangPing 15 | * on 2020/8/11 16 | */ 17 | class IssueRegister : IssueRegistry() { 18 | override val issues: List 19 | get() = arrayListOf( 20 | NameDetector.ISSUE, 21 | InvisibleElementDetector.ISSUE, 22 | NoValueCanNotPreviewElementDetector.ISSUE, 23 | ElementIdDetector.ISSUE, 24 | ActivityAndFragmentDetector.ISSUE 25 | ) 26 | 27 | override val api: Int 28 | get() = CURRENT_API 29 | } -------------------------------------------------------------------------------- /lint-coding-constraint-jar/src/main/java/com/wp/lint_coding_constraint_jar/detector/ActivityAndFragmentDetector.kt: -------------------------------------------------------------------------------- 1 | package com.wp.lint_coding_constraint_jar.detector 2 | 3 | import com.android.tools.lint.client.api.UElementHandler 4 | import com.android.tools.lint.detector.api.* 5 | import org.jetbrains.uast.* 6 | import org.jetbrains.uast.visitor.AbstractUastVisitor 7 | 8 | 9 | /** 10 | * 检查类里是否包含某些方法,有些类必须要有固定方法 11 | * 例如: 12 | * Activity里必须要有静态方法startActivity(params) 13 | * Fragment里必须要有静态方法newInstance(params) 14 | * 以便于可以直观的知道使用此Activity或Fragment必须要什么东西以及哪里需要启动此页面 15 | * create by WangPing 16 | * on 2020/8/11 17 | */ 18 | class ActivityAndFragmentDetector : Detector(), Detector.UastScanner { 19 | companion object { 20 | val ISSUE = Issue.create( 21 | id = "NoRequiredMethodError", 22 | briefDescription = "缺失必要方法", 23 | explanation = "请添加必要的方法", 24 | category = Category.USABILITY, 25 | priority = 5, 26 | severity = Severity.ERROR, 27 | implementation = Implementation( 28 | ActivityAndFragmentDetector::class.java, 29 | Scope.JAVA_FILE_SCOPE 30 | ) 31 | ) 32 | } 33 | 34 | //指定扫描范围 35 | override fun getApplicableUastTypes(): List>? { 36 | return arrayListOf(UClass::class.java) 37 | } 38 | 39 | override fun createUastHandler(context: JavaContext): UElementHandler? { 40 | return object : UElementHandler() { 41 | override fun visitClass(node: UClass) { 42 | node.accept(object : AbstractUastVisitor() { 43 | override fun visitClass(node: UClass): Boolean { 44 | return classContainsRequiredMethod(context, node) 45 | } 46 | 47 | private fun classContainsRequiredMethod( 48 | context: JavaContext, 49 | node: UClass 50 | ): Boolean { 51 | if (node.name?.startsWith("Base") == true) { 52 | //activity和fragment的基类不需要验证是否有startActivity、newInstance静态方法 53 | return super.visitClass(node) 54 | } 55 | var classSuper = node.supers.find { 56 | !it.isInterface 57 | } 58 | var isActivity = false 59 | var isFragment = false 60 | while (classSuper != null) { 61 | if (classSuper.name == "Activity") { 62 | isActivity = true 63 | break 64 | } 65 | if (classSuper.name == "Fragment") { 66 | isFragment = true 67 | break 68 | } 69 | if (classSuper.supers.isNullOrEmpty()) { 70 | break 71 | } 72 | classSuper = classSuper.supers.find { 73 | !it.isInterface 74 | } 75 | } 76 | if (isActivity) { 77 | //kotlin版和Java版判断,满足一个就行 78 | val hadRequiredMethod = node.innerClasses?.find { 79 | it.isStatic && it.name == "Companion" && it.methods.find { method -> 80 | method.name == "startActivity" 81 | } != null 82 | } != null || node.methods.find { 83 | it.isStatic && it.name == "startActivity" 84 | } != null 85 | if (!hadRequiredMethod) { 86 | context.report( 87 | ISSUE, 88 | context.getNameLocation(node), 89 | message = "Activity里必须要有静态方法startActivity(),以便于可以统一管理此Activity必须要什么参数以及哪里需要启动此页面" 90 | ) 91 | return true 92 | } 93 | } 94 | if (isFragment) { 95 | //kotlin版和Java版判断,满足一个就行 96 | val hadRequiredMethod = node.innerClasses?.find { 97 | it.isStatic && it.name == "Companion" && it.methods.find { method -> 98 | method.name == "newInstance" 99 | } != null 100 | } != null || node.methods.find { 101 | it.isStatic && it.name == "newInstance" 102 | } != null 103 | if (!hadRequiredMethod) { 104 | context.report( 105 | ISSUE, 106 | context.getNameLocation(node), 107 | message = "Fragment里必须要有静态方法newInstance(),以便于可以统一管理此newInstance必须要什么参数以及哪里需要启动此页面" 108 | ) 109 | return true 110 | } 111 | } 112 | return super.visitClass(node) 113 | } 114 | }) 115 | } 116 | } 117 | } 118 | 119 | } -------------------------------------------------------------------------------- /lint-coding-constraint-jar/src/main/java/com/wp/lint_coding_constraint_jar/detector/NameDetector.kt: -------------------------------------------------------------------------------- 1 | package com.wp.lint_coding_constraint_jar.detector 2 | 3 | import com.android.tools.lint.client.api.UElementHandler 4 | import com.android.tools.lint.detector.api.* 5 | import com.wp.lint_coding_constraint_jar.util.* 6 | import org.jetbrains.uast.* 7 | import org.jetbrains.uast.visitor.AbstractUastVisitor 8 | import java.util.* 9 | 10 | 11 | /** 12 | * 检查命名是否规范,是否有拼写错误 13 | * 类名大驼峰:形如TestClass 14 | * 方法名小驼峰:形如testFunction() 15 | * 变量名小驼峰:形如testVariable 16 | * 静态变量名全大写下划线分割:形如TEST_VARIABLE 17 | * create by WangPing 18 | * on 2020/8/11 19 | */ 20 | class NameDetector : Detector(), Detector.UastScanner { 21 | companion object { 22 | const val VARIABLE_TYPE_IS_LOCAL = 0x0001 23 | const val VARIABLE_TYPE_IS_MEMBER = 0x0010 24 | const val VARIABLE_TYPE_IS_METHOD_PARAMETER = 0x0100 25 | val ISSUE = Issue.create( 26 | id = "NamingInvalidError", 27 | briefDescription = "命名不合法", 28 | explanation = "请使用规范的命名", 29 | category = Category.USABILITY, 30 | priority = 5, 31 | severity = Severity.ERROR, 32 | implementation = Implementation(NameDetector::class.java, Scope.JAVA_FILE_SCOPE) 33 | ) 34 | } 35 | 36 | //指定扫描范围 37 | override fun getApplicableUastTypes(): List>? { 38 | return arrayListOf(UClass::class.java) 39 | } 40 | 41 | override fun createUastHandler(context: JavaContext): UElementHandler? { 42 | return object : UElementHandler() { 43 | override fun visitClass(node: UClass) { 44 | node.accept(NamingConventionVisitor(context)) 45 | } 46 | } 47 | } 48 | 49 | class NamingConventionVisitor( 50 | private val context: JavaContext 51 | ) : AbstractUastVisitor() { 52 | 53 | //扫描类 54 | override fun visitClass(node: UClass): Boolean { 55 | val className = node.name ?: "" 56 | if (className.isEmpty()) { 57 | //伴生对象生成的类不扫描 58 | return super.visitClass(node) 59 | } 60 | if (className.isAllLowerCase()) { 61 | //如果类名全小写证明命名就不规范了 62 | context.report( 63 | ISSUE, 64 | context.getNameLocation(node), 65 | message = noticeClassNameInvalid(className) 66 | ) 67 | return true 68 | } else { 69 | if (className.isBigCamelCase()) { 70 | //是大驼峰 71 | if (!className.eachWordSpellCorrect(true)) { 72 | //拼写不正确 73 | context.report( 74 | ISSUE, 75 | context.getNameLocation(node), 76 | message = noticeWordSpellIncorrect(className) 77 | ) 78 | return true 79 | } 80 | } else { 81 | //不是大驼峰 82 | context.report( 83 | ISSUE, 84 | context.getNameLocation(node), 85 | message = noticeClassNameInvalid(className) 86 | ) 87 | return true 88 | } 89 | } 90 | return super.visitClass(node) 91 | } 92 | 93 | //扫描成员变量 94 | override fun visitField(node: UField): Boolean { 95 | val variableName = node.name 96 | if (variableName.startsWith("var")) { 97 | //kotlin自动生成的乱七八糟的变量 98 | return super.visitField(node) 99 | } 100 | if (node.isStatic) { 101 | //如果是静态变量 102 | if (variableName.isAllUpperCase()) { 103 | var wordSpellCorrect = true 104 | variableName.split("_").forEach { 105 | if (!it.toLowerCase(Locale.ROOT).wordSpellCorrect()) { 106 | wordSpellCorrect = false 107 | return@forEach 108 | } 109 | } 110 | if (!wordSpellCorrect) { 111 | //拼写不正确 112 | context.report( 113 | ISSUE, 114 | context.getNameLocation(node), 115 | message = noticeWordSpellIncorrect(variableName) 116 | ) 117 | return true 118 | } 119 | } else { 120 | //不是全大写,命名不规范 121 | context.report( 122 | ISSUE, 123 | context.getNameLocation(node), 124 | message = noticeStaticVariableNameInvalid(variableName) 125 | ) 126 | return true 127 | } 128 | } else { 129 | //不是静态变量 130 | if (variableName.isAllLowerCase()) { 131 | //全小写 132 | if (variableName.containsCharIsNotLetter()) { 133 | //有非字母的字符,命名不规范 134 | context.report( 135 | ISSUE, 136 | context.getNameLocation(node), 137 | message = noticeVariableNameInvalid( 138 | variableName, 139 | VARIABLE_TYPE_IS_MEMBER 140 | ) 141 | ) 142 | return true 143 | } else { 144 | if (!variableName.wordSpellCorrect()) { 145 | //拼写不正确 146 | context.report( 147 | ISSUE, 148 | context.getNameLocation(node), 149 | message = noticeWordSpellIncorrect(variableName) 150 | ) 151 | return true 152 | } 153 | } 154 | } else { 155 | if (variableName.isLowerCamelCase()) { 156 | //是小驼峰,检查拼写是否有误 157 | if (!variableName.eachWordSpellCorrect()) { 158 | context.report( 159 | ISSUE, 160 | context.getNameLocation(node), 161 | message = noticeWordSpellIncorrect(variableName) 162 | ) 163 | return true 164 | } 165 | } else { 166 | //不是小驼峰,命名不规范 167 | if (!variableName.eachWordSpellCorrect()) { 168 | context.report( 169 | ISSUE, 170 | context.getNameLocation(node), 171 | message = noticeVariableNameInvalid( 172 | variableName, 173 | VARIABLE_TYPE_IS_MEMBER 174 | ) 175 | ) 176 | return true 177 | } 178 | } 179 | } 180 | } 181 | return super.visitField(node) 182 | } 183 | 184 | //扫描方法 185 | override fun visitMethod(node: UMethod): Boolean { 186 | if (node.name.startsWith("get") 187 | && node.name.substring(3, node.name.length).isAllUpperCase() 188 | ) { 189 | //有些kotlin自动生成的代码确实不好区分 190 | return super.visitMethod(node) 191 | } 192 | if (!node.isConstructor && !node.name.startsWith("component")) { 193 | //当前方法不是构造方法 194 | val methodName = node.name 195 | if (methodName.isAllLowerCase()) { 196 | if (methodName.containsCharIsNotLetter()) { 197 | //有非字母的字符,命名不规范 198 | context.report( 199 | ISSUE, 200 | context.getNameLocation(node), 201 | message = noticeMethodNameInvalid(methodName) 202 | ) 203 | return true 204 | } else { 205 | if (!methodName.wordSpellCorrect()) { 206 | //拼写不正确,直接当命名不规范甩出去 207 | context.report( 208 | ISSUE, 209 | context.getNameLocation(node), 210 | message = noticeWordSpellIncorrect(methodName) 211 | ) 212 | return true 213 | } 214 | } 215 | } else { 216 | if (methodName.isLowerCamelCase()) { 217 | //是小驼峰,检查拼写是否有误 218 | if (!methodName.eachWordSpellCorrect()) { 219 | context.report( 220 | ISSUE, 221 | context.getNameLocation(node), 222 | message = noticeWordSpellIncorrect(methodName) 223 | ) 224 | return true 225 | } 226 | } else { 227 | //不是小驼峰,命名不规范 228 | if (!methodName.eachWordSpellCorrect()) { 229 | context.report( 230 | ISSUE, 231 | context.getNameLocation(node), 232 | message = noticeMethodNameInvalid(methodName) 233 | ) 234 | return true 235 | } 236 | } 237 | } 238 | } 239 | 240 | if (node.hasParameters()) { 241 | //方法有参数 242 | node.parameterList.parameters.forEach { 243 | val parameterName = it.name 244 | if (!parameterName.startsWith("$")) { 245 | //不是kotlin自动生成的乱七八糟的变量 246 | if (parameterName.isAllLowerCase()) { 247 | //全小写,检查是否拼写有误 248 | if (parameterName.containsCharIsNotLetter()) { 249 | //有非字母的字符,命名不规范 250 | context.report( 251 | ISSUE, 252 | context.getNameLocation(it), 253 | message = noticeVariableNameInvalid( 254 | parameterName, 255 | VARIABLE_TYPE_IS_METHOD_PARAMETER 256 | ) 257 | ) 258 | return true 259 | } else { 260 | if (!parameterName.wordSpellCorrect()) { 261 | //拼写不正确,直接当命名不规范甩出去 262 | context.report( 263 | ISSUE, 264 | context.getNameLocation(it), 265 | message = noticeWordSpellIncorrect(parameterName) 266 | ) 267 | return true 268 | } 269 | } 270 | } else { 271 | if (parameterName.isLowerCamelCase()) { 272 | //是小驼峰,检查拼写是否有误 273 | if (!parameterName.eachWordSpellCorrect()) { 274 | context.report( 275 | ISSUE, 276 | context.getNameLocation(it), 277 | message = noticeWordSpellIncorrect(parameterName) 278 | ) 279 | return true 280 | } 281 | } else { 282 | //不是小驼峰,命名不规范 283 | if (!parameterName.eachWordSpellCorrect()) { 284 | context.report( 285 | ISSUE, 286 | context.getNameLocation(it), 287 | message = noticeVariableNameInvalid( 288 | parameterName, 289 | VARIABLE_TYPE_IS_METHOD_PARAMETER 290 | ) 291 | ) 292 | return true 293 | } 294 | } 295 | } 296 | } 297 | } 298 | } 299 | return super.visitMethod(node) 300 | } 301 | 302 | //扫描局部变量 303 | override fun visitLocalVariable(node: ULocalVariable): Boolean { 304 | val variableName = node.name 305 | if (variableName.startsWith("var")) { 306 | //kotlin自动生成的乱七八糟的变量 307 | return super.visitLocalVariable(node) 308 | } 309 | if (variableName.isAllLowerCase()) { 310 | //全小写,检查是否拼写有误 311 | if (variableName.containsCharIsNotLetter()) { 312 | //有非字母的字符,命名不规范 313 | context.report( 314 | ISSUE, 315 | context.getNameLocation(node), 316 | message = noticeVariableNameInvalid(variableName, VARIABLE_TYPE_IS_LOCAL) 317 | ) 318 | return true 319 | } else { 320 | if (!variableName.wordSpellCorrect()) { 321 | //拼写不正确,直接当命名不规范甩出去 322 | context.report( 323 | ISSUE, 324 | context.getNameLocation(node), 325 | message = noticeWordSpellIncorrect(variableName) 326 | ) 327 | return true 328 | } 329 | } 330 | } else { 331 | if (variableName.isLowerCamelCase()) { 332 | //是小驼峰,检查拼写是否有误 333 | if (!variableName.eachWordSpellCorrect()) { 334 | context.report( 335 | ISSUE, 336 | context.getNameLocation(node), 337 | message = noticeWordSpellIncorrect(variableName) 338 | ) 339 | return true 340 | } 341 | } else { 342 | //不是小驼峰,命名不规范 343 | if (!variableName.eachWordSpellCorrect()) { 344 | context.report( 345 | ISSUE, 346 | context.getNameLocation(node), 347 | message = noticeVariableNameInvalid( 348 | variableName, 349 | VARIABLE_TYPE_IS_LOCAL 350 | ) 351 | ) 352 | return true 353 | } 354 | } 355 | } 356 | return super.visitLocalVariable(node) 357 | } 358 | 359 | //单词拼写错误提示 360 | private fun noticeWordSpellIncorrect(incorrectWord: String): String = 361 | "${incorrectWord}单词拼写不正确,请检查单词是否拼写有误,若确认无误,请联系lint规则提供者" 362 | 363 | //类命名不规范错误提示 364 | private fun noticeClassNameInvalid(className: String): String = 365 | "${className}命名不规范,类命名采用大驼峰方式,形如:TestClass" 366 | 367 | //变量命名不规范错误提示 368 | private fun noticeVariableNameInvalid( 369 | variableName: String, 370 | variableType: Int 371 | ): String = 372 | "${variableName}命名不规范,${when (variableType) { 373 | VARIABLE_TYPE_IS_LOCAL -> "局部" 374 | VARIABLE_TYPE_IS_MEMBER -> "成员" 375 | VARIABLE_TYPE_IS_METHOD_PARAMETER -> "方法参数" 376 | else -> "" 377 | } 378 | }变量命名采用小驼峰方式,形如:testVariable" 379 | 380 | //静态变量命名不规范错误提示 381 | private fun noticeStaticVariableNameInvalid(variableName: String): String = 382 | "${variableName}命名不规范,静态变量命名采用全大写加下划线方式,形如:TEST_VARIABLE" 383 | 384 | //方法命名不规范错误提示 385 | private fun noticeMethodNameInvalid(variableName: String): String = 386 | "${variableName}命名不规范,方法命名采用小驼峰方式,形如:testFunction()" 387 | } 388 | } -------------------------------------------------------------------------------- /lint-coding-constraint-jar/src/main/java/com/wp/lint_coding_constraint_jar/detector/element/ElementIdDetector.kt: -------------------------------------------------------------------------------- 1 | package com.wp.lint_coding_constraint_jar.detector.element 2 | 3 | import com.android.SdkConstants.* 4 | import com.android.tools.lint.detector.api.* 5 | import com.wp.lint_coding_constraint_jar.util.* 6 | import org.w3c.dom.Attr 7 | 8 | /** 9 | * 规范布局控件的id命名 10 | * create by WangPing 11 | * on 2020/8/11 12 | */ 13 | class ElementIdDetector : Detector(), Detector.XmlScanner { 14 | companion object { 15 | val ISSUE = Issue.create( 16 | id = "ElementIdInvalidError", 17 | briefDescription = "控件id命名错误", 18 | explanation = "控件id命名不规范,请过饭命名", 19 | category = Category.SECURITY, 20 | priority = 5, 21 | severity = Severity.ERROR, 22 | implementation = Implementation( 23 | ElementIdDetector::class.java, 24 | Scope.RESOURCE_FILE_SCOPE 25 | ) 26 | ) 27 | } 28 | 29 | override fun getApplicableAttributes(): Collection? { 30 | return arrayListOf(ATTR_ID) 31 | } 32 | 33 | override fun visitAttribute(context: XmlContext, attribute: Attr) { 34 | val attrIdValue = attribute.value.substring(5, attribute.value.length) 35 | val elementName = attribute.ownerElement.nodeName 36 | //验证前缀 37 | when { 38 | elementName.endsWith(FRAME_LAYOUT) -> { 39 | if (!attrIdValue.startsWith("fl")) { 40 | context.report( 41 | ISSUE, 42 | context.getNameLocation(attribute), 43 | message = noticeElementIdNameInvalid(elementName, "fl") 44 | ) 45 | return 46 | } 47 | } 48 | elementName.endsWith(LINEAR_LAYOUT) -> { 49 | if (!attrIdValue.startsWith("ll")) { 50 | context.report( 51 | ISSUE, 52 | context.getNameLocation(attribute), 53 | message = noticeElementIdNameInvalid(elementName, "ll") 54 | ) 55 | return 56 | } 57 | } 58 | elementName.endsWith(RELATIVE_LAYOUT) -> { 59 | if (!attrIdValue.startsWith("rl")) { 60 | context.report( 61 | ISSUE, 62 | context.getNameLocation(attribute), 63 | message = noticeElementIdNameInvalid(elementName, "rl") 64 | ) 65 | return 66 | } 67 | } 68 | elementName.endsWith(SCROLL_VIEW) -> { 69 | if (!attrIdValue.startsWith("sv")) { 70 | context.report( 71 | ISSUE, 72 | context.getNameLocation(attribute), 73 | message = noticeElementIdNameInvalid(elementName, "sv") 74 | ) 75 | return 76 | } 77 | } 78 | elementName.endsWith(BUTTON) -> { 79 | if (!attrIdValue.startsWith("btn")) { 80 | context.report( 81 | ISSUE, 82 | context.getNameLocation(attribute), 83 | message = noticeElementIdNameInvalid(elementName, "btn") 84 | ) 85 | return 86 | } 87 | } 88 | elementName.endsWith(GRID_VIEW) -> { 89 | if (!attrIdValue.startsWith("gv")) { 90 | context.report( 91 | ISSUE, 92 | context.getNameLocation(attribute), 93 | message = noticeElementIdNameInvalid(elementName, "gv") 94 | ) 95 | return 96 | } 97 | } 98 | elementName.endsWith(EDIT_TEXT) -> { 99 | if (!attrIdValue.startsWith("et")) { 100 | context.report( 101 | ISSUE, 102 | context.getNameLocation(attribute), 103 | message = noticeElementIdNameInvalid(elementName, "et") 104 | ) 105 | return 106 | } 107 | } 108 | elementName.endsWith(LIST_VIEW) -> { 109 | if (!attrIdValue.startsWith("lv")) { 110 | context.report( 111 | ISSUE, 112 | context.getNameLocation(attribute), 113 | message = noticeElementIdNameInvalid(elementName, "lv") 114 | ) 115 | return 116 | } 117 | } 118 | elementName.endsWith(TEXT_VIEW) -> { 119 | if (!attrIdValue.startsWith("tv")) { 120 | context.report( 121 | ISSUE, 122 | context.getNameLocation(attribute), 123 | message = noticeElementIdNameInvalid(elementName, "tv") 124 | ) 125 | return 126 | } 127 | } 128 | elementName.endsWith(IMAGE_VIEW) -> { 129 | if (!attrIdValue.startsWith("iv")) { 130 | context.report( 131 | ISSUE, 132 | context.getNameLocation(attribute), 133 | message = noticeElementIdNameInvalid(elementName, "iv") 134 | ) 135 | return 136 | } 137 | } 138 | elementName.endsWith(PROGRESS_BAR) -> { 139 | if (!attrIdValue.startsWith("pb")) { 140 | context.report( 141 | ISSUE, 142 | context.getNameLocation(attribute), 143 | message = noticeElementIdNameInvalid(elementName, "pb") 144 | ) 145 | return 146 | } 147 | } 148 | elementName.endsWith(HORIZONTAL_SCROLL_VIEW) -> { 149 | if (!attrIdValue.startsWith("hsv")) { 150 | context.report( 151 | ISSUE, 152 | context.getNameLocation(attribute), 153 | message = noticeElementIdNameInvalid(elementName, "hsv") 154 | ) 155 | return 156 | } 157 | } 158 | elementName.endsWith(RELATIVE_LAYOUT) -> { 159 | if (!attrIdValue.startsWith("rl")) { 160 | context.report( 161 | ISSUE, 162 | context.getNameLocation(attribute), 163 | message = noticeElementIdNameInvalid(elementName, "rl") 164 | ) 165 | return 166 | } 167 | } 168 | elementName.endsWith("RecyclerView") -> { 169 | if (!attrIdValue.startsWith("rv")) { 170 | context.report( 171 | ISSUE, 172 | context.getNameLocation(attribute), 173 | message = noticeElementIdNameInvalid(elementName, "rv") 174 | ) 175 | return 176 | } 177 | } 178 | } 179 | //验证命名规范 180 | if (attrIdValue.isAllLowerCase()) { 181 | //全小写 182 | if (attrIdValue.containsCharIsNotLetter()) { 183 | //有非字母的字符,命名不规范 184 | context.report( 185 | ISSUE, 186 | context.getNameLocation(attribute), 187 | message = noticeVariableNameInvalid(attrIdValue) 188 | ) 189 | } else { 190 | if (!attrIdValue.wordSpellCorrect()) { 191 | //拼写不正确 192 | context.report( 193 | ISSUE, 194 | context.getNameLocation(attribute), 195 | message = noticeWordSpellIncorrect(attrIdValue) 196 | ) 197 | } 198 | } 199 | } else { 200 | if (attrIdValue.isLowerCamelCase()) { 201 | //是小驼峰,检查拼写是否有误 202 | if (!attrIdValue.eachWordSpellCorrect()) { 203 | context.report( 204 | ISSUE, 205 | context.getNameLocation(attribute), 206 | message = noticeWordSpellIncorrect(attrIdValue) 207 | ) 208 | } 209 | } else { 210 | //不是小驼峰,命名不规范 211 | if (!attrIdValue.eachWordSpellCorrect()) { 212 | context.report( 213 | ISSUE, 214 | context.getNameLocation(attribute), 215 | message = noticeVariableNameInvalid(attrIdValue) 216 | ) 217 | } 218 | } 219 | } 220 | } 221 | 222 | //单词拼写错误提示 223 | private fun noticeWordSpellIncorrect(incorrectWord: String): String = 224 | "${incorrectWord}单词拼写不正确,请检查单词是否拼写有误,若确认无误,请联系lint规则提供者" 225 | 226 | //变量命名不规范错误提示 227 | private fun noticeVariableNameInvalid(variableName: String): String = 228 | "${variableName}命名不规范,变量命名采用小驼峰方式,形如:testVariable" 229 | 230 | //控件命名不规范错误提示 231 | private fun noticeElementIdNameInvalid(elementName: String, prefix: String): String = 232 | "${elementName}的id应以${prefix}开头,以便能直接看出使用的什么控件" 233 | 234 | } -------------------------------------------------------------------------------- /lint-coding-constraint-jar/src/main/java/com/wp/lint_coding_constraint_jar/detector/element/InvisibleElementDetector.kt: -------------------------------------------------------------------------------- 1 | package com.wp.lint_coding_constraint_jar.detector.element 2 | 3 | import com.android.SdkConstants.* 4 | import com.android.tools.lint.detector.api.* 5 | import org.w3c.dom.Attr 6 | 7 | /** 8 | * 使用了android:gone或者android:invisible的标签 9 | * 必须使用tools:visible标签以便于预览 10 | * create by WangPing 11 | * on 2020/8/11 12 | */ 13 | class InvisibleElementDetector : Detector(), Detector.XmlScanner { 14 | companion object { 15 | val ISSUE = Issue.create( 16 | id = "CanNotPreviewError", 17 | briefDescription = "布局无法预览", 18 | explanation = "布局无法预览,请添加tools:visible以便于预览", 19 | category = Category.SECURITY, 20 | priority = 5, 21 | severity = Severity.ERROR, 22 | implementation = Implementation( 23 | InvisibleElementDetector::class.java, 24 | Scope.RESOURCE_FILE_SCOPE 25 | ) 26 | ) 27 | } 28 | 29 | override fun getApplicableAttributes(): Collection? { 30 | return arrayListOf(ATTR_VISIBILITY) 31 | } 32 | 33 | override fun visitAttribute(context: XmlContext, attribute: Attr) { 34 | if (!elementCanPreview(attribute)) { 35 | context.report( 36 | ISSUE, 37 | attribute, 38 | context.getLocation(attribute), 39 | message = "添加此属性后布局无法预览,请添加tools:visible以便于预览,如是重叠布局确实无需预览,可以忽略此error", 40 | quickfixData = LintFix.create().set(TOOLS_URI, ATTR_VISIBILITY, "visible").build() 41 | ) 42 | } 43 | } 44 | 45 | //标签是否可以预览 46 | private fun elementCanPreview(attribute: Attr): Boolean { 47 | if (attribute.namespaceURI == ANDROID_URI && (attribute.value == "gone" || attribute.value == "invisible")) { 48 | //使用的是android命名空间,并且visibility在布局文件设置为invisible或者gone 49 | val toolVisibilityAttribute = 50 | attribute.ownerElement.getAttributeNodeNS(TOOLS_URI, ATTR_VISIBILITY) 51 | if (toolVisibilityAttribute?.value != "visible") { 52 | return false 53 | } 54 | } 55 | return true 56 | } 57 | } -------------------------------------------------------------------------------- /lint-coding-constraint-jar/src/main/java/com/wp/lint_coding_constraint_jar/detector/element/NoValueCanNotPreviewElementDetector.kt: -------------------------------------------------------------------------------- 1 | package com.wp.lint_coding_constraint_jar.detector.element 2 | 3 | import com.android.SdkConstants.* 4 | import com.android.tools.lint.detector.api.* 5 | import org.w3c.dom.Element 6 | 7 | /** 8 | * 一些控件如果不设置值那么预览不出来,eg:TextView,EditText 9 | * 必须使用tools:text || tools:hint标签以便于预览 10 | * create by WangPing 11 | * on 2020/8/11 12 | */ 13 | class NoValueCanNotPreviewElementDetector : Detector(), Detector.XmlScanner { 14 | companion object { 15 | val ISSUE = Issue.create( 16 | id = "NoValueCanNotPreviewError", 17 | briefDescription = "布局无法预览", 18 | explanation = "布局无法预览,请添加对应的tools属性以便于预览", 19 | category = Category.SECURITY, 20 | priority = 5, 21 | severity = Severity.ERROR, 22 | implementation = Implementation( 23 | NoValueCanNotPreviewElementDetector::class.java, 24 | Scope.RESOURCE_FILE_SCOPE 25 | ) 26 | ) 27 | } 28 | 29 | override fun getApplicableElements(): Collection? { 30 | //这没办法,源码里不支持后缀扫描,那就只有全盘扫描 31 | //例如我自定义了个DemoTextView就扫描不出来,你要是自定义的View继承自TextView但不以TextView结尾那我没啥好说的了 32 | return XmlScannerConstants.ALL 33 | } 34 | 35 | override fun visitElement(context: XmlContext, element: Element) { 36 | when { 37 | element.nodeName.endsWith(TEXT_VIEW) -> { 38 | //TextView如果没有设置text那么预览不了 39 | val androidTextAttribute = element.getAttributeNodeNS(ANDROID_URI, ATTR_TEXT) 40 | val toolsTextAttribute = element.getAttributeNodeNS(TOOLS_URI, ATTR_TEXT) 41 | if (!toolsTextAttribute?.value.isNullOrEmpty() || !androidTextAttribute?.value.isNullOrEmpty()) { 42 | //android:text或者tools:text属性值不为空,那么能预览 43 | return 44 | } 45 | context.report( 46 | ISSUE, 47 | element, 48 | context.getLocation(element), 49 | message = "TextView如果不设置android:text或者tools:text属性,那么不能预览,请设置。" + 50 | "什么情况选择android:text,什么情况选择tools:text请了解好自行选择", 51 | quickfixData = LintFix.create() 52 | .set(TOOLS_URI, ATTR_TEXT, "自行设置") 53 | .build() 54 | ) 55 | } 56 | element.nodeName.endsWith(IMAGE_VIEW) -> { 57 | val androidSrcAttribute = element.getAttributeNodeNS(ANDROID_URI, ATTR_SRC) 58 | val toolsSrcAttribute = element.getAttributeNodeNS(TOOLS_URI, ATTR_SRC) 59 | if (!androidSrcAttribute?.value.isNullOrEmpty() || !toolsSrcAttribute?.value.isNullOrEmpty()) { 60 | //android:src或者tools:src属性值不为空,那么能预览 61 | return 62 | } 63 | context.report( 64 | ISSUE, 65 | element, 66 | context.getLocation(element), 67 | message = "ImageView如果不设置android:src或者tools:src属性,那么不能预览,请设置。" + 68 | "什么情况选择android:src,什么情况选择tools:src请了解好自行选择", 69 | quickfixData = LintFix.create() 70 | .set(TOOLS_URI, ATTR_SRC, "自行设置") 71 | .build() 72 | ) 73 | } 74 | element.nodeName.endsWith(EDIT_TEXT) -> { 75 | val androidTextAttribute = element.getAttributeNodeNS(ANDROID_URI, ATTR_TEXT) 76 | val androidHintAttribute = element.getAttributeNodeNS(ANDROID_URI, ATTR_HINT) 77 | val toolsTextAttribute = element.getAttributeNodeNS(TOOLS_URI, ATTR_TEXT) 78 | val toolsHintAttribute = element.getAttributeNodeNS(TOOLS_URI, ATTR_HINT) 79 | arrayListOf( 80 | androidTextAttribute, 81 | androidHintAttribute, 82 | toolsTextAttribute, 83 | toolsHintAttribute 84 | ).forEach { 85 | if (!it?.value.isNullOrEmpty()) { 86 | //但凡设置了一个都可以预览,就不报错 87 | return 88 | } 89 | } 90 | context.report( 91 | ISSUE, 92 | element, 93 | context.getLocation(element), 94 | message = "EditText如果不设置android:text || android:hint || tools:text || tools:hint属性," + 95 | "那么不能预览,请设置。上述属性请自行选择设置", 96 | quickfixData = LintFix.create() 97 | .set(TOOLS_URI, ATTR_TEXT, "自行设置") 98 | .build() 99 | ) 100 | } 101 | element.nodeName.endsWith("RecyclerView") -> { 102 | val androidItemCountAttribute = 103 | element.getAttributeNodeNS(ANDROID_URI, ATTR_ITEM_COUNT) 104 | val toolsItemCountAttribute = element.getAttributeNodeNS(TOOLS_URI, ATTR_ITEM_COUNT) 105 | val toolsListItemAttribute = element.getAttributeNodeNS(TOOLS_URI, ATTR_LISTITEM) 106 | if (androidItemCountAttribute?.value.isNullOrEmpty() && toolsItemCountAttribute?.value.isNullOrEmpty()) { 107 | //android:itemCount或者tools:itemCount属性值为空,那么预览效果很差 108 | context.report( 109 | ISSUE, 110 | element, 111 | context.getLocation(element), 112 | message = "RecyclerView如果不设置android:itemCount或者tools:itemCount属性,那么条目预览很差,请设置。" + 113 | "什么情况选择android:itemCount,什么情况选择tools:itemCount请了解好自行选择", 114 | quickfixData = LintFix.create() 115 | .set(TOOLS_URI, ATTR_ITEM_COUNT, "4") 116 | .build() 117 | ) 118 | } 119 | if (toolsListItemAttribute?.value.isNullOrEmpty()) { 120 | //android:listItem或者tools:listItem属性值为空,那么预览效果很差 121 | context.report( 122 | ISSUE, 123 | element, 124 | context.getLocation(element), 125 | message = "RecyclerView如果不设置tools:listitem属性,那么条目预览很差,请设置。多条目数据类型如不好设置可忽略,但最好设置一个", 126 | quickfixData = LintFix.create() 127 | .set(TOOLS_URI, ATTR_LISTITEM, "自行设置") 128 | .build() 129 | ) 130 | } 131 | } 132 | element.nodeName.endsWith(PROGRESS_BAR) -> { 133 | //判断是否设置进度 134 | val androidProgressAttribute = 135 | element.getAttributeNodeNS(ANDROID_URI, ATTR_PROGRESS) 136 | val toolsProgressAttribute = element.getAttributeNodeNS(TOOLS_URI, ATTR_PROGRESS) 137 | if (androidProgressAttribute?.value.isNullOrEmpty() && toolsProgressAttribute?.value.isNullOrEmpty()) { 138 | //android:progress或者tools:progress属性值为空,那么不能预览 139 | context.report( 140 | ISSUE, 141 | element, 142 | context.getLocation(element), 143 | message = "ProgressBar如果不设置android:progress或者tools:progress属性,那么进度效果不能预览,请设置。" + 144 | "什么情况选择android:progress,什么情况选择tools:progress请了解好自行选择", 145 | quickfixData = LintFix.create() 146 | .set(TOOLS_URI, ATTR_PROGRESS, "自行设置") 147 | .build() 148 | ) 149 | } 150 | 151 | //判断是否设置进度样式 152 | val androidProgressDrawableAttribute = 153 | element.getAttributeNodeNS(ANDROID_URI, ATTR_PROGRESS_DRAWABLE) 154 | val toolsProgressDrawableAttribute = 155 | element.getAttributeNodeNS(TOOLS_URI, ATTR_PROGRESS_DRAWABLE) 156 | if (androidProgressDrawableAttribute?.value.isNullOrEmpty() && toolsProgressDrawableAttribute?.value.isNullOrEmpty()) { 157 | //android:progressDrawable或者tools:progressDrawable属性值为空,那么不能预览 158 | context.report( 159 | ISSUE, 160 | element, 161 | context.getLocation(element), 162 | message = "ProgressBar如果不设置android:progressDrawable或者tools:progressDrawable属性,那么进度条样式不能预览,请设置。" + 163 | "什么情况选择android:progressDrawable,什么情况选择tools:progressDrawable请了解好自行选择", 164 | quickfixData = LintFix.create() 165 | .set(TOOLS_URI, ATTR_PROGRESS_DRAWABLE, "自行设置") 166 | .build() 167 | ) 168 | } 169 | } 170 | } 171 | } 172 | } -------------------------------------------------------------------------------- /lint-coding-constraint-jar/src/main/java/com/wp/lint_coding_constraint_jar/util/EnglishDictionary.kt: -------------------------------------------------------------------------------- 1 | package com.wp.lint_coding_constraint_jar.util 2 | 3 | /** 4 | * 英文字典 5 | * create by WangPing 6 | * on 2020/8/13 7 | */ 8 | object EnglishDictionary { 9 | 10 | private val wordsList by lazy { 11 | EnglishDictionary.javaClass.getResourceAsStream("/words.txt").bufferedReader().readLines() 12 | } 13 | 14 | /** 15 | * 字典里是否包含此单词 16 | * 17 | * @param word 单词 18 | * @return 是否包含 19 | */ 20 | fun contains(word: String) = wordsList.contains(word) 21 | } -------------------------------------------------------------------------------- /lint-coding-constraint-jar/src/main/java/com/wp/lint_coding_constraint_jar/util/StringExt.kt: -------------------------------------------------------------------------------- 1 | package com.wp.lint_coding_constraint_jar.util 2 | 3 | import java.lang.StringBuilder 4 | import java.util.* 5 | 6 | /** 7 | * 字符串是否全小写 8 | * 9 | * @return 是否全小写 10 | */ 11 | fun String.isAllLowerCase(): Boolean { 12 | this.forEach { 13 | if (it.isUpperCase()) { 14 | return false 15 | } 16 | } 17 | return true 18 | } 19 | 20 | /** 21 | * 是否是大驼峰命名 22 | * @return 是否合法 23 | */ 24 | fun String.isBigCamelCase(): Boolean = this.matches("^(([A-Z][a-z]+)+)\$".toRegex()) 25 | 26 | /** 27 | * 是否是小驼峰命名 28 | * @return 是否合法 29 | */ 30 | fun String.isLowerCamelCase(): Boolean = this.matches("^[a-z]+(([A-Z][a-z]+)+)\$".toRegex()) 31 | 32 | /** 33 | * 每个单词是否都拼写正确 34 | * @param isBigCamelCase 是否是大驼峰的命名方式 35 | * @return 是否正确 36 | */ 37 | fun String.eachWordSpellCorrect(isBigCamelCase: Boolean = false): Boolean { 38 | splitByUpperCase(isBigCamelCase).forEach { 39 | if (!it.wordSpellCorrect()) { 40 | return false 41 | } 42 | } 43 | return true 44 | } 45 | 46 | /** 47 | * 单个单词是否拼写正确 48 | * 49 | * @return 是否正确 50 | */ 51 | fun String.wordSpellCorrect(): Boolean { 52 | return EnglishDictionary.contains(this) 53 | } 54 | 55 | /** 56 | * 判断单词中是否有除字母外的字符 57 | * 58 | * @return true有字母外的字符 59 | */ 60 | fun String.containsCharIsNotLetter(): Boolean { 61 | this.forEach { 62 | if (it !in 'a'..'z' && it !in 'A'..'Z') { 63 | return true 64 | } 65 | } 66 | return false 67 | } 68 | 69 | /** 70 | * 单词是否全大写,没管特殊符号 71 | * 72 | * @return 是否全大写 73 | */ 74 | fun String.isAllUpperCase(): Boolean { 75 | this.forEach { 76 | if (it.isLowerCase()) { 77 | return false 78 | } 79 | } 80 | return true 81 | } 82 | 83 | //将字符串按大写拆分成单词 84 | private fun String.splitByUpperCase(isBigCamelCase: Boolean = false): List { 85 | val words = arrayListOf() 86 | val word = StringBuilder() 87 | var currentIndex = 0 88 | this.forEach { 89 | if (it.isUpperCase()) { 90 | if (!isBigCamelCase || currentIndex != 0) { 91 | words.add(word.toString().toLowerCase(Locale.ROOT)) 92 | word.clear() 93 | } 94 | } 95 | word.append(it) 96 | currentIndex++ 97 | } 98 | words.add(word.toString().toLowerCase(Locale.ROOT)) 99 | return words 100 | } 101 | 102 | 103 | -------------------------------------------------------------------------------- /pic/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wp529/CodingConstraint/2f5bc86452cddf21b50e8ef479d9bca86b7fc83f/pic/1.png -------------------------------------------------------------------------------- /pic/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wp529/CodingConstraint/2f5bc86452cddf21b50e8ef479d9bca86b7fc83f/pic/2.png -------------------------------------------------------------------------------- /pic/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wp529/CodingConstraint/2f5bc86452cddf21b50e8ef479d9bca86b7fc83f/pic/3.png -------------------------------------------------------------------------------- /pic/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wp529/CodingConstraint/2f5bc86452cddf21b50e8ef479d9bca86b7fc83f/pic/4.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':lint-coding-constraint-arr' 2 | include ':lint-coding-constraint-jar' 3 | include ':app' 4 | rootProject.name = "CodingConstraint" --------------------------------------------------------------------------------