Tối Ưu Hiệu Suất Quét Mã Tĩnh Trong Phát Triển Android

Trong thực tiễn DevOps hiện đại, quy trình Tích hợp Liên tục (CI) thường bao gồm các bước chính như gửi mã, kiểm tra tĩnh, kiểm thử đơn vị và biên dịch/đóng gói. Đặc biệt, phân tích mã tĩnh đóng vai trò quan trọng trong việc phát hiện sớm các vấn đề về quy tắc mã hóa, lỗi tiềm ẩn và hiệu suất, từ đó đảm bảo chất lượng sản phẩm cuối cùng. Đối với các dự án Android, các công cụ quét mã tĩnh phổ biến bao gồm CheckStyle, Lint và FindBugs. Để giảm chi phí tích hợp, nhiều tổ chức phát triển các plugin quét mã tĩnh nội bộ, tổng hợp các công cụ này.

Ban đầu, khi tích hợp plugin quét mã tĩnh vào hạ tầng CI/CD, chúng tôi sử dụng Jenkins để tự động hóa quá trình xây dựng trong luồng Pull Request (PR) nhằm giám sát chất lượng mã. Thời gian xây dựng trung bình ban đầu chỉ khoảng 1-2 phút, ít ảnh hưởng đến hiệu suất phát triển. Tuy nhiên, theo thời gian, lượng mã tăng lên đáng kể cùng với sự phát triển của nghiệp vụ, và dự án bắt đầu sử dụng nhiều Flavor để đáp ứng các yêu cầu phức tạp. Điều này khiến thời gian xây dựng cho mỗi PR tăng vọt lên khoảng 8-9 phút, trong đó quá trình quét mã tĩnh chiếm tới 50%. Tình trạng này gây ra những thách thức đáng kể về hiệu suất tích hợp liên tục và tốc độ phát triển.

Phân Tích và Chiến Lược Tối Ưu

Để giải quyết các vấn đề trên, chúng tôi đã đặt ra và phân tích ba câu hỏi cốt lõi:

1. Các công cụ quét tích hợp có thực sự cần thiết không?

Chúng tôi đã tiến hành đánh giá chi tiết các công cụ quét mã (CheckStyle, FindBugs, Lint) dựa trên các tiêu chí như: trọng tâm quét, số lượng quy tắc tích hợp, loại đối tượng quét (mã nguồn, bytecode), nguyên lý hoạt động, và ưu nhược điểm (hiệu suất, khả năng mở rộng, tùy chỉnh). Mặc dù FindBugs chỉ hỗ trợ Java 1.0-1.8 và đã được thay thế bởi SpotBugs, chúng tôi vẫn giữ FindBugs cho các dự án cũ chưa nâng cấp lên Java 8.

Kết quả phân tích cho thấy mỗi công cụ có thế mạnh riêng biệt: CheckStyle nhanh chóng và hiệu quả trong việc kiểm tra phong cách mã hóa và độ phức tạp xoay vòng; FindBugs tập trung vào các vấn đề tiềm ẩn trong mã Java, giúp phát hiện lỗi lập trình, vấn đề bảo mật và hiệu suất; Lint là công cụ chính thức của Android, mạnh mẽ, có khả năng tùy chỉnh, mở rộng và toàn diện. Do đó, chúng tôi kết luận rằng việc tích hợp cả ba công cụ này là cần thiết để tận dụng tối đa các đặc tính chuyên biệt của từng công cụ, đảm bảo chất lượng mã toàn diện.

2. Liệu có thể tối ưu hóa quy trình quét?

Sau khi quyết định tích hợp cả ba công cụ, thách thức tiếp theo là hiệu suất quét. Chúng tôi bắt đầu bằng việc phân tích các điểm nghẽn về thời gian:

Phân Tích Thời Gian Quét Mã Tĩnh

