Thực hiện phân tích tĩnh trong quy trình, thay vì sử dụng nó để tìm lỗi

Tôi được thôi thúc viết bài này bởi số lượng lớn tài liệu về phân tích tĩnh đang ngày càng thu hút sự chú ý của tôi. Thứ nhất, điều này Blog của PVS-studio, công ty này tích cực quảng bá chính mình trên Habré với sự trợ giúp của việc đánh giá các lỗi mà công cụ của họ tìm thấy trong các dự án nguồn mở. Gần đây PVS-studio đã triển khai Hỗ trợ Javavà tất nhiên là các nhà phát triển của IntelliJ IDEA, bộ phân tích tích hợp có lẽ là tiên tiến nhất cho Java hiện nay, không thể tránh xa.

Khi đọc những đánh giá như vậy, bạn sẽ có cảm giác như chúng ta đang nói về một loại thần dược: hãy nhấn nút và đây là - một danh sách các khuyết điểm trước mắt bạn. Có vẻ như khi máy phân tích được cải tiến, ngày càng có nhiều lỗi được tự động tìm ra và các sản phẩm được quét bởi những robot này sẽ ngày càng tốt hơn mà không cần bất kỳ nỗ lực nào từ phía chúng tôi.

Nhưng không có thuốc tiên kỳ diệu. Tôi muốn nói về những điều thường không được nói đến trong các bài đăng như “đây là những thứ mà robot của chúng tôi có thể tìm thấy”: những gì máy phân tích không thể làm, vai trò và vị trí thực sự của chúng trong quá trình phân phối phần mềm là gì và cách triển khai chúng một cách chính xác .

Thực hiện phân tích tĩnh trong quy trình, thay vì sử dụng nó để tìm lỗi
Bánh cóc (nguồn: wikipedia).

Những gì máy phân tích tĩnh không bao giờ có thể làm được

Phân tích mã nguồn là gì, từ quan điểm thực tế? Chúng tôi cung cấp một số mã nguồn làm đầu vào và làm đầu ra, trong một thời gian ngắn (ngắn hơn nhiều so với việc chạy thử nghiệm), chúng tôi có được một số thông tin về hệ thống của mình. Hạn chế cơ bản và không thể vượt qua về mặt toán học là chúng ta chỉ có thể thu được một loại thông tin khá hẹp theo cách này.

Ví dụ nổi tiếng nhất về một vấn đề không thể giải được bằng phân tích tĩnh là vấn đề tắt máy: Đây là định lý chứng minh rằng không thể phát triển một thuật toán tổng quát có thể xác định từ mã nguồn của một chương trình xem nó sẽ lặp hay kết thúc trong một khoảng thời gian hữu hạn. Một phần mở rộng của định lý này là Định lý gạo, trong đó nêu rõ rằng đối với bất kỳ thuộc tính không tầm thường nào của các hàm tính toán được, việc xác định liệu một chương trình tùy ý có đánh giá một hàm có thuộc tính như vậy hay không là một vấn đề khó giải quyết về mặt thuật toán. Ví dụ, không thể viết một bộ phân tích có thể xác định từ bất kỳ mã nguồn nào xem chương trình đang được phân tích có phải là một triển khai của một thuật toán tính toán, chẳng hạn như bình phương của một số nguyên hay không.

Vì vậy, chức năng của máy phân tích tĩnh có những hạn chế không thể vượt qua. Máy phân tích tĩnh sẽ không bao giờ có thể phát hiện trong mọi trường hợp những điều như sự xuất hiện của "ngoại lệ con trỏ null" trong các ngôn ngữ cho phép giá trị null hoặc trong mọi trường hợp để xác định sự xuất hiện của " không tìm thấy thuộc tính" bằng các ngôn ngữ được gõ động. Tất cả những gì máy phân tích tĩnh tiên tiến nhất có thể làm là nêu bật các trường hợp đặc biệt, trong số tất cả các vấn đề có thể xảy ra với mã nguồn của bạn, số lượng các trường hợp đó, không cường điệu chút nào, chỉ là một giọt nước trong đại dương.

Phân tích tĩnh không phải là tìm lỗi

