Укараняйце статычны аналіз у працэс, а не шукайце з яго дапамогай багі

Напісаць гэты артыкул мяне падштурхнула вялікая колькасць матэрыялаў аб статычным аналізе, якія ўсё часцей трапляюцца на вочы. Па-першае, гэта блог PVS-studio, які актыўна прасоўвае сябе на Хабры пры дапамозе аглядаў памылак, знойдзеных іх інструментам у праектах з адкрытым кодам. Нядаўна PVS-studio рэалізавалі падтрымку Java, і, вядома, распрацоўнікі IntelliJ IDEA, чый убудаваны аналізатар з'яўляецца на сёння, мусіць, самым прасунутым для Java, не маглі застацца ўбаку.

Пры чытанні такіх аглядаў узнікае адчуванне, што гаворка ідзе пра чароўны эліксір: націсні на кнопку, і вось ён - спіс дэфектаў перад вачыма. Падаецца, што па меры ўдасканалення аналізатараў, багаў аўтаматычна будзе знаходзіцца ўсё больш і больш, а прадукты, прасканаваныя гэтымі робатамі, будуць станавіцца ўсё лепш і лепш, без якіх-небудзь намаганняў з нашага боку.

Але чароўных эліксіраў не бывае. Я хацеў бы пагаварыць аб тым, пра што звычайна не гавораць у пастах выгляду «вось якія штукі можа знайсці наш робат»: чаго не могуць аналізатары, у чым іх рэальная роля і месца ў працэсе пастаўкі софту, і як укараняць іх правільна.

Укараняйце статычны аналіз у працэс, а не шукайце з яго дапамогай багі
Храпавік (крыніца: вікіпедыя).

Чаго ніколі не змогуць статычныя аналізатары

Што такое, з практычнага пункта гледжання, аналіз зыходнага кода? Мы падаем на ўваход некаторыя зыходнікі, і на выхадзе за кароткі час (значна больш кароткі, чым прагон тэстаў) атрымліваем некаторыя звесткі аб нашай сістэме. Прынцыповае і матэматычна непераадольнае абмежаванне ў тым, што атрымаць мы такім чынам можам толькі даволі вузкі клас звестак.

Самы знакаміты прыклад задачы, не развязальнай пры дапамозе статычнага аналізу. праблема супыну: гэта тэарэма, якая даказвае, што немагчыма распрацаваць агульны алгарытм, які б па зыходным кодзе праграмы вызначаў, зацыкліцца яна ці завершыцца за канчатковы час. Пашырэннем дадзенай тэарэмы з'яўляецца тэарэма Райса, Якая сцвярджае, для любой нетрывіяльнай уласцівасці вылічальных функцый вызначэнне таго, ці вылічае адвольная праграма функцыю з такой уласцівасцю, з'яўляецца алгарытмічна невырашальнай задачай. Напрыклад, немагчыма напісаць аналізатар, па любым зыходным кодзе вызначальны, ці з'яўляецца аналізаваная праграма імплементацыяй алгарытму, які вылічае, скажам, узвядзенне ў квадрат цэлага ліку.

Такім чынам, функцыянальнасць статычных аналізатараў мае непераадольныя абмежаванні. Статычны аналізатар ніколі не зможа ва ўсіх выпадках вызначыць такія рэчы, як, напрыклад, узнікненне "null pointer exception" у мовах, якія дапускаюць значэнне null, або ва ўсіх выпадках вызначыць узнікненне "attribute not found" у мовах з дынамічнай тыпізацыяй. Усё, што можа самы дасканалы статычны аналізатар - гэта вылучаць прыватныя выпадкі, колькасць якіх сярод усіх магчымых праблем з вашым зыходным кодам з'яўляецца, без перабольшання, кропляй у моры.

Статычны аналіз - гэта не пошук багаў.

З вышэйсказанага варта выснова: статычны аналіз - гэта не сродак памяншэння колькасці дэфектаў у праграме. Рызыкну сцвярджаць: будучы ўпершыню ўжыты да вашага праекту, ён знойдзе ў кодзе «забаўныя» месцы, але, хутчэй за ўсё, не знойдзе ніякіх дэфектаў, якія ўплываюць на якасць працы вашай праграмы.