Quá trình xây dựng dự án Android phụ thuộc vào Gradle. Mỗi lần xây dựng thực chất là việc thực thi tất cả các Gradle Task. Với đặc tính của Gradle, mỗi Module trong dự án cần thực hiện các Task liên quan đến CheckStyle, FindBugs và Lint. Số lượng Task còn phụ thuộc vào số lượng các biến thể xây dựng (Variant), được xác định bởi công thức: Variant = Flavor * BuildType. Do đó, tổng số Task cho một Module có thể được mô tả bằng công thức: Flavor * BuildType * (Lint, CheckStyle, FindBugs).

Qua ví dụ về một lần quét thực tế, chúng tôi nhận thấy các Task của FindBugs và Lint chiếm phần lớn thời gian, trong khi CheckStyle ít ảnh hưởng. Nhìn chung, ngoài thời gian quét của bản thân công cụ, thời gian còn bị ảnh hưởng đáng kể bởi số lượng Task phát sinh từ việc có nhiều Module và nhiều Variant.

Phân Tích Ý Tưởng Tối Ưu

Đối với thời gian quét của bản thân công cụ, nó phụ thuộc vào thuật toán quét, số lượng quy tắc và số lượng tệp cần quét. Các công cụ quét mã nguồn (CheckStyle, Lint) cần phân tích từ vựng, cú pháp để tạo cây cú pháp trừu tượng (AST), sau đó duyệt AST để khớp với các quy tắc. Các công cụ quét bytecode (FindBugs) cần biên dịch mã nguồn thành tệp Class, sau đó sử dụng BCEL để phân tích lệnh bytecode và khớp với các quy tắc.

Việc tối ưu thuật toán quét của bản thân công cụ là rất phức tạp và có thể không mang lại hiệu quả cao. Do đó, chúng tôi tập trung vào việc giảm thiểu tác động của số lượng Module và Variant.

  • Đối với nhiều Variant: Giải pháp đơn giản hóa là sử dụng phương pháp "đổi không gian lấy thời gian". Khi gửi PR, chúng ta có thể sử dụng các Jenkins Job khác nhau để thực thi tác vụ quét cho từng Variant riêng biệt.
  • Đối với nhiều Module: Một cách là tách các Module thành các thành phần độc lập, đóng gói dưới dạng AAR và tích hợp vào dự án chính, giảm số lượng Module cần quét trong dự án chính. Tuy nhiên, với các Module còn lại, chúng ta cần tìm cách thu thập tất cả các tệp mã nguồn và bytecode đã biên dịch từ mọi Module trong một lần, để chỉ cần chạy một Task quét duy nhất cho mỗi công cụ. Mục tiêu chính cho quét toàn bộ là giải quyết việc thu thập tập hợp tệp mục tiêu từ tất cả các Module một cách hiệu quả.

3. Liệu có hỗ trợ quét tăng dần (incremental scan)?

Các ý tưởng tối ưu trên đều dựa trên quét toàn bộ. Để giảm số lượng tệp cần quét và tăng hiệu suất hơn nữa, chúng tôi xem xét quét tăng dần. Trong quá trình phát triển thông thường, khi gửi PR, chỉ có một phần nhỏ tệp được sửa đổi. Việc chỉ quét các tệp thay đổi, thay vì toàn bộ dự án, sẽ giảm đáng kể thời gian quét không cần thiết.

Chúng tôi đã đặt ra các câu hỏi sau cho giải pháp quét tăng dần:

  • Làm thế nào để thu thập các tệp tăng dần (mã nguồn và tệp Class)?
  • Có giải pháp quét tăng dần nào trong ngành không, và liệu chúng có phù hợp với tình hình hiện tại của chúng ta không?
  • Các công cụ quét hiện tại (Lint, CheckStyle, FindBugs) hỗ trợ quét tệp tăng dần như thế nào?

Dựa trên các phân tích và ý tưởng này, chúng tôi đã triển khai các giải pháp tối ưu hóa sau:

Khám Phá và Triển Khai Tối Ưu

Tối Ưu Quét Toàn Bộ

Thu Thập Tập Hợp Tệp Mục Tiêu Từ Tất Cả Module