Từ những điều trên, kết luận như sau: phân tích tĩnh không phải là phương tiện để giảm số lượng lỗi trong một chương trình. Tôi dám nói: khi áp dụng cho dự án của bạn lần đầu tiên, nó sẽ tìm thấy những vị trí “thú vị” trong mã, nhưng rất có thể, nó sẽ không tìm thấy bất kỳ khiếm khuyết nào ảnh hưởng đến chất lượng chương trình của bạn.

Các ví dụ về lỗi được máy phân tích tự động tìm thấy rất ấn tượng, nhưng chúng ta không nên quên rằng những ví dụ này được tìm thấy bằng cách quét một tập hợp lớn các cơ sở mã lớn. Theo nguyên tắc tương tự, tin tặc có cơ hội thử một số mật khẩu đơn giản trên một số lượng lớn tài khoản cuối cùng sẽ tìm thấy những tài khoản có mật khẩu đơn giản.

Điều này có nghĩa là không nên sử dụng phân tích tĩnh? Dĩ nhiên là không! Và vì lý do tương tự, bạn nên kiểm tra từng mật khẩu mới để đảm bảo nó nằm trong danh sách dừng các mật khẩu “đơn giản”.

Phân tích tĩnh không chỉ là tìm lỗi

Trên thực tế, các vấn đề được giải quyết trên thực tế bằng phân tích còn rộng hơn nhiều. Xét cho cùng, nói chung, phân tích tĩnh là bất kỳ quá trình xác minh mã nguồn nào được thực hiện trước khi chúng được tung ra. Dưới đây là một số điều bạn có thể làm:

  • Kiểm tra phong cách mã hóa theo nghĩa rộng nhất của từ này. Điều này bao gồm cả việc kiểm tra định dạng, tìm kiếm việc sử dụng dấu ngoặc đơn trống/bổ sung, đặt ngưỡng cho các số liệu như số dòng/độ phức tạp chu kỳ của một phương thức, v.v. - bất kỳ điều gì có khả năng cản trở khả năng đọc và khả năng bảo trì của mã. Trong Java, một công cụ như vậy là Checkstyle, trong Python - Flake8. Các chương trình của lớp này thường được gọi là “linters”.
  • Không chỉ mã thực thi mới có thể được phân tích. Các tệp tài nguyên như JSON, YAML, XML, .properties có thể (và nên!) được tự động kiểm tra tính hợp lệ. Rốt cuộc, tốt hơn hết là bạn nên phát hiện ra rằng cấu trúc JSON bị hỏng do một số trích dẫn không ghép đôi ở giai đoạn đầu của quá trình xác minh Yêu cầu Kéo tự động hơn là trong quá trình thực hiện hoặc chạy thử nghiệm? Các công cụ thích hợp có sẵn: ví dụ: YAMLlint, JSONLint.
  • Biên dịch (hoặc phân tích cú pháp cho các ngôn ngữ lập trình động) cũng là một loại phân tích tĩnh. Nói chung, trình biên dịch có khả năng đưa ra các cảnh báo cho biết có vấn đề về chất lượng mã nguồn và không nên bỏ qua.
  • Đôi khi việc biên dịch không chỉ đơn thuần là biên dịch mã thực thi. Ví dụ: nếu bạn có tài liệu ở định dạng AsciiDoctor, sau đó tại thời điểm chuyển nó thành HTML/PDF, trình xử lý AsciiDoctor (Plugin Maven) có thể đưa ra cảnh báo, chẳng hạn như về các liên kết nội bộ bị hỏng. Và đây là lý do chính đáng để không chấp nhận Yêu cầu kéo với các thay đổi về tài liệu.
  • Kiểm tra chính tả cũng là một loại phân tích tĩnh. Tính thiết thực một bùa phép có thể kiểm tra chính tả không chỉ trong tài liệu mà còn trong mã nguồn chương trình (nhận xét và chữ) bằng nhiều ngôn ngữ lập trình khác nhau, bao gồm C/C++, Java và Python. Lỗi chính tả trong giao diện người dùng hoặc tài liệu cũng là một lỗi!
  • Kiểm tra cấu hình (về chúng là gì - xem. này и này báo cáo), mặc dù được thực thi trong thời gian chạy thử nghiệm đơn vị như pytest, trên thực tế cũng là một loại phân tích tĩnh, vì chúng không thực thi mã nguồn trong quá trình thực thi.

