├── .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 | 
6 |
7 | 
8 |
9 | 
10 |
11 | 
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"
--------------------------------------------------------------------------------