Để thu thập tất cả các tệp mục tiêu từ các Module, trước tiên chúng ta cần xác định những Module nào tham gia vào quá trình quét. Trong hệ thống xây dựng Gradle, mỗi Module được mô tả là một "Project". Do đó, chúng ta cần tìm tất cả các Project mà dự án chính phụ thuộc. Vì cấu hình phụ thuộc có thể khác nhau giữa các Variant, chúng ta cần thu thập các Project liên quan đến Variant hiện tại:

static Set<Project> layCacDuAnPhuThuoc(Project duAnGoc, BaseVariant bienThe, Set<Project> ketQua = null) {
  if (ketQua == null) {
    ketQua = new HashSet<>()
  }
  Set taskPhuThuoc = bienThe.javaCompiler.taskDependencies.getDependencies(bienThe.javaCompiler)
  taskPhuThuoc.each { Task tacVu ->
    if (tacVu.project != duAnGoc && coPluginAndroid(tacVu.project)) {
      ketQua.add(tacVu.project)
      BaseVariant bienTheCon = layBienThe(tacVu.project)
      if (bienTheCon.name == bienThe.name || "${bienThe.flavorName}${bienTheCon.buildType.name}".toLowerCase() == bienThe.name.toLowerCase()) {
        layCacDuAnPhuThuoc(tacVu.project, bienTheCon, ketQua)
      }
    }
  }
  return ketQua
}

Sau khi có được tập hợp các Project, chúng ta có thể xử lý tập hợp tệp nguồn và bytecode như sau:

Đối với tệp mã nguồn (dùng cho CheckStyle):

tapHopDuAn.each { duAnMucTieu ->
  if (duAnMucTieu.plugins.hasPlugin(CodeDetectorPlugin) && GradleUtils.coPluginAndroid(duAnMucTieu)) {
    GradleUtils.layMoRongAndroid(duAnMucTieu).sourceSets.all { AndroidSourceSet tapHopNguon ->
      if (!tapHopNguon.name.startsWith("test") && !tapHopNguon.name.startsWith(SdkConstants.FD_TEST)) {
        nguon tapHopNguon.java.srcDirs // 'nguon' là thuộc tính của CheckStyle Task để chỉ định tập hợp tệp
      }
    }
  }
}

Đối với tệp bytecode (dùng cho FindBugs):

static final Collection<String> macDinhLoaiTru = (androidDataBindingLoaiTru + androidLoaiTru + butterKnifeLoaiTru + dagger2LoaiTru).asImmutable()

List<ConfigurableFileTree> tatCaCacTepClass = new ArrayList<>()
ConfigurableFileTree tepClassDuAnHienTai = project.fileTree(dir: bienThe.javaCompile.destinationDir, excludes: macDinhLoaiTru)
tatCaCacTepClass.add(tepClassDuAnHienTai)
GradleUtils.layCacDuAnPhuThuoc(project, bienThe).each { duAnMucTieu ->
  if (duAnMucTieu.plugins.hasPlugin(CodeDetectorPlugin) && GradleUtils.coPluginAndroid(duAnMucTieu)) {
     GradleUtils.layCacBienTheAndroid(duAnMucTieu).each { bienTheDuAnMucTieu ->
     if (bienTheDuAnMucTieu.name == bienThe.name || "${bienTheDuAnMucTieu.name}".toLowerCase() == bienThe.buildType.name.toLowerCase()) {
         tatCaCacTepClass.add(duAnMucTieu.fileTree(dir: bienTheDuAnMucTieu.javaCompile.destinationDir, excludes: macDinhLoaiTru))
      }
    }
  }
}
// Sau khi thu thập, có thể dùng thuộc tính Class của FindBugsTask để chỉ định tập hợp tệp.

Đối với công cụ Lint, Task tương ứng không có thuộc tính trực tiếp để chỉ định các tệp cần quét, nên chúng tôi chưa tối ưu được Lint cho quét toàn bộ.

Kết Quả Tối Ưu Quét Toàn Bộ