Прыклады дэфектаў, аўтаматычна знойдзеных аналізатарамі, уражваюць, але не варта забываць, што гэтыя прыклады знойдзены пры дапамозе сканавання вялікага набору вялікіх кодавых баз. Па такім жа прынцыпе ўзломшчыкі, якія маюць магчымасць перабраць некалькі простых пароляў на вялікай колькасці акаўнтаў, у выніку знаходзяць тыя акаўнты, на якіх варта просты пароль.

Ці значыць гэта, што статычны аналіз не трэба прымяняць? Канечне не! І роўна па тым жа чынніку, па якой варта правяраць кожны новы пароль на трапленне ў стоп-ліст "простых" пароляў.

Статычны аналіз - гэта больш, чым пошук багаў

Насамрэч, практычна вырашальныя аналізам задачы значна шырэй. Бо ў цэлым статычны аналіз - гэта любая праверка зыходнікаў, якая ажыццяўляецца да іх запуску. Вось некаторыя рэчы, якія можна рабіць:

  • Праверка стылю кадавання ў шырокім сэнсе гэтага слова. Сюды ўваходзіць як праверка фарматавання, так і пошук выкарыстання пустых/лішніх дужак, усталёўка, парогавых значэнняў на метрыкі накшталт колькасці радкоў / цыкламатычнай складанасці метаду і т. д. - усяго, што патэнцыйна абцяжарвае чытальнасць і падтрымлівальнасць кода. У Java такой прыладай з'яўляецца Checkstyle, у Python - flake8. Праграмы такога класа звычайна называюцца "лінтэры".
  • Аналізу можа падвяргацца не толькі выкананы код. Файлы рэсурсаў, такія як JSON, YAML, XML, .properties могуць (і павінны!) быць аўтаматычна правяраюцца на валіднасць. Бо лепш пазнаць пра тое, што з-за якіх-небудзь няпарных двукоссяў парушаная структура JSON на раннім этапе аўтаматычнай праверкі Pull Request, чым пры выкананні тэстаў ці ў Run time? Якія адпавядаюць прылады маюцца: напрыклад, YAMLlint, JSONLint.
  • Кампіляцыя (або парсінг для дынамічных моў праграмавання) – гэта таксама від статычнага аналізу. Як правіла, кампілятары здольныя выдаваць папярэджанні, якія сігналізуюць аб праблемах з якасцю зыходнага кода, і іх не варта ігнараваць.
  • Часам кампіляцыя - гэта не толькі кампіляцыя выкананага кода. Напрыклад, калі ў вас дакументацыя ў фармаце AsciiDoctor, то ў момант ператварэння яе ў HTML/PDF апрацоўшчык AsciiDoctor (Убудова Maven) можа выдаваць папярэджанні, напрыклад, аб парушаных унутраных спасылках. І гэта – важкая нагода не прыняць Pull Request са зменамі дакументацыі.
  • Праверка правапісу - таксама від статычнага аналізу. Утыліта аспел здольная правяраць правапіс не толькі ў дакументацыі, але і ў зыходных кодах праграм (каментарах і літаралах) на розных мовах праграмавання, у тым ліку C/C++, Java і Python. Памылка правапісу ў карыстацкім інтэрфейсе або дакументацыі - гэта таксама дэфект!
  • Канфігурацыйныя тэсты (пра тое, што гэта такое - гл. гэты и гэты даклады), хоць і выконваюцца ў асяроддзі выканання модульных тэстаў тыпу pytest, насамрэч таксама з'яўляюцца разнавіднасцю статычнага аналізу, бо не выконваюць зыходныя коды падчас свайго выканання.

Як бачым, пошук багаў у гэтым спісе займае найменш важную ролю, а ўсё астатняе даступна шляхам выкарыстання бясплатных open source інструментаў.

Якія з гэтых тыпаў статычнага аналізу варта прымяняць у вашым праекце? Вядома, усё, чым больш - тым лепш! Галоўнае, укараніць гэта правільна, аб чым і пойдзе размова далей.

Канвеер пастаўкі як шматступенны фільтр і статычны аналіз як яго першы каскад

Класічнай метафарай бесперапыннай інтэграцыі з'яўляецца трубаправод (pipeline), па якім працякаюць змены - ад змены зыходнага кода да пастаўкі ў production. Стандартная паслядоўнасць этапаў гэтага канвеера выглядае так:

  1. статычны аналіз
  2. кампіляцыя
  3. модульныя тэсты
  4. інтэграцыйныя тэсты
  5. UI тэсты
  6. ручная праверка

