Моя самая любимая часть в статическом анализе кода — это выдвижение гипотез о потенциальных ошибках в коде с последующей их проверкой.
Пример гипотезы:
Функции strpos легко передать аргументы в неправильном порядке.
Но есть вероятность, что даже на нескольких миллионах строк кода подобная диагностика не "выстрелит", поэтому на неудачные гипотезы тратить много времени не хочется.
Сегодня я покажу как выполнять простейший статический анализ с помощью утилиты phpgrep без написания кода.
Под катом:
|
Предпосылки
Вот уже несколько месяцев я занимаюсь поддержкой PHP линтера NoVerify (почитать о котором можно в статье NoVerify: линтер для PHP от Команды ВКонтакте).
Время от времени в команде появляются идеи для новых диагностик. Идей может быть много, а проверить хочется все, особенно если предложенная проверка нацелена на выявление критических дефектов.
Ранее я активно разрабатывал go-critic и ситуация была схожей, с той лишь разницей, что анализировались исходники на Go, а не на PHP. Когда я узнал об утилите gogrep, мой мир перевернулся. Как видно из названия, эта утилита имеет что-то общее с grep'ом, только поиск производится не по регулярным выражениям, а по синтаксическим шаблонам (позже объясню, что это значит).
Я уже не хотел жить без умного grep, поэтому в один вечер решил сесть и написать phpgrep
.
Анализируемый корпус
Чтобы было увлекательно, мы сразу погрузимся в применение. Будем анализировать небольшой набор достаточно известных и крупных PHP проектов, доступных на GitHub.
В наш набор попали следующие проекты:
WordPress | Wikia | Drupal |
yii2 | CodeIgniter | composer |
Phalcon | Joomla! | Laravel |
moodle | ownCloud core | Phabricator |
PHPUnit | symfony | woocommerce |
Для людей, которые замышляют то, что мы с вами замышляем, это очень аппетитный набор.
Итак, поехали!
Использование присваивания в качестве выражения
Если присваивание используется как выражение, причём:
- контекст ожидает результат логической операции (логическое условие) и
- правая часть выражения не имеет побочных эффектов и является константной,
то, скорее всего, в коде ошибка.
Для начала, возьмём за "логический контекст" следующие конструкции:
- Выражение внутри "
if ($cond)
". - Условие тернарного оператора: "
$cond ? $x : $y
". - Условия продолжений циклов "
while ($cond)
" и "for ($init; $cond; $post)
".
В правой части присваивания мы ожидаем константы или литералы.
Оба ограничения нужны для уменьшения количества ложных срабатываний. Если мы ищем присваивания исключительно внутри условий, то вероятность, что имела место опечатка и вместо "=" подразумевался "==", выше. Но даже в этом случае остаются спорные ситуации, где присваивание имеет смысл из-за побочных эффектов, которые могут возникать внутри функции. Чтобы не иметь с ними дело, мы будем выбирать только простейшие присваиваемые значения. |
Начнём с (1):
# утилита поиска по синтаксическим шаблонам
# | производим поиск по текущей директории (и всем дочерним)
# | |
# | |
phpgrep . 'if ($_ = []) $_' # 1
# |
# |
# строка шаблона поиска
# Дополнительные 3 шаблона через отдельные запуски.
phpgrep . 'if ($_ = ${"const"}) $_' # 2
phpgrep . 'if ($_ = ${"str"}) $_' # 3
phpgrep . 'if ($_ = ${"num"}) $_' # 4
Здесь мы видим 4 шаблона, единственным различием между которыми выступает присваиваемое выражение (RHS). Начнём с первого из них.
Шаблон "if ($_ = []) $_
" захватывает if
, у которого любому выражению присваивается пустой массив. $_
сопоставляется с любым expression или statement.
литерал пустого массива (RHS)
|
if ($_ = []) $_
| |
| любое тело if'а, причём не важно, с {} или без
любой LHS у присваивания
В следующих примерах используются более сложные группы const, str и num. В отличие от $_
они описывают ограничения на совместимые операции.
const
— именованная константа или константа класса.str
— строковой литерал любого типа.num
— числовой литерал любого типа.
Этих шаблонов достаточно, чтобы добиться нескольких срабатываний на корпусе.
? moodle/blocks/rss_client/viewfeed.php#L37:
if ($courseid = SITEID) {
$courseid = 0;
}
Вторым срабатыванием в moodle стала зависимость ADOdb. В upstream библиотеки проблема всё ещё присутствует.
? ADOdb/drivers/adodb-odbtp.inc.php#L741:
В этом фрагменте прекрасно многое, но для нас релевантна только первая строка. Вместо сравнения поля databaseType
мы выполняем присваивание и всегда входим внутрь условия.
Ещё одно интересное место, где мы хотим выполнять действия только для "корректных" записей, но, вместо этого, выполняем их всегда и, более того, отмечаем любую запись как корректную!
? moodle/question/format/blackboard_six/formatqti.php#L598:
// For BB Fill in the Blank, only interested in correct answers.
if ($response->feedback = 'correct') {
// ...
}
phpgrep . 'for ($_; $_ = []; $_) $_' phpgrep . 'for ($_; $_ = ${"const"}; $_) $_' phpgrep . 'for ($_; $_ = ${"num"}; $_) $_' phpgrep . 'for ($_; $_ = ${"str"}; $_) $_' phpgrep . 'while ($_ = []) $_' phpgrep . 'while ($_ = ${"const"}) $_' phpgrep . 'while ($_ = ${"num"}) $_' phpgrep . 'while ($_ = ${"str"}) $_' phpgrep . 'if ($_ = []) $_' phpgrep . 'if ($_ = ${"const"}) $_' phpgrep . 'if ($_ = ${"str"}) $_' phpgrep . 'if ($_ = ${"num"}) $_' phpgrep . '$_ = [] ? $_ : $_' phpgrep . '$_ = ${"const"} ? $_ : $_' phpgrep . '$_ = ${"str"} ? $_ : $_' phpgrep . '$_ = ${"num"} ? $_ : $_' phpgrep . '($_ = []) && $_' phpgrep . '($_ = ${"const"}) && $_' phpgrep . '($_ = ${"str"}) && $_' phpgrep . '($_ = ${"num"}) && $_' phpgrep . '$_ && $_ = []' phpgrep . '$_ && $_ = ${"const"}' phpgrep . '$_ && $_ = ${"str"}' phpgrep . '$_ && $_ = ${"num"}' phpgrep . '($_ = []) || $_' phpgrep . '($_ = ${"const"}) || $_' phpgrep . '($_ = ${"str"}) || $_' phpgrep . '($_ = ${"num"}) || $_' phpgrep . '$_ || $_ = []' phpgrep . '$_ || $_ = ${"const"}' phpgrep . '$_ || $_ = ${"str"}' phpgrep . '$_ || $_ = ${"num"}' |
Повторим то, что мы изучили:
- Шаблоны выглядят как PHP-код, который они находят.
$_
обозначает "что угодно". Можно сравнить с.
в регулярных выражениях.${"<class>"}
работает как$_
с ограничением на тип элементов AST.
Стоит ещё подчеркнуть, что всё, кроме переменных, сопоставляется дословно (literally). Это значит, что шаблону "array(1, 2 + 3)
" будет удовлетворять лишь идентичный по синтаксической структуре код (пробелы не влияют). С другой стороны, шаблону "array($_, $_)
" удовлетворяет любой литерал массива из двух элементов.
Сравнение выражения с самим собой
Потребность сравнить что-либо с самим собой возникает очень редко. Это может быть проверка на NaN
, но как минимум в половине случаев это ошибка copy/paste.
? Wikia/app/extensions/SemanticDrilldown/includes/SD_FilterValue.php#L103:
if ( $fv1->month == $fv1->month ) return 0;
Справа должно быть "$fv2->month
".
Для выражения повторяющихся частей в шаблоне мы используем переменные с именами, отличными от "_
". Механизм повторений в шаблоне похож на обратные ссылки в регулярных выражениях.
Шаблон "$x == $x
" будет как раз тем, что найдёт пример выше. Вместо "x
" может использоваться любое имя. Здесь важно лишь то, чтобы имена были идентичны. Переменные шаблона, которые имеют различающиеся имена, не обязаны иметь совпадающее содержимое при захвате.
Следующий пример найден с помощью "$x <= $x
".
? Drupal/core/modules/views/tests/src/Unit/ViewsDataTest.php#L166:
$prev = $base_tables[$base_tables_keys[$i - 1]];
$current = $base_tables[$base_tables_keys[$i]];
$this->assertTrue(
$prev['weight'] <= $current['weight'] &&
$prev['title'] <= $prev['title'], // <-------------- ошибка
'The tables are sorted as expected.');
Дублирующиеся подвыражения
Теперь, когда мы знаем про возможности повторяемых подвыражений, мы можем составить множество любопытных шаблонов.
Один из моих любимцев — "$_ ? $x : $x
".
Это тернарный оператор, у которого true/false ветки идентичны.
? joomla-cms/libraries/src/User/UserHelper.php#L522:
return ($show_encrypt) ? '{SHA256}' . $encrypted : '{SHA256}' . $encrypted;
Обе ветви дублируются, что подсказывает о потенциальной проблеме в коде. Если мы посмотрим на код вокруг, то сможем понять, что там должно было быть вместо этого. В угоду читабельности я вырезал часть кода и сократил название переменной $encrypted
до $enc
.
case 'crypt-blowfish':
return ($show_encrypt ? '{crypt}' : '') . crypt($plaintext, $salt);
case 'md5-base64':
return ($show_encrypt) ? '{MD5}' . $enc : $enc;
case 'ssha':
return ($show_encrypt) ? '{SSHA}' . $enc : $enc;
case 'smd5':
return ($show_encrypt) ? '{SMD5}' . $enc : $enc;
case 'sha256':
return ($show_encrypt) ? '{SHA256}' . $enc : '{SHA256}' . $enc;
default:
return ($show_encrypt) ? '{MD5}' . $enc : $enc;
Я бы поставил на то, что коду необходим следующий патч:
- ($show_encrypt) ? '{SHA256}' . $encrypted : '{SHA256}' . $encrypted;
+ ($show_encrypt) ? '{SHA256}' . $encrypted : $encrypted;
Опасные приоритеты операций в PHP
Хорошей мерой предосторожности в PHP является использование группирующих скобочек везде, где важно иметь правильный порядок вычислений.
Во многих языках программирования выражение "x & mask != 0
" имеет интуитивный смысл. Если mask
описывает какой-то бит, то данный код проверяет, что в x
этот бит не равен нулю. К сожалению, для PHP это выражение будет вычисляться так: "x & (mask != 0)
", что почти всегда не то что вам нужно.
WordPress, Joomla и moodle используют SimplePie.
? SimplePie/library/SimplePie/Locator.php#L254
? SimplePie/library/SimplePie/Locator.php#L384
? SimplePie/library/SimplePie/Locator.php#L412
? SimplePie/library/SimplePie/Sanitize.php#L349
? SimplePie/library/SimplePie.php#L1634
$feed->method & SIMPLEPIE_FILE_SOURCE_REMOTE === 0
SIMPLEPIE_FILE_SOURCE_REMOTE
определён как 1
, поэтому выражение будет эквивалентно:
$feed->method & (1 === 0)
// =>
$feed->method & false
phpgrep . '$x & $mask == $y' phpgrep . '$x & $mask === $y' phpgrep . '$x & $mask !== $y' phpgrep . '$x & $mask != $y' phpgrep . '$x | $mask == $y' phpgrep . '$x | $mask === $y' phpgrep . '$x | $mask !== $y' phpgrep . '$x | $mask != $y' |
Продолжая тему неожиданных приоритетов операций, вы можете почитать про тернарный оператор в PHP. На хабре этому даже посвятили статью: Порядок выполнения тернарного оператора.
Можно ли такие места найти с помощью phpgrep
? Ответ: да!
phpgrep . '$_ == $_ ? $_ : $_ ? $_ : $_'
phpgrep . '$_ != $_ ? $_ : $_ ? $_ : $_'
Прелести проверки URL с помощью регулярных выражений
? Wikia/app/maintenance/wikia/updateCentralInterwiki.inc#L95:
if ( preg_match( '/(wowwiki.com|wikia.com|falloutvault.com)/', $url ) ) {
$local = 1;
} else {
$local = 0;
}
По задумке автора кода, мы проверяем URL на совпадение с одним из 3-х вариантов. К сожалению, символ .
не экранирован, что приведёт к тому, что вместо falloutvault.com
мы можем завести себе falloutvaultxcom
на любом домене и пройти проверку.
Это не PHP-специфичная ошибка. В любом приложении, где валидация выполняется через регулярные выражения и частью проверяемой строки является мета-символ, есть риск забыть экранирование там, где оно нужно, и получить уязвимость.
Найти такие места можно с помощью запуска phpgrep
:
phpgrep . 'preg_match(${"pat:str"}, ${"*"})' 'pat~[^\\]\.(com|ru|net|org)\b'
Мы вводим именованный подшаблон pat
, который захватывает любой строковой литерал, а затем применяем к нему фильтр из регулярного выражения.
Фильтры можно применять к любой переменной шаблона. Кроме регулярных выражений есть также структурные операторы =
и !=
. Полный список можно найти в документации.
${"*"}
захватывает произвольное количество любых аргументов, поэтому нам можно не волноваться за опциональные параметры функции preg_match
.
Дубликаты ключей в литерале массива
В PHP вы не получите никакого предупреждения, если выполните этот код:
<?php
var_dump(['a' => 1, 'a' => 2]);
// Результат: array(1) {["a"]=> int(2)}
Мы можем найти такие массивы с помощью phpgrep
:
[${"*"}, $k => $_, ${"*"}, $k => $_, ${"*"}]
Этот шаблон можно расшифровать так: "литерал массива, в котором есть хотя бы два идентичных ключа в произвольной позиции". Выражения ${"*"}
помогают нам описать "произвольную позицию", допуская 0-N элементов до, между и после интересующих нас ключей.
? Wikia/app/extensions/wikia/WikiaMiniUpload/WikiaMiniUpload_body.php#L23:
$script_a = [
'wmu_back' => wfMessage( 'wmu_back' )->escaped(),
'wmu_back' => wfMessage( 'wmu_back' )->escaped(),
// ...
];
В данном случае это не является грубой ошибкой, но мне известны случаи, когда дублирование ключей в крупных (100+ элементов) массивах несло как минимум неожиданное поведение, в котором один из ключей перекрывал значение другого.
На этом наш краткий экскурс на примерах окончен. Если вам хочется ещё, в конце статьи описано, как получить все результаты.
Что же такое phpgrep?
Большая часть редакторов и IDE используют для поиска кода (если это не поиск по специальному символу типа класса или переменной) обычный текстовой поиск — проще говоря, что-то вроде grep'а.
Вы вводите "$x
", находите "$x
". Вам могут быть доступны регулярные выражения, тогда вы можете пытаться, по сути, парсить PHP-код регулярками. Иногда это даже работает, если ищется что-то вполне определённое и простое — например, «любая переменная с некоторым суффиксом». Но если эта переменная с суффиксом должна быть частью другого составного выражения, возникают трудности.
phpgrep — это инструмент для удобного поиска PHP-кода, который позволяет искать не с помощью text-oriented регулярок, а с помощью syntax-aware шаблонов.
Syntax-aware означает, что язык шаблонов отражает целевой язык, а не оперирует отдельными символами, как это делают регулярные выражения. Нам также нет никакой разницы до форматирования кода, важна лишь его структура.
Quick start
Установка
Для amd64 есть готовые релизные сборки под Linux и Windows, но если у вас установлен Go, то достаточно одной команды, чтобы получить свежий бинарник под вашу платформу:
go get -v github.com/quasilyte/phpgrep/cmd/phpgrep
Если $GOPATH/bin
находится в системном $PATH
, то команда phpgrep
станет сразу же доступной. Чтобы это проверить, попробуйте запустить команду с параметром -help
:
phpgrep -help
Если же ничего не происходит, найдите, куда Go установил бинарник и добавьте его в переменную окружения $PATH
.
Старый и надёжный способ посмотреть $GOPATH
, даже если он не выставлен явно:
go env GOPATH
Использование
Создайте тестовый файл hello.php
:
<?php
function f(...$xs) {}
f(10);
f(20);
f(30);
f($x);
f();
Запустите на нём phpgrep
:
# phpgrep hello.php 'f(${"x:int"})' 'x!=20'
hello.php:3: f(10)
hello.php:5: f(30)
Мы нашли все вызовы функции f
с одним аргументом-числом, значение которого не равно 20.
Как работает phpgrep
Для разбора PHP используется библиотека github.com/z7zmey/php-parser. Она достаточно хороша, но некоторые ограничения phpgrep
следуют из особенностей используемого парсера. Особенно много трудностей возникает при попытках нормально работать со скобочками.
Принцип работы phpgrep
прост:
- строится AST из входного шаблона, разбираются фильтры;
- для каждого входного файла строится полное AST-дерево;
- обходим AST каждого файла, пытаясь найти такие поддеревья, которые совпадают с шаблоном;
- для каждого результата применяется список фильтров;
- все результаты, которые прошли фильтры, печатаются на экран.
Самое интересное — это как именно сопоставляются на равенство два AST-узла. Иногда тривиально: один-к-одному, а мета-узлы могут захватывать более одного элемента. Примерами мета-узлов является ${"*"}
и ${"str"}
.
Заключение
Было бы нечестно говорить о phpgrep
, не упомянув structural search and replace (SSR) из PhpStorm. Они решают похожие задачи, причём у SSR есть свои преимущества, например, интеграция в IDE, а phpgrep
может похвастаться тем, что является standalone программой, которую гораздо проще поставить, например, на CI.
Помимо прочего, phpgrep
— это ещё и библиотека, которую можно использовать в своих программах для матчинга PHP кода. Особенно это полезно для линтеров и кодогенерации.
Буду рад, если этот инструмент будет вам полезен. Если же эта статья просто мотивирует вас посмотреть в сторону вышеупомянутого SSR, тоже хорошо.
Дополнительные материалы
Полный список шаблонов, который был использован для анализа, можно найти в файле patterns.txt. Рядом с этим файлом можно найти скрипт phpgrep-lint.sh
, упрощающий запуск phpgrep
со списком шаблонов.
В статье не дан полный список срабатываний, но вы можете воспроизвести эксперимент, произведя клонирование всех названых репозиториев и запустив phpgrep-lint.sh
на них.
Черпать вдохновение на шаблоны проверок можно, например, из статей PVS studio. Мне очень понравилась Logical Expressions: Mistakes Made by Professionals, которая трансформируется во что-то такое:
# Для "x != y || x != z":
phpgrep . '$x != $a || $x != $b'
phpgrep . '$x !== $a || $x != $b'
phpgrep . '$x != $a || $x !== $b'
phpgrep . '$x !== $a || $x !== $b'
Вам также может быть интересна презентация phpgrep: syntax-aware code search.
В статье используются изображения гоферов, которые были созданы через gopherkon.
Комментарии (21)
ghost404
27.08.2019 08:15Есть лайфхак Yoda conditions. Он помогает избежать ошибок в операциях сравнения, хотя мне такой стиль записи условий не нравится.
berezuev
27.08.2019 09:37PSR-2 не рекомендует к использованию йода-стайл. Обусловлено лучшей читаемостью.
ghost404
27.08.2019 20:27До недавнего времени, он был по умолчанию включён в php-cs-fixer и Style CI. Сейчас уже исправились.
quasilyte Автор
27.08.2019 14:39В языках, где
=
не является выражением, yoda style является нежелательным. :)
От этого обидно, что в PHP приходится такое использовать как защиту от ошибки. Но есть линтеры, которые найдут подозрительные применения присваиваний, так что иногда проще сделать линтеры на CI построже, а код оставить читабельным.
В Python частично решили проблему, там присваивание-как-выражение другой токен имеет, опечатку допустить риск почти нулевой. Но даже если запись другая, всё равно эту фичу лучше использовать с ограничениями.
В go-critic даже проверка есть, yodaStyleExpr, которая просит переписать в нормальную запись.
dvmedvedev
27.08.2019 10:34Есть хорошее расширение Php Inspections (EA Extended) для PhpShtorm, которое учитывает все указанные в статье проблемы и не только.
quasilyte Автор
27.08.2019 14:44phpgrep
всё же не столько про инспекции. Вы можете с помощью него упрощать рефакторинг, поиск bad practices по вашим собственным правилам. Ещё можно узнавать ответы на вопросы: "как часто в нашем проекте эта функция вызывается с определённой комбинацией аргументов?" или "есть ли у нас места, где тяжёлая для вычисления функция вызывается в цикле?".
Есть шанс, что позже добавлю опцию replace, чтобы не нужно было руками всё заменять.
censor2005
27.08.2019 12:29if ( preg_match( '/(wowwiki.com|wikia.com|falloutvault.com)/', $url ) ) {
По-моему, тут и экранирование не поможет, можно зарегистрировать домен falloutvault.comxxxx.comTatikoma
27.08.2019 14:20Здесь ещё хуже. Отсутствует ограничитель начала и конца строки (кстати довольно частая ошибка, нужна обязательно такая инспекция).
Здесь пройдёт дажеlalalalawikia.comlalalala.lala`'DROP TABLE students;
А вот матчить точку можеть быть не обязательно — валидных доменов без точки в этом контексте не будет. Если поставить ^$ в начале и конце строки — будет вполне достаточно, как по мне.
Экранирование точки лучше сделать через preg_quote — есть варианты повышения читабельности в этом кейсе.quasilyte Автор
27.08.2019 21:27Возможно, если регулярка уже не читабельна и при этом всё равно посредственно справляется с задачей, хорошо бы URL распарсить какой-нибудь либой и проверять домен или другую интересующую часть через switch/мапу/ifelse.
Я не эксперт в PHP, но в Go есть url.Parse, например.
u, err := url.Parse(urlString) if err != nil { // URL не валиден. } switch u.Host { case "blah.com", "foo.com": // Good. default: // Опционально обработать остальные урлы. }
Если "слишком много кода", то можно это в функцию завернуть.
ghost404
28.08.2019 11:26В PHP это делается так
$allowed_hosts = ['wowwiki.com', 'wikia.com', 'falloutvault.com']; if (in_array(parse_url($url, PHP_URL_HOST), $allowed_hosts)) { // ... }
tendium
28.08.2019 11:36-1У вас третий параметр потерялся :D
in_array(parse_url($url, PHP_URL_HOST), $allowed_hosts, true)
(Да, я знаю, что он опциональный.)
Но и это еще не всё. Читаем документацию:
On seriously malformed URLs, parse_url() may return FALSE.
Поэтому код надо переписать:
$allowed_hosts = ['wowwiki.com', 'wikia.com', 'falloutvault.com']; $host = parse_url($url, PHP_URL_HOST); if ($host !== false && in_array($host, $allowed_hosts, true)) { // ... }
Принимайте мой PR :D
P.S. На случай $host === false можно отдельно поведение навесить при необходимости.ghost404
28.08.2019 11:55В изначальном коде не выполнялась проверка на корректность URL и я полагаю, что валидация URL была сделана где-то ранее. То есть, задача этой строчки кода только проверить допустимый домен в URL, а не проверять корректность URL.
И здесь неважен strict mode и false который может вернуть функция
parse_url()
. В этом примереin_array()
всегда будет возвращать false для таких случаев.
ghost404
28.08.2019 12:03Я безусловно согласен с тем, что использование strict mode по умолчанию является хорошей практикой, но в данном случае и без него код будет работать корректно.
И вы забыли еще об одной детали:
If the requested component doesn't exist within the given URL, NULL will be returned.
tendium
28.08.2019 15:01В изначальном коде не выполнялась проверка на корректность URL
Если вы о go-коде, то она там таки выполнялась.
u, err := url.Parse(urlString) if err != nil { // URL не валиден. }
И вы забыли еще об одной детали
Да, вы правы. И это надо тоже учесть.
В этом примере in_array() всегда будет возвращать false для таких случаев.
Ключевое здесь «в этом примере». Если не делать необходимые проверки, не использовать повсеместно строгий режим (полагаясь на то, что «конкретно тут это не важно»), то рано или поздно оно вернется бумерангом в самый неожиданный момент (достаточно, чтобы кто-то где-то что-то исправил и внезапно получилось, что проверка/строгий режим уже стали «конкретно тут важны»).
quasilyte Автор
28.08.2019 15:41-1Спасибо!
Я не совсем понимаю, почему в треде пошли минусы, но здорово, что такой подход в PHP требует совсем незначительного количества дополнительного кода.
Сложно судить, насколько такой подход в PHP распространён, на корпусе из статьи 709 использований, а у preg_match — 5486 (при этом не все из них разбирают URL'ы). Но вроде бы достаточно часто используется. :)
ghost404
27.08.2019 20:33Точка без звёздочки заменяет только один символ. Как сказали выше, добавление
^$
решит проблему.
reactoranime
27.08.2019 23:57Спасибо за статью! Запустил на своём проекте и был приятно удивлён что только одна ошибка и именно с случаем точки в регулярном выражении.
rsashka
Скачал, запустил анализатор на рабочих проектах и не поверил своим глазам.
Почти два экрана ошибок!
Полез разбираться, оказалось ложный шухер.
Анализатор ругается на строки, вида:
А за статью спасибо, хотя результат проверки и добавил немного адреналинчику ;-)
quasilyte Автор
Возможности такого анализа ограничены.
Это подходит для прототипирования, но не для production решения. Хотя если аккуратно написать шаблоны, должно помочь.
Скорее всего, тут сработало сравнение со строкой через
==
вместо===
. Это не очень хорошая практика.tendium
Использование нестрогого сравнения ведет вот таким вещам:
Может, конечно, конкретно в вашем случае это не так важно, но в общем случае это источник неприятностей.