Thông qua việc tối ưu hóa quét toàn bộ cho CheckStyle và FindBugs, chúng tôi đã giảm tổng thời gian quét từ khoảng 9 phút xuống còn khoảng 5 phút.

Tối Ưu Quét Tăng Dần

Như đã phân tích, không phải tất cả các tệp đều cần được quét mỗi lần. Do đó, quét tăng dần là một phương pháp hiệu quả để nâng cao tốc độ.

Nghiên Cứu Công Nghệ Quét Tăng Dần

Trước khi triển khai giải pháp cụ thể, chúng tôi đã khảo sát các giải pháp hiện có. Đối với Lint, chúng tôi cần nghiên cứu sâu nguyên lý quét để tìm giải pháp trên phiên bản 3.x. Đối với CheckStyle và FindBugs, chúng tôi cần hiểu rõ các tham số cấu hình để chỉ định tập hợp tệp khác biệt.

Một số công cụ như diff_cover tập trung vào kiểm tra độ bao phủ mã của unit test dựa trên mã tăng dần, không hoàn toàn phù hợp với phân tích mã tĩnh. Tuy nhiên, ý tưởng tìm ra các dòng mã khác biệt là hữu ích. Đối với phân tích mã tĩnh, chỉ các dòng khác biệt là không đủ để phân tích ngữ nghĩa, đặc biệt với FindBugs (cần phân tích bytecode), nên cần giải pháp toàn diện hơn.

Tìm Kiếm Các Tệp Thay Đổi Tăng Dần

Bước đầu tiên của quét tăng dần là xác định các tệp mục tiêu cần quét. Chúng ta có thể sử dụng lệnh git diff để lấy các tệp khác biệt, đồng thời bỏ qua các tệp đã xóa hoặc đổi tên. Chúng ta chỉ quan tâm đến các tệp mới thêm hoặc sửa đổi, và chỉ cần lấy đường dẫn của chúng. Ví dụ: git diff --name-only --diff-filter=dr commitHash1 commitHash2 sẽ so sánh hai commit và lấy đường dẫn các tệp khác biệt, loại trừ tệp đã xóa và đổi tên.

Đối với kịch bản PR, phức tạp hơn một chút vì cần so sánh mã cục bộ với nhánh đích trên kho lưu trữ từ xa. Với các công cụ quản lý mã tích hợp trong Jenkins, chúng ta có thể sử dụng các tham số như ${targetBranch} (nhánh đích) và ${sourceCommitHash} (commit mã nguồn). Chuỗi lệnh sau sẽ giúp lấy các tệp khác biệt so với nhánh đích từ xa:

# 1. Cấu hình bí danh cho kho từ xa
git remote add upstream ${upstreamGitUrl}

# 2. Lấy cập nhật từ nhánh đích từ xa
git fetch upstream ${targetBranch}

# 3. So sánh và lấy đường dẫn các tệp khác biệt
git diff --name-only --diff-filter=dr $sourceCommitHash upstream/$targetBranch

Bằng cách này, chúng tôi đã có được tập hợp các tệp thay đổi tăng dần.

Phân Tích Nguyên Lý Quét của Lint

Trước khi đi sâu vào quét tăng dần của Lint, hãy tìm hiểu quy trình làm việc cơ bản:

  • App Source Files: Các tệp nguồn trong dự án (Java, XML, tài nguyên, ProGuard, v.v.).
  • lint.xml: Cấu hình loại trừ các kiểm tra Lint không mong muốn và tùy chỉnh mức độ nghiêm trọng của vấn đề.
  • lint Tool: Bộ công cụ quét hoàn chỉnh để phân tích cấu trúc mã Android, có thể chạy qua dòng lệnh, IDEA hoặc Gradle.
  • lint Output: Kết quả quét của Lint.