Змены, забракаваныя на N-ым этапе канвеера, не перадаюцца на этап N+1.

Чаму менавіта так, а няйначай? У той частцы канвеера, якая датычыцца тэсціравання, тэсціроўшчыкі даведаюцца шырока вядомую піраміду тэсціравання.

Укараняйце статычны аналіз у працэс, а не шукайце з яго дапамогай багі
Тэставая піраміда. Крыніца: артыкул Марціна Фаулера.

У ніжняй частцы гэтай піраміды размешчаны тэсты, якія лягчэй пісаць, якія хутчэй выконваюцца і не маюць тэндэнцыі да ілжывага спрацоўвання. Таму іх павінна быць больш, яны павінны пакрываць больш кода і выконвацца першымі. У верхняй частцы піраміды ўсё ідзе наадварот, таму колькасць інтэграцыйных і UI тэстаў павінна быць зменшана да неабходнага мінімуму. Чалавек у гэтым ланцужку - самы дарагі, павольны і ненадзейны рэсурс, таму ён знаходзіцца ў самым канцы і выконвае працу толькі ў тым выпадку, калі папярэднія этапы не выявілі ніякіх дэфектаў. Аднак па тых жа самых прынцыпах будуецца канвеер і ў частках, не злучаных непасрэдна з тэставаннем!

Я б хацеў прапанаваць аналогію ў выглядзе шматкаскаднай сістэмы фільтрацыі вады. На ўваход падаецца брудная вада (змены з дэфектамі), на выхадзе мы павінны атрымаць чыстую ваду, усе непажаданыя забруджванні ў якой адсеяны.

Укараняйце статычны аналіз у працэс, а не шукайце з яго дапамогай багі
Шматступенны фільтр. Крыніца: Wikimedia Commons

Як вядома, ачышчальныя фільтры праектуюцца так, што кожны наступны каскад можа адсяваць усё драбнейшую фракцыю забруджванняў. Пры гэтым каскады грубейшай ачысткі маюць вялікую прапускную здольнасць і малодшы кошт. У нашай аналогіі гэта азначае, што ўваходныя quality gates маюць большую хуткадзейнасць, патрабуюць менш намаганняў для запуску і самі па сабе больш непатрабавальныя ў працы – і менавіта ў такой паслядоўнасці яны і выбудаваны. Роля статычнага аналізу, які, як мы зараз разумеем, здольны адсеяць толькі самыя грубыя дэфекты - гэта роля рашоткі-"гразевіка" у самым пачатку каскаду фільтраў.

Статычны аналіз сам па сабе не паляпшае якасць канчатковага прадукта, як «гразевік» не робіць ваду пітнай. І тым не менш, у агульным звязку з іншымі элементамі канвеера яго важнасць відавочная. Хоць у шматкаскадным фільтры выходныя каскады патэнцыйна здольныя ўлавіць усё тое ж, што і ўваходныя - ясна, да якіх наступстваў прывядзе спроба абыйсціся аднымі толькі каскадамі тонкай ачысткі, без уваходных каскадаў.

Мэта «гразевіка» - разгрузіць наступныя каскады ад улоўлівання зусім ужо грубых дэфектаў. Напрыклад, прынамсі, чалавек, які вырабляе code review, не павінен адцягвацца на няправільна адфарматаваны код і парушэнне ўсталяваных нормаў кадавання (накшталт лішніх дужак або занадта глыбока ўкладзеных галінаванняў). Багі накшталт NPE павінны ўлоўлівацца модульнымі тэстамі, але калі яшчэ да тэсту аналізатар нам паказвае на тое, што баг павінен немінуча адбыцца - гэта значна паскорыць яго выпраўленне.

Мяркую, зараз ясна, чаму статычны аналіз не паляпшае якасць прадукта, калі ўжываецца эпізадычна, і павінен ужывацца ўвесь час для адсявання змен з грубымі дэфектамі. Пытанне, ці палепшыць ужыванне статычнага аналізатара якасць вашага прадукта, прыкладна эквівалентны пытанню «ці палепшацца ці пітныя якасці вады, узятай з бруднага вадаёма, калі яе прапусціць праз друшляк?»

Укараненне ў legacy-праект