Như bạn có thể thấy, việc tìm kiếm lỗi trong danh sách này đóng vai trò ít quan trọng nhất và mọi thứ khác đều có sẵn bằng cách sử dụng các công cụ nguồn mở miễn phí.

Bạn nên sử dụng loại phân tích tĩnh nào trong dự án của mình? Tất nhiên, càng nhiều càng tốt! Điều chính là thực hiện nó một cách chính xác, điều này sẽ được thảo luận thêm.

Đường ống phân phối dưới dạng bộ lọc nhiều giai đoạn và phân tích tĩnh là giai đoạn đầu tiên

Phép ẩn dụ cổ điển cho sự tích hợp liên tục là một đường ống dẫn qua đó các luồng thay đổi, từ thay đổi mã nguồn đến phân phối đến sản xuất. Trình tự chuẩn của các giai đoạn trong quy trình này trông như thế này:

  1. phân tích tĩnh
  2. biên soạn
  3. bài kiểm tra đơn vị
  4. kiểm tra tích hợp
  5. kiểm tra giao diện người dùng
  6. kiểm tra thủ công

Những thay đổi bị từ chối ở giai đoạn thứ N của quy trình không được chuyển sang giai đoạn N+1.

Tại sao chính xác là theo cách này mà không phải cách khác? Trong phần thử nghiệm của quy trình, người thử nghiệm sẽ nhận ra kim tự tháp thử nghiệm nổi tiếng.

Thực hiện phân tích tĩnh trong quy trình, thay vì sử dụng nó để tìm lỗi
Kim tự tháp thử nghiệm. Nguồn: bài viết Martin Fowler.

Ở dưới cùng của kim tự tháp này là các bài kiểm tra dễ viết hơn, thực hiện nhanh hơn và không có xu hướng thất bại. Do đó, cần có nhiều mã hơn, chúng phải bao gồm nhiều mã hơn và được thực thi trước. Ở đỉnh kim tự tháp, điều ngược lại là đúng, do đó, số lượng thử nghiệm tích hợp và giao diện người dùng phải giảm xuống mức tối thiểu cần thiết. Người trong chuỗi này là nguồn lực tốn kém nhất, chậm chạp và không đáng tin cậy nhất nên anh ta ở cuối cùng và chỉ thực hiện công việc nếu các giai đoạn trước không tìm thấy bất kỳ khiếm khuyết nào. Tuy nhiên, các nguyên tắc tương tự được sử dụng để xây dựng quy trình ở các phần không liên quan trực tiếp đến thử nghiệm!

Tôi muốn đưa ra một sự tương tự dưới dạng một hệ thống lọc nước nhiều giai đoạn. Nước bẩn (thay đổi có khuyết tật) được cung cấp vào đầu vào, ở đầu ra chúng ta phải nhận được nước sạch, trong đó tất cả các chất gây ô nhiễm không mong muốn đã được loại bỏ.

Thực hiện phân tích tĩnh trong quy trình, thay vì sử dụng nó để tìm lỗi
Bộ lọc nhiều giai đoạn. Nguồn: Wikimedia Commons

Như bạn đã biết, các bộ lọc làm sạch được thiết kế sao cho mỗi tầng tiếp theo có thể lọc ra phần chất gây ô nhiễm ngày càng mịn hơn. Đồng thời, các tầng tinh chế thô hơn có công suất cao hơn và chi phí thấp hơn. Theo cách tương tự của chúng tôi, điều này có nghĩa là các cổng chất lượng đầu vào nhanh hơn, cần ít nỗ lực hơn để khởi động và bản thân chúng hoạt động đơn giản hơn - và đây là trình tự mà chúng được xây dựng. Vai trò của phân tích tĩnh, như chúng ta hiểu bây giờ, chỉ có khả năng loại bỏ những khiếm khuyết lớn nhất, là vai trò của lưới “bùn” ở giai đoạn đầu của tầng lọc.