Công cụ Lint hoạt động như một nhà máy, xử lý các nguyên liệu (mã nguồn) thông qua các bộ phát hiện (detector) và tạo ra sản phẩm cuối cùng (kết quả quét). Lint có thể chạy qua dòng lệnh, IDEA hoặc các tác vụ Gradle, nhưng tất cả đều đi qua lớp LintDriver để thực hiện quét. Các thư viện cốt lõi của Lint là lint-api, lint-checks, và lint. Các thư viện lint-gradlelint-gradle-api chỉ là lớp bao bọc cho cách chạy bằng Gradle. Logic cốt lõi nằm trong phương thức analyze() của LintDriver.

Trong phương thức analyze(), có ba bước quan trọng:

  • registerCustomDetectors(projects): Đăng ký các bộ phát hiện tùy chỉnh và tích hợp chúng với các bộ phát hiện có sẵn.
  • computeDetectors(project): Thu thập tất cả các bộ phát hiện khả dụng cho dự án hiện tại.
  • checkProject(project, main): Đây là bước quan trọng nhất, gọi runFileDetectors để quét tệp.

runFileDetectors sử dụng một Scope để mô tả phạm vi quét. Nếu thuộc tính project.subsetnull, Scope sẽ là Scope.ALL, quét toàn bộ dự án. Nếu project.subset không phải là null, nó sẽ duyệt tập hợp các tệp trong subset để xác định phạm vi. Đây chính là điểm đột phá cho việc quét tăng dần. Phương thức addFile(@NonNull File file)getSubset() của lớp Project cho phép chúng ta chỉ định các tệp cần quét.

Triển Khai Tác Vụ Gradle Quét Tăng Dần cho Lint

Các tác vụ Lint trong Gradle (như lint, lintDebug) đều kế thừa từ LintBaseTask. Quá trình quét cuối cùng được thực thi trong phương thức runLint của LintGradleExecution. Phương thức này tạo ra IssueRegistry, LintCliFlags, LintGradleClient, đồng bộ hóa LintOptions và cuối cùng gọi client.run(registry) để bắt đầu quét.

Vì điểm mấu chốt là client.run(registry), chúng tôi cần xây dựng một Client tùy chỉnh để thực hiện quét tăng dần. Thay vì sử dụng reflection (có thể mất đi một số ngữ cảnh từ Gradle), chúng tôi đã chọn cách kế thừa LintBaseTask và tự triển khai tác vụ quét tăng dần, cho phép kiểm soát tốt hơn các tham số và quy trình quét.

Giới Thiệu Quét FindBugs

FindBugs là một công cụ phân tích tĩnh kiểm tra các tệp Class hoặc JAR. Nó sử dụng thư viện BCEL của Apache để phân tích bytecode, so sánh chúng với một tập hợp các mẫu lỗi để tìm vấn đề. FindBugs định nghĩa hơn 300 loại lỗi. Trong Gradle, FindBugs có một Task riêng với các thuộc tính quan trọng:

  • Classes: Tập hợp các tệp Class cần phân tích.
  • Classpath: Đường dẫn đến tất cả các Class liên quan mà các tệp mục tiêu cần để phân tích, nhưng bản thân chúng sẽ không bị quét.
  • Effort: Mức độ phân tích (MIN, Default, MAX), mức càng cao càng nghiêm ngặt và tốn thời gian.
  • findBugsClasspath: Đường dẫn đến thư viện FindBugs, dùng để cấu hình engine quét.
  • reportLevel: Mức độ báo cáo (Low, Medium, High).
  • Reports: Đường dẫn lưu trữ kết quả quét.

Để thực hiện quét tăng dần với FindBugs, chúng ta chỉ cần chỉ định tập hợp tệp Class thông qua thuộc tính Classes.

Phân Tích Quét Tăng Dần của FindBugs

Khi quét một tệp đơn lẻ bằng plugin FindBugs trong IDEA, chúng ta thấy rằng nó chỉ quét tệp đó. Các thông tin hữu ích từ kết quả quét IDEA bao gồm: danh sách thư mục mã nguồn, tập hợp tệp Class mục tiêu đã biên dịch, và Aux Classpath Entries (đường dẫn class cần thiết cho phân tích). Do đó, để thực hiện quét tăng dần, chúng ta cần thu thập ba thuộc tính này.