Важнае практычнае пытанне: як укараніць статычны аналіз у працэс бесперапыннай інтэграцыі ў якасці "quality gate"? У выпадку з аўтаматычнымі тэстамі ўсё відавочна: ёсць набор тэстаў, падзенне любога з іх - дастатковая падстава лічыць, што зборка не прайшла quality gate. Спроба такой жа выявай усталяваць gate па выніках статычнага аналізу правальваецца: на legacy-кодзе папярэджанняў аналізу занадта шмат, цалкам ігнараваць іх не жадаецца, але і спыняць пастаўку прадукта толькі таму, што ў ім ёсць папярэджанні аналізатара, немагчыма.

Будучы ўжыты ўпершыню, на любым праекце аналізатар выдае велізарную колькасць папярэджанняў, пераважная большасць якіх не маюць дачынення да правільнага функцыянавання прадукта. Выпраўляць адразу ўсе гэтыя заўвагі немагчыма, а многія - і не трэба. У рэшце рэшт, мы ж ведаем, што наш прадукт у цэлым працуе, і да ўкаранення статычнага аналізу!

У выніку, многія абмяжоўваюцца эпізадычным выкарыстаннем статычнага аналізу, альбо выкарыстоўваюць яго толькі ў рэжыме інфармавання, калі пры зборцы проста выдаецца справаздача аналізатара. Гэта эквівалентна адсутнасці ўсякага аналізу, таму што калі ў нас ужо маецца мноства папярэджанняў, тое ўзнікненне яшчэ аднаго (калі заўгодна сур'ёзнага) пры змене кода застаецца незаўважаным.

Вядомыя наступныя спосабы ўвядзення quality gates:

  • Устаноўка ліміту агульнай колькасці папярэджанняў або колькасці папярэджанняў, падзеленай на колькасць радкоў кода. Працуе гэта дрэнна, бо такі gate свабодна прапускае змены з новымі дэфектамі, пакуль іх ліміт не перавышаны.
  • Фіксацыя, у пэўны момант, усіх старых папярэджанняў у кодзе як ігнаруемых, і адмова ў зборцы пры ўзнікненні новых папярэджанняў. Такую функцыянальнасць падае PVS-studio і некаторыя анлайн-рэсурсы, напрыклад, Codacy. Мне не давялося працаваць у PVS-studio, што да майго досведу з Codacy, то асноўная іх праблема складаецца ў тым, што вызначэнне што ёсць "старая", а што "новая" памылка – даволі складаны і не заўсёды правільна які працуе алгарытм, асабліва калі файлы моцна змяняюцца ці пераназываюцца. На маёй памяці Codacy мог прапускаць у пул-рэквесце новыя папярэджанні, і ў той жа час не прапускаць pull request з-за папярэджанняў, якія не адносяцца да змен у кодзе дадзенага PR.
  • На мой погляд, найбольш эфектыўным рашэннем з'яўляецца апісаны ў кнізе бесперапынная пастаўка "метад храпавік" ("ratcheting"). Асноўная ідэя заключаецца ў тым, што ўласцівасцю кожнага рэлізу з'яўляецца колькасць папярэджанняў статычнага аналізу, і дапускаюцца толькі такія змены, якія агульную колькасць папярэджанняў не павялічваюць.

Храпавік

Працуе гэта такім чынам:

  1. На першапачатковым этапе рэалізуецца запіс у метададзеных аб рэлізе колькасці папярэджанняў у кодзе, знойдзеных аналізатарамі. Такім чынам, пры зборцы асноўнай галіны ў ваш менеджэр рэпазітароў запісваецца не проста "рэліз 7.0.2", але "рэліз 7.0.2, які змяшчае 100500 Checkstyle-папярэджанняў". Калі вы выкарыстоўваеце прасунуты менеджэр рэпазітароў (такі як Artifactory), захаваць такія метададзеныя аб вашым рэлізе лёгка.
  2. Цяпер кожны pull request пры зборцы параўноўвае колькасць якія атрымліваюцца папярэджанняў з тым, якая колькасць маецца ў бягучым рэлізе. Калі PR прыводзіць да павелічэння гэтага ліку, то код не праходзіць quality gate па статычным аналізе. Калі колькасць папярэджанняў памяншаецца ці не змяняецца - тое праходзіць.
  3. Пры наступным рэлізе пералічаная колькасць папярэджанняў будзе зноў запісана ў метададзеныя рэлізу.

Так патроху, але няўхільна (як пры працы храпавік), колькасць папярэджанняў будзе імкнуцца да нуля. Вядома, сістэму можна падмануць, унёсшы новае папярэджанне, але выправіўшы чужое. Гэта нармальна, т. К. На доўгай дыстанцыі дае вынік: папярэджанні выпраўляюцца, як правіла, не па адзіночцы, а адразу групай вызначанага тыпу, і ўсе лёгка-ухіляемыя папярэджанні даволі хутка аказваюцца ліквідаваны.

На гэтым графіку паказана агульная колькасць Checkstyle-папярэджанняў за паўгода працы такога "храпавік" адным з нашых OpenSource праектаў. Колькасць папярэджанняў зменшылася на парадак, прычым адбылося гэта натуральным чынам, паралельна з распрацоўкай прадукта!

Укараняйце статычны аналіз у працэс, а не шукайце з яго дапамогай багі

Я ўжываю мадыфікаваную версію гэтага метаду, асобна падлічваючы папярэджанні ў разбіўцы па модулях праекту і прыладам аналізу, фармаваны пры гэтым YAML-файл з метададзенымі аб зборцы выглядае прыкладна наступным чынам:

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

У любой прасунутай CI-сістэме «храпавік» можна рэалізаваць для любых прылад статычнага аналізу, не належачы на ​​плагіны і іншыя прылады. Кожны з аналізатараў выдае сваю справаздачу ў простым тэкставым або XML фармаце, які лёгка паддаецца аналізу. Застаецца прапісаць толькі неабходную логіку ў CI-скрыпце. Падглядзець, як гэта рэалізавана ў нашых open source праектах на базе Jenkins і Artifactory, можна тут або тут. Абодва прыклады залежаць ад бібліятэкі ratchetlib: метад countWarnings() звычайнай выявай падлічвае xml-тэгі ў файлах, фармаваных Checkstyle і Spotbugs, а compareWarningMaps() рэалізуе той самы храпавік, выкідваючы памылку ў выпадку, калі колькасць папярэджанняў у якой-небудзь з катэгорый павялічваецца.

Цікавы варыянт рэалізацыі "храпавік" магчымы для аналізу правапісу каментароў, тэкставых літаралаў і дакументацыі з дапамогай aspell. Як вядома, пры праверцы правапісу не ўсе невядомыя стандартнаму слоўніку словы з'яўляюцца няправільнымі, яны могуць быць дададзены ў карыстацкі слоўнік. Калі зрабіць карыстацкі слоўнік часткай зыходнага кода праекта, то quality gate па правапісе можа быць сфармуляваны такім чынам: выкананне aspell са стандартным і карыстацкім слоўнікам не павінна знаходзіць ніякіх памылак правапісу.

Аб важнасці фіксацыі версіі аналізатара

У заключэнне трэба адзначыць наступнае: якім бы выявай вы б ні ўкаранялі аналіз у ваш канвеер пастаўкі, версія аналізатара павінна быць фіксаваная. Калі дапусціць самаадвольнае абнаўленне аналізатара, то пры зборцы чарговага pull request могуць «усплыць» новыя дэфекты, якія не злучаны са зменай кода, а злучаны з тым, што новы аналізатар проста здольны знаходзіць больш дэфектаў – і гэта паламае вам працэс прыёмкі pull request-ов . Апгрэйд аналізатара павінен быць усвядомленым дзеяннем. Зрэшты, цвёрдая фіксацыя версіі кожнай кампаненты зборкі - гэта ў цэлым неабходнае патрабаванне і тэма для асобнай гутаркі.

Высновы

  • Статычны аналіз не знойдзе вам багі і не палепшыць якасць вашага прадукта ў выніку аднаразовага ўжывання. Станоўчы эфект для якасці дае толькі яго пастаяннае прымяненне ў працэсе пастаўкі.
  • Пошук багаў наогул не з'яўляецца галоўнай задачай аналізу, пераважная большасць карысных функцый даступна ў opensource прыладах.
  • Укараняйце quality gates па выніках статычнага аналізу на самым першым этапе канвеера пастаўкі, выкарыстоўваючы "храпавік" для legacy-кода.

Спасылкі

  1. бесперапынная пастаўка
  2. А. Кудраўцаў: Аналіз праграм: як зразумець, што ты добры праграміст даклад аб розных метадах аналізу кода (не толькі статычным!)

Крыніца: habr.com

Дадаць каментар