Bản thân phân tích tĩnh không cải thiện chất lượng của sản phẩm cuối cùng, giống như “bộ lọc bùn” không làm cho nước có thể uống được. Chưa hết, cùng với các yếu tố khác của đường ống, tầm quan trọng của nó là rõ ràng. Mặc dù trong bộ lọc nhiều giai đoạn, các giai đoạn đầu ra có khả năng nắm bắt mọi thứ mà các giai đoạn đầu vào thực hiện, nhưng rõ ràng hậu quả sẽ xảy ra khi cố gắng thực hiện chỉ với các giai đoạn tinh chế mà không có các giai đoạn đầu vào.

Mục đích của “bẫy bùn” là giúp các tầng tiếp theo tránh khỏi những khiếm khuyết rất nghiêm trọng. Ví dụ: ở mức tối thiểu, người thực hiện đánh giá mã không nên bị phân tâm bởi mã được định dạng không chính xác và vi phạm các tiêu chuẩn mã hóa đã được thiết lập (như dấu ngoặc đơn bổ sung hoặc các nhánh lồng nhau quá sâu). Các lỗi như NPE phải được phát hiện bằng các bài kiểm tra đơn vị, nhưng nếu ngay cả trước khi kiểm tra, bộ phân tích chỉ ra cho chúng ta rằng một lỗi chắc chắn sẽ xảy ra thì điều này sẽ tăng tốc đáng kể việc sửa lỗi.

Tôi tin rằng bây giờ chúng ta đã hiểu rõ tại sao phân tích tĩnh không cải thiện chất lượng sản phẩm nếu thỉnh thoảng được sử dụng và nên được sử dụng liên tục để lọc ra những thay đổi có lỗi nghiêm trọng. Câu hỏi liệu việc sử dụng máy phân tích tĩnh có cải thiện chất lượng sản phẩm của bạn hay không cũng tương tự như câu hỏi: “Liệu nước lấy từ ao bẩn có chất lượng nước uống được cải thiện nếu nó được đưa qua một cái chao không?”

Triển khai vào một dự án kế thừa

Một câu hỏi thực tế quan trọng: làm thế nào để triển khai phân tích tĩnh vào quá trình tích hợp liên tục như một “cổng chất lượng”? Trong trường hợp kiểm tra tự động, mọi thứ đều hiển nhiên: có một bộ kiểm tra, việc thất bại của bất kỳ kiểm tra nào trong số đó là đủ lý do để tin rằng việc lắp ráp không vượt qua cổng chất lượng. Nỗ lực cài đặt một cổng theo cách tương tự dựa trên kết quả phân tích tĩnh không thành công: có quá nhiều cảnh báo phân tích trong mã kế thừa, bạn không muốn hoàn toàn bỏ qua chúng, nhưng cũng không thể ngừng vận chuyển sản phẩm chỉ vì nó chứa cảnh báo phân tích.

Khi được sử dụng lần đầu tiên, máy phân tích sẽ đưa ra một số lượng lớn cảnh báo cho bất kỳ dự án nào, phần lớn trong số đó không liên quan đến hoạt động bình thường của sản phẩm. Không thể sửa tất cả các nhận xét này cùng một lúc và nhiều nhận xét là không cần thiết. Sau cùng, chúng tôi biết rằng toàn bộ sản phẩm của chúng tôi hoạt động ngay cả trước khi giới thiệu phân tích tĩnh!

Kết quả là, nhiều người bị hạn chế sử dụng phân tích tĩnh không thường xuyên hoặc chỉ sử dụng nó ở chế độ thông tin khi báo cáo máy phân tích được đưa ra trong quá trình lắp ráp. Điều này tương đương với việc không có bất kỳ phân tích nào, bởi vì nếu chúng ta đã có nhiều cảnh báo, thì việc xuất hiện một cảnh báo khác (dù nghiêm trọng đến đâu) khi thay đổi mã sẽ không được chú ý.