Cấu Hình AuxClasspath

Classpath (hoặc AuxClasspath) là các thư viện phụ thuộc mà FindBugs cần để phân tích các tệp Class mục tiêu, nhưng bản thân chúng không bị phân tích. Việc thu thập đầy đủ các thư viện phụ thuộc là rất quan trọng để tránh lỗi "class not found" trong quá trình quét:

FileCollection cacTepClassDuocTao = project.fileTree(dir: "${project.buildDir}/intermediates/classes/${bienThe.flavorName}/${bienThe.buildType.name}",includes: classIncludes)

FileCollection classpathMucTieu = project.files()
GradleUtils.layCacDuAnPhuThuoc(project, bienThe).each { duAnMucTieu ->
    GradleUtils.layCacBienTheAndroid(duAnMucTieu).each { bienTheMucTieu ->
        if (bienTheMucTieu.name.capitalize().equalsIgnoreCase(bienThe.name.capitalize())) {
            classpathMucTieu += bienTheMucTieu.javaCompile.classpath
        }
    }
}

classpath = bienThe.javaCompile.classpath + classpathMucTieu + cacTepClassDuocTao

Tối Ưu Hóa Báo Cáo Sai Lệch trong Quét Tăng Dần của FindBugs

Khi quét tăng dần chỉ với một số tệp nhỏ, FindBugs có thể đưa ra các báo cáo sai lệch ở một số quy tắc nhất định, điều không xảy ra khi quét toàn bộ. Ví dụ, một biến tĩnh như public static String buildTime = ""; có thể bị báo cáo là thiếu final nếu nó được gán giá trị ở một lớp khác. Để giải quyết vấn đề này, chúng ta cần xác định các lớp có liên quan trực tiếp đến lớp bị thay đổi (lớp phụ thuộc và lớp được phụ thuộc) và đưa chúng vào quá trình quét. Chúng tôi sử dụng thư viện ASM để phân tích và tìm ra các phụ thuộc này:

void timTatCaCacLopCanQuet(ConfigurableFileTree tatCaCacTepClass) {
    tatCaCacTepCanQuet = [] as HashSet
    String thuMucClassBuild = "${project.buildDir}/$FINDBUGS_ANALYSIS_DIR/$FINDBUGS_ANALYSIS_DIR_ORIGIN"

    Set<File> tepClassModule = tatCaCacTepClass.files
    for (File tệp : tepClassModule) {
        String[] duongDanPhanTach = tệp.absolutePath.split("$FINDBUGS_ANALYSIS_DIR/$FINDBUGS_ANALYSIS_DIR_ORIGIN/")
        if (duongDanPhanTach.length > 1) {
            String tenLop = layTenTepKhongCoDuoi(duongDanPhanTach[1],'.')
            String tienToLopCon = ""
            if (tenLop.contains('$')) {
                tienToLopCon = tenLop.split('\\$')[0]
            }
            if (duongDanTenLopThayDoi.contains(tenLop) || duongDanTenLopThayDoi.contains(tienToLopCon)) {
                tatCaCacTepCanQuet.add(tệp)
            } else {
                Iterable<String> cacLopCanPhanGiai = new ArrayList<String>()
                cacLopCanPhanGiai.add(tệp.absolutePath)
                Set<File> cacLopPhuThuoc = Dependencies.timCacLopPhuThuoc(project, new ClassAcceptor(), thuMucClassBuild, cacLopCanPhanGiai)
                for (File lopPhuThuoc : cacLopPhuThuoc) {
                    if (duongDanTenLopThayDoi.contains(layTenDuongDanPackage(lopPhuThuoc))) {
                        tatCaCacTepCanQuet.add(tệp)
                        break
                    }
                }
            }
        }
    }
}

Cách tiếp cận này giúp giảm đáng kể tỷ lệ báo cáo sai lệch trong quét tăng dần so với các công cụ như IDEA.

