پیاده سازی قواعد lint در اندروید — به زبان ساده
در این مقاله با روش پیادهسازی قواعد lint در اندروید آشنا میشویم. lint یک تحلیلگر استاتیک است که هدف آن یافتن باگها در سورس کد بدون نیاز به کامپایل یا اجرا کردن آن است. حتی اگر در این زمینه اطلاعات مناسبی ندارید، حتماً در زمان کار در اندروید با آن مواجه شدهاید. آیا پیامهایی از نوع زیر برای شما آشنا نیستند؟
اگر چنین پیامهایی را تاکنون دیدهاید پس با lint آشنا هستید. در ادامه با قدرت این قواعد آشنا میشویم و روش کمک آن به توسعهدهندگان برای شناسایی سریع و اصلاح باگها به شیوهای موثر را مشاهده میکنیم.
با این حال مجموعه پیشفرض قواعد lint محدود هستند و در برخی موقعیتها میتوان از ایجاد قواعد سفارشی که مشکلاتی خاص را در پروژهها شناسایی میکنند بهره جست. در ادامه این مقاله با روش ایجاد یک قاعده lint و یکپارچهسازی آن در پروژه آشنا میشویم.
ایجاد ماژول قواعد
کار خود را با تعریف کردن یک ماژول جداگانه Java/Kotlin آغاز میکنیم که قواعد در آن اعلان میشوند. این ماژول به نام قاعده rules نامیده میشود. سپس به rules/build.gradle میرویم و وابستگیهای زیر را اضافه میکنیم:
1dependencies {
2 compileOnly "com.android.tools.lint:lint-api:lint-version"
3 compileOnly "com.android.tools.lint:lint-checks:lint-version"
4}
نکته مهم: به دلایل تاریخی نسخه lint شما باید متناظر با افزونه Gradle اندروید 23 و بالاتر باشد. در صورتی که AGP برابر با 3.5.1 باشد، این شرط برقرار است، در این صورت نسخه lint شما برابر با 26.5.1 است. برای یکپارچهسازی قواعد سفارشی با ماژول app باید کد زیر را به app/build.gradle اضافه کنیم:
1dependencies {
2 ...
3 lintChecks project(path: ':rules')
4}
بدین ترتیب قواعد lint موجود در ماژول rules به صورت نهایی lint.jar کامپایل میشوند که متعاقباً از سوی اپلیکیشن مورد استفاده قرار میگیرند.
مشکلات و شناساگرها
پس از راهاندازی اولیه، اینک میتوانیم به بررسی چگونگی نوشتن عملی قواعد سفارشی lint بپردازیم. به این منظور باید دو مفهوم بنیادی را درک کنیم: مشکلات (Issues) و شناساگرها (Detectors).
مشکل چیست؟
بر اساس مستندات یک مشکل به «باگ بالقوه در اپلیکیشن اندروید» گفته میشود. با استفاده از این مفهوم باگی که باید مورد بررسی و اصلاح قرار گیرد را اعلان میکنیم. یک مشکل ساختار مبنایی به صورت زیر دارد:
- id: برای شناسایی مشکل به صورت یکتا.
- briefDescription: توضیحی خلاصه در مورد مشکل.
- Explanation: یک توضیح دقیقتر در مورد مشکل است و به طور معمول روش حل آن را نیز شامل میشود.
- Category: نوع مشکل را مشخص میسازد. دستهبندیهای بالقوه زیادی مانند CORRECTNESS ،USABILITY ،I18N ،COMPLIANCE ،PERFORMANCE و غیره وجود دارند.
- Priority: شمارهای بین 1 تا 10 است که هر چه بزرگتر باشد، مشکل جدیتر محسوب میشود.
- Severity: میتواند یکی از مقادیر FATAL ،ERROR ،WARNING ،INFORMATIONAL و IGNORE را داشته باشد. اگر severity به صورت FATAL یا ERROR باشد، اجرای lint با مشکل مواجه میشود و باید این مشکل را حل کنید.
- Implementation: این کلاس مسئول آنالیز کردن فایلها و شناسایی مشکلها است.
شناساگر چیست؟
شناساگر یا Detector کلاسی است که امکان یافتن یک یا چند مشکل را دارد و بسته به مشکل میتوان از انواع متفاوتی از شناساگر استفاده کرد تا اینترفیسهای بهتری برای انواع مختلف فایل در اختیار داشت:
- SourceCodeScanner: یک شناساگر خاص برای فایلهای سورس جاوا/کاتلین است.
- XmlScanner: یک شناساگر خاص برای فایلهای XML است.
- GradleScanner: شناساگری خاص برای فایلهای Gradle است.
- ResourceFolderScanner: شناساگری خاص برای پوشههای منابع (و نه فایلهایی که در آنها قرار دارند) است.
اجرای عملی
نخستین مثال مورد بررسی یک مثال کاملاً ساده است. هدف ما ایجاد یک قاعده است که کاربرد android.util.Log را شناسایی کرده و آن را با لاگر عالی com.fabiocarballo.lint.AmazingLog جایگزین میکند. به این منظور ابتدا AndroidLogDetector تعریف میکنیم:
1class AndroidLogDetector : Detector(), SourceCodeScanner {
2
3 override fun getApplicableMethodNames(): List<String> =
4 listOf("tag", "format", "v", "d", "i", "w", "e", "wtf")
5
6 override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
7 super.visitMethodCall(context, node, method)
8 val evaluator = context.evaluator
9 if (evaluator.isMemberInClass(method, "android.util.Log")) {
10 reportUsage(context, node)
11 }
12 }
13
14 private fun reportUsage(context: JavaContext, node: UCallExpression) {
15 context.report(
16 issue = ISSUE,
17 scope = node,
18 location = context.getCallLocation(
19 call = node,
20 includeReceiver = true,
21 includeArguments = true
22 ),
23 message = "android.util.Log usage is forbidden."
24 )
25 }
26
27 (...)
28}
با توجه به امضای کلاس، موارد زیر را میتوان شرح داد:
- این کلاس Detector را بسط میدهد و میتوان برای Lint کردن اندروید جهت شناسایی یک مشکل استفاده شود.
- SourceCodeScanner را بسط میدهد چون باید هر دو مورد فایلهای جاوا و کاتلین را بررسی کنیم.
با توجه به بدنه کلاس میتوان موارد زیر را بیان کرد:
- getApplicableMethodNames تنها امضای متدی که در android.util.Log قرار دارد را فیلتر میکند.
- visitMethodCall: از evaluator استفاده میکند تا مطمئن شود که متد از سوی android.util.Log و نه به وسیله کلاس دیگر فراخوانی میشود. برای نمونه AmazingLog دقیقاً همان متدها را دارد و نمیتواند فلگ شود.
- reportUsage: برای گزارش یک مشکل پس از کشف استفاده میشود.
اینک که شناساگر تعریف شده است تنها کاری که باقی مانده است، تعریف کردن Issue (مشکل) است. این کار را درون یک companion object در فایل AndroidLogDetector انجام میدهیم:
1class AndroidLogDetector : Detector(), SourceCodeScanner {
2
3 (...)
4
5 companion object {
6 private val IMPLEMENTATION = Implementation(
7 AndroidLogDetector::class.java,
8 Scope.JAVA_FILE_SCOPE
9 )
10
11 val ISSUE: Issue = Issue
12 .create(
13 id = "AndroidLogDetector",
14 briefDescription = "The android Log should not be used",
15 explanation = """
16 For amazing showcasing purposes we should not use the Android Log. We should the
17 AmazingLog instead.
18 """.trimIndent(),
19 category = Category.CORRECTNESS,
20 priority = 9,
21 severity = Severity.ERROR,
22 androidSpecific = true,
23 implementation = IMPLEMENTATION
24 )
25 }
26}
رجیستری مشکل
از آنجا که نخستین Issue خود و Detector مربوطه را ایجاد کردهایم، در ادامه باید آن را در اختیار موتور Lint اندروید قرار دهیم. روش انجام این کار از طریق یک IssueRegistry است که تنها مسئولیت آن تعریف کردن مشکلات و شناساگرهای مرتبط با آنها است.
1import com.android.tools.lint.client.api.IssueRegistry
2import com.android.tools.lint.detector.api.CURRENT_API
3import com.android.tools.lint.detector.api.Issue
4
5class IssueRegistry : IssueRegistry() {
6
7 override val api: Int = CURRENT_API
8
9 override val issues: List<Issue>
10 get() = listOf(AndroidLogDetector.ISSUE)
11}
با این حال برای این که Lint اندروید بتواند IssueRegistry ما را کشف کند، باید یک Service Locator اعلان کنیم. Service Locator در یک مکان خاص در پوشه resources تعریف میشود. برای تعریف کردن آن باید مدخل فایل زیر را ایجاد کنیم:
rules/src/main/resources/META-INF/services/com/android/tools/lint/client/api/IssueRegistry
com.fabiocarballo.rules.IssueRegistry
اجرای قواعد
در نهایت همه موارد لازم برای تعریف قواعد lint سفارشی را که به شرح زیر هستند طی کردیم:
- مشکل و شناساگر (AndroidLogDetector و AndroidLogDetector.ISSUE).
- رجیستری مشکل، مشکلات را اعلان میکند.
- Service locator به پیادهسازی IssueRegistry ارجاع میدهد.
- App قواعد lint تعریف شده در rules را از طریق lintCheck مصرف میکند.
بدین ترتیب میتوانیم خطاهایی مانند زیر را شناسایی کنیم:
1import android.util.Log
2
3class Dog {
4
5 fun bark() {
6 Log.d(TAG, "woof! woof!")
7 }
8
9 companion object {
10 private const val TAG = "Sample"
11 }
12}
چنان که دیدیم، این کلاس استفاده از android.util.Log را ممنوع میکند. اینک صرفاً باید ./gradlew app:lintDebug را اجرا کنیم.
چنان که میبینید issue شناسایی شده است. همچنین گزینهای برای بررسی دقیقتر گزارش تولید شده XML/HTML در اختیار دارید. به علاوه مشکل در اندروید استودیو نیز به صورت زیر دیده میشود:
سخن پایانی
در این مقاله با موارد مقدماتی قواعد Lint اندروید آشنا شدیم و یک قاعده سفارشی نیز تعریف کردیم. کدهای این مطلب را میتوانید در این ریپوی گیتهاب (+) مشاهده کنید.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی اندروید
- مجموعه آموزشهای برنامهنویسی
- آموزش برنامه نویسی اندروید (Android) – مقدماتی
- ابزار آنالیز استاتیک کد در اندروید استودیو — راهنمای مقدماتی
- ساده سازی کد کاتلین با Ktlint — راهنمای کاربردی
==