Các phương pháp giới thiệu cổng chất lượng sau đây đã được biết:

  • Đặt giới hạn về tổng số cảnh báo hoặc số lượng cảnh báo chia cho số dòng mã. Điều này hoạt động kém, bởi vì một cổng như vậy tự do cho phép những thay đổi có khiếm khuyết mới đi qua, miễn là giới hạn của chúng không bị vượt quá.
  • Sửa lỗi, tại một thời điểm nhất định, tất cả các cảnh báo cũ trong mã bị bỏ qua và từ chối xây dựng khi có cảnh báo mới. Chức năng này được cung cấp bởi PVS-studio và một số tài nguyên trực tuyến, chẳng hạn như Codacy. Tôi không có cơ hội làm việc tại PVS-studio, vì theo kinh nghiệm của tôi với Codacy, vấn đề chính của họ là việc xác định đâu là lỗi “cũ” và đâu là lỗi “mới” là một thuật toán khá phức tạp và không phải lúc nào cũng hoạt động. chính xác, đặc biệt nếu các tập tin bị sửa đổi hoặc đổi tên nhiều. Theo kinh nghiệm của tôi, Codacy có thể bỏ qua các cảnh báo mới trong yêu cầu kéo, đồng thời không chuyển yêu cầu kéo do các cảnh báo không liên quan đến thay đổi mã của một PR nhất định.
  • Theo tôi, giải pháp hiệu quả nhất là giải pháp được mô tả trong cuốn sách Giao hàng liên tục “phương pháp bắt vít”. Ý tưởng cơ bản là số lượng cảnh báo phân tích tĩnh là một thuộc tính của mỗi bản phát hành và chỉ cho phép những thay đổi không làm tăng tổng số cảnh báo.

Ratchet

Nó hoạt động theo cách này:

  1. Ở giai đoạn đầu, một bản ghi được tạo trong siêu dữ liệu về việc đưa ra số lượng cảnh báo trong mã mà máy phân tích tìm thấy. Vì vậy, khi bạn xây dựng ngược dòng, trình quản lý kho lưu trữ của bạn không chỉ ghi “bản phát hành 7.0.2” mà còn ghi “bản phát hành 7.0.2 chứa 100500 cảnh báo kiểu kiểm tra”. Nếu bạn sử dụng trình quản lý kho lưu trữ nâng cao (chẳng hạn như Artifactory), việc lưu trữ siêu dữ liệu như vậy về bản phát hành của bạn thật dễ dàng.
  2. Bây giờ, mỗi yêu cầu kéo, khi được xây dựng, sẽ so sánh số lượng cảnh báo thu được với số lượng cảnh báo có sẵn trong bản phát hành hiện tại. Nếu PR dẫn đến tăng con số này thì mã sẽ không vượt qua cổng chất lượng để phân tích tĩnh. Nếu số lượng cảnh báo giảm hoặc không thay đổi thì nó sẽ qua.
  3. Ở lần phát hành tiếp theo, số lượng cảnh báo được tính toán lại sẽ được ghi lại trong siêu dữ liệu của bản phát hành.

Vì vậy, từng chút một nhưng đều đặn (như khi bánh cóc hoạt động), số lượng cảnh báo sẽ có xu hướng bằng không. Tất nhiên, hệ thống có thể bị đánh lừa bằng cách đưa ra cảnh báo mới nhưng lại sửa cảnh báo của người khác. Điều này là bình thường, bởi vì trên một khoảng cách dài, nó cho kết quả: các cảnh báo được sửa, theo quy luật, không phải riêng lẻ mà theo một nhóm thuộc loại nhất định cùng một lúc và tất cả các cảnh báo dễ tháo rời đều được loại bỏ khá nhanh.

Biểu đồ này hiển thị tổng số cảnh báo Kiểu kiểm tra trong sáu tháng hoạt động của một “bánh cóc” như vậy trên một trong những dự án OpenSource của chúng tôi. Số lượng cảnh báo đã giảm đi rất nhiều và điều này diễn ra một cách tự nhiên, song song với quá trình phát triển sản phẩm!

Thực hiện phân tích tĩnh trong quy trình, thay vì sử dụng nó để tìm lỗi