Quét Tăng Dần với CheckStyle

So với FindBugs và Lint, việc triển khai quét tăng dần cho CheckStyle đơn giản hơn nhiều. Vì CheckStyle quét mã nguồn, chúng ta chỉ cần chỉ định các tệp mục tiêu thông qua thuộc tính Source:

void cauHinhQuetTangDanNguon() {
    boolean laKiemTraPR = false
    DiffFileFinder congCuTimTepThayDoi

    if (project.hasProperty(CodeDetectorExtension.CHECK_PR)) {
        laKiemTraPR = project.getProperties().get(CodeDetectorExtension.CHECK_PR)
    }

    if (laKiemTraPR) {
        congCuTimTepThayDoi = new DiffFileFinderHelper.PRDiffFileFinder()
    } else {
        congCuTimTepThayDoi = new DiffFileFinderHelper.LocalDiffFileFinder()
    }

    nguon congCuTimTepThayDoi.timCacTepThayDoi(project) // 'nguon' là thuộc tính của CheckStyle Task

    if (getNguon().isEmpty()) {
        println 'Không tìm thấy tệp Java thay đổi, bỏ qua kiểm tra CheckStyle'
    }
}

Kết Quả Tối Ưu

Nhờ việc tối ưu hóa cả quét toàn bộ và quét tăng dần, hiệu suất tổng thể của quá trình quét mã tĩnh đã được cải thiện đáng kể. Thời gian xây dựng PR với quét mã tĩnh đã giảm hơn 50%.

Triển Khai và Chiến Lược Tối Ưu

Đảm Bảo Tính Đa Năng Của Công Cụ Quét

Để plugin quét có thể được sử dụng rộng rãi và với chi phí thấp cho nhiều dự án, chúng tôi đã triển khai các tính năng sau:

  • Hỗ trợ linh hoạt: Cho phép cấu hình để sử dụng quét tăng dần cho mã mới và quét toàn bộ cho các phần mã cũ.
  • Tùy chọn công cụ: Cung cấp khả năng cho các dự án lựa chọn công cụ quét nào sẽ được bật/tắt.

Điều này đảm bảo plugin phù hợp với cả dự án mới và cũ, cũng như các kịch bản sử dụng khác nhau.

Đảm Bảo Tính Toàn Vẹn Của Quét

Để khắc phục vấn đề báo cáo sai lệch trong quét tăng dần của FindBugs, chiến lược của chúng tôi là kết hợp quét tăng dần (cho hiệu suất trong PR) và quét toàn bộ (cho tính toàn vẹn và độ chính xác). Cụ thể, PR sẽ kích hoạt quét tăng dần, trong khi các bản dựng hàng ngày (Daily Build) sẽ thực hiện quét toàn bộ.

Dưới đây là cấu hình ví dụ trong dự án:

apply plugin: 'code-detector'