Tôi sử dụng phiên bản sửa đổi của phương pháp này, tính riêng các cảnh báo theo mô-đun dự án và công cụ phân tích, dẫn đến tệp YAML có siêu dữ liệu bản dựng trông giống như thế này:

celesta-sql:
  checkstyle: 434
  spotbugs: 45
celesta-core:
  checkstyle: 206
  spotbugs: 13
celesta-maven-plugin:
  checkstyle: 19
  spotbugs: 0
celesta-unit:
  checkstyle: 0
  spotbugs: 0

Trong bất kỳ hệ thống CI nâng cao nào, bánh cóc có thể được triển khai cho mọi công cụ phân tích tĩnh mà không cần dựa vào plugin và công cụ của bên thứ ba. Mỗi máy phân tích tạo ra báo cáo riêng ở định dạng văn bản hoặc XML đơn giản, dễ phân tích. Tất cả những gì còn lại là viết logic cần thiết vào tập lệnh CI. Bạn có thể thấy điều này được triển khai như thế nào trong các dự án nguồn mở của chúng tôi dựa trên Jenkins và Artifactory đây hoặc đây. Cả hai ví dụ đều phụ thuộc vào thư viện ratchetlib: phương pháp countWarnings() đếm các thẻ xml trong các tệp được tạo bởi Checkstyle và Spotbugs theo cách thông thường và compareWarningMaps() thực hiện cùng một bánh cóc, đưa ra lỗi khi số lượng cảnh báo trong bất kỳ danh mục nào tăng lên.

Có thể triển khai thú vị "ratchet" để phân tích chính tả của nhận xét, chữ văn bản và tài liệu bằng cách sử dụng aspell. Như bạn đã biết, khi kiểm tra chính tả, không phải tất cả các từ chưa biết trong từ điển chuẩn đều sai, chúng có thể được thêm vào từ điển người dùng. Nếu bạn tạo một phần từ điển tùy chỉnh trong mã nguồn của dự án thì cổng chất lượng chính tả có thể được xây dựng theo cách này: chạy aspell với từ điển tiêu chuẩn và tùy chỉnh không nên không tìm thấy lỗi chính tả.

Về tầm quan trọng của việc sửa phiên bản máy phân tích

Tóm lại, điểm cần lưu ý là cho dù bạn triển khai phân tích vào quy trình phân phối của mình như thế nào thì phiên bản của máy phân tích vẫn phải được sửa. Nếu bạn cho phép bộ phân tích cập nhật tự động, thì khi tập hợp yêu cầu kéo tiếp theo, các lỗi mới có thể "bật lên" không liên quan đến thay đổi mã, nhưng liên quan đến thực tế là bộ phân tích mới có thể tìm thấy nhiều lỗi hơn - và điều này sẽ phá vỡ quá trình chấp nhận yêu cầu kéo của bạn. Nâng cấp máy phân tích phải là một hành động có ý thức. Tuy nhiên, việc cố định chặt chẽ phiên bản của từng bộ phận lắp ráp nói chung là một yêu cầu cần thiết và là một chủ đề để thảo luận riêng.

Những phát hiện

  • Phân tích tĩnh sẽ không tìm ra lỗi cho bạn và sẽ không cải thiện chất lượng sản phẩm của bạn chỉ nhờ một ứng dụng. Tác động tích cực đến chất lượng chỉ có thể đạt được thông qua việc sử dụng liên tục trong quá trình giao hàng.
  • Tìm lỗi không phải là nhiệm vụ chính của phân tích; phần lớn các chức năng hữu ích đều có sẵn trong các công cụ nguồn mở.
  • Triển khai các cổng chất lượng dựa trên kết quả phân tích tĩnh ở giai đoạn đầu tiên của quy trình phân phối, sử dụng “bánh cóc” cho mã kế thừa.

tài liệu tham khảo

  1. Giao hàng liên tục
  2. A. Kudryavtsev: Phân tích chương trình: làm thế nào để hiểu rằng bạn là một lập trình viên giỏi báo cáo về các phương pháp phân tích mã khác nhau (không chỉ tĩnh!)

Nguồn: www.habr.com

Thêm một lời nhận xét