codeDetector {
    // Đường dẫn tương đối để lưu trữ báo cáo kiểm tra mã tĩnh
    reportRelativePath = rootProject.file('reports')

    /**
     * URL kho lưu trữ từ xa, dùng cho kiểm tra tăng dần khi gửi PR
     */
    upstreamGitUrl = "ssh://git@your_git_repo_url.git"

    checkStyleConfig {
        /**
         * Bật hoặc tắt kiểm tra CheckStyle
         * Bật: true
         * Tắt: false
         */
        enable = true
        /**
         * Có dừng kiểm tra khi có lỗi không
         * Dừng: false
         * Không dừng: true. Nếu không dừng, CheckStyleTask sẽ không thất bại và báo cáo lỗi sẽ không được sao chép
         */
        ignoreFailures = false
        /**
         * Có hiển thị thông tin vi phạm trong log không
         * Hiển thị: true
         * Không hiển thị: false
         */
        showViolations = true
        /**
         * Cấu hình URI thống nhất cho các tệp checkstyle.xml và checkstyle.xsl tùy chỉnh
         * Đường dẫn cấu hình:
         *      "${checkStyleUri}/checkstyle.xml"
         *      "${checkStyleUri}/checkstyle.xsl"
         * Mặc định là null, sử dụng cấu hình mặc định của CodeDetector
         */
        checkStyleUri = rootProject.file('codequality/checkstyle')
    }

    findBugsConfig {
        /**
         * Bật hoặc tắt kiểm tra FindBugs
         * Bật: true
         * Tắt: false
         */
        enable = true
        /**
         * Tùy chọn, đặt cấp độ phân tích, giá trị mặc định là max
         * min, default, hoặc max. max phân tích nghiêm ngặt hơn, báo cáo nhiều lỗi hơn. min ít hơn một chút
         */
        effort = "max"
        /**
         * Tùy chọn, giá trị mặc định là high
         * low, medium, high. Nếu là low, tất cả các lỗi sẽ được báo cáo
         */
        reportLevel = "high"
        /**
         * Cấu hình URI thống nhất cho các tệp findbugs_include.xml và findbugs_exclude.xml tùy chỉnh
         * Đường dẫn cấu hình:
         *      "${findBugsUri}/findbugs_include.xml"
         *      "${findBugsUri}/findbugs_exclude.xml"
         * Mặc định là null, sử dụng cấu hình mặc định của CodeDetector
         */
        findBugsUri = rootProject.file('codequality/findbugs')
    }

    lintConfig {

        /**
         * Bật hoặc tắt kiểm tra Lint
         * Bật: true
         * Tắt: false
         */
        enable = true

        /**
         * Cấu hình URI thống nhất cho các tệp lint.xml và retrolambda_lint.xml tùy chỉnh
         * Đường dẫn cấu hình:
         *      "${lintConfigUri}/lint.xml"
         *      "${lintConfigUri}/retrolambda_lint.xml"
         * Mặc định là null, sử dụng cấu hình mặc định của CodeDetector
         */
        lintConfigUri = rootProject.file('codequality/lint')
    }
}

Chúng tôi mong muốn plugin quét có thể linh hoạt chỉ định quét tăng dần hoặc quét toàn bộ để đáp ứng các kịch bản sử dụng khác nhau, như tích hợp dự án hiện có, dự án mới hoặc kiểm tra khi đóng gói.

Ví dụ lệnh thực thi:

./gradlew ":${tenModuleApp}:assemble${tenBienTheCuoiCung}" -PdetectorEnable=true -PcheckStyleIncrement=true -PlintIncrement=true -PfindBugsIncrement=true -PcheckPR=${kiemTraPR} -PsourceCommitHash=${maCommitNguon} -PtargetBranch=${nhanhDich} --stacktrace

Để đảm bảo rằng các lỗi phát hiện bởi bất kỳ công cụ quét nào cũng không làm gián đoạn nhiệm vụ, và để người dùng có thể dễ dàng xem báo cáo khi chạy cục bộ, chúng tôi đã cấu hình như sau:

def mangTacVuHoanThanh = [tacVuLint, tacVuCheckStyle, tacVuFindbugs]
tacVuKiemTraMa.finalizedBy mangTacVuHoanThanh

"open ${duongDanBaoCao}".execute()

Để đảm bảo PR không gây ra lỗi đóng gói ảnh hưởng đến việc phát hành, các tác vụ kích hoạt khi PR thực chất là tác vụ đóng gói. Chúng tôi đã gắn các tác vụ quét mã tĩnh vào trong tác vụ đóng gói. Đối với các dự án đa Flavor, trên CI, chúng tôi kích hoạt nhiều Job chạy đồng thời các tác vụ quét tăng dần và đóng gói tương ứng với mỗi Flavor. Đồng thời, để đảm bảo tính toàn vẹn của việc quét mã, chúng tôi thực hiện quét toàn bộ trên Job đóng gói cuối cùng.

Thẻ: Android StaticAnalysis CodeQuality Lint CheckStyle

Đăng vào ngày 8 tháng 8 lúc 14:11