Моя самая любимая часть в статическом анализе кода — это выдвижение гипотез о потенциальных ошибках в коде с последующей их проверкой.


Пример гипотезы:


Функции strpos легко передать аргументы в неправильном порядке. 

Но есть вероятность, что даже на нескольких миллионах строк кода подобная диагностика не "выстрелит", поэтому на неудачные гипотезы тратить много времени не хочется.


Сегодня я покажу как выполнять простейший статический анализ с помощью утилиты phpgrep без написания кода.



Под катом:


  • Поиск и разбор багов в open source проектах.
  • Quick start по phpgrep.
  • Принцип работы синтаксического поиска.




Предпосылки


Вот уже несколько месяцев я занимаюсь поддержкой PHP линтера NoVerify (почитать о котором можно в статье NoVerify: линтер для PHP от Команды ВКонтакте).


Время от времени в команде появляются идеи для новых диагностик. Идей может быть много, а проверить хочется все, особенно если предложенная проверка нацелена на выявление критических дефектов.


Ранее я активно разрабатывал go-critic и ситуация была схожей, с той лишь разницей, что анализировались исходники на Go, а не на PHP. Когда я узнал об утилите gogrep, мой мир перевернулся. Как видно из названия, эта утилита имеет что-то общее с grep'ом, только поиск производится не по регулярным выражениям, а по синтаксическим шаблонам (позже объясню, что это значит).


Я уже не хотел жить без умного grep, поэтому в один вечер решил сесть и написать phpgrep.


Анализируемый корпус


Чтобы было увлекательно, мы сразу погрузимся в применение. Будем анализировать небольшой набор достаточно известных и крупных PHP проектов, доступных на GitHub.


В наш набор попали следующие проекты:



Для людей, которые замышляют то, что мы с вами замышляем, это очень аппетитный набор.


Итак, поехали!


Использование присваивания в качестве выражения


Если присваивание используется как выражение, причём:


  • контекст ожидает результат логической операции (логическое условие) и
  • правая часть выражения не имеет побочных эффектов и является константной,

то, скорее всего, в коде ошибка.


Для начала, возьмём за "логический контекст" следующие конструкции:


  1. Выражение внутри "if ($cond)".
  2. Условие тернарного оператора: "$cond ? $x : $y".
  3. Условия продолжений циклов "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

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)


  1. rsashka
    26.08.2019 17:53

    Скачал, запустил анализатор на рабочих проектах и не поверил своим глазам.
    Почти два экрана ошибок!
    Полез разбираться, оказалось ложный шухер.
    Анализатор ругается на строки, вида:

    $format_12_hour = $element['#hourformat'] == '12-hour'
    и 
    while ($percentage == '100')

    А за статью спасибо, хотя результат проверки и добавил немного адреналинчику ;-)


    1. quasilyte Автор
      26.08.2019 17:57

      Возможности такого анализа ограничены.


      Это подходит для прототипирования, но не для production решения. Хотя если аккуратно написать шаблоны, должно помочь.


      while ($percentage == '100')

      Скорее всего, тут сработало сравнение со строкой через == вместо ===. Это не очень хорошая практика.


    1. tendium
      26.08.2019 18:26
      +1

      Использование нестрогого сравнения ведет вот таким вещам:

      $element['#hourformat'] = 12;
      $format_12_hour = $element['#hourformat'] == '12-hour';
      var_dump($format_12_hour); // bool(true)
      

      Может, конечно, конкретно в вашем случае это не так важно, но в общем случае это источник неприятностей.


  1. ghost404
    27.08.2019 08:15

    Есть лайфхак Yoda conditions. Он помогает избежать ошибок в операциях сравнения, хотя мне такой стиль записи условий не нравится.


    1. berezuev
      27.08.2019 09:37

      PSR-2 не рекомендует к использованию йода-стайл. Обусловлено лучшей читаемостью.


      1. ghost404
        27.08.2019 20:27

        До недавнего времени, он был по умолчанию включён в php-cs-fixer и Style CI. Сейчас уже исправились.


    1. quasilyte Автор
      27.08.2019 14:39

      В языках, где = не является выражением, yoda style является нежелательным. :)
      От этого обидно, что в PHP приходится такое использовать как защиту от ошибки. Но есть линтеры, которые найдут подозрительные применения присваиваний, так что иногда проще сделать линтеры на CI построже, а код оставить читабельным.


      В Python частично решили проблему, там присваивание-как-выражение другой токен имеет, опечатку допустить риск почти нулевой. Но даже если запись другая, всё равно эту фичу лучше использовать с ограничениями.


      В go-critic даже проверка есть, yodaStyleExpr, которая просит переписать в нормальную запись.


  1. dvmedvedev
    27.08.2019 10:34

    Есть хорошее расширение Php Inspections (EA Extended) для PhpShtorm, которое учитывает все указанные в статье проблемы и не только.


    1. quasilyte Автор
      27.08.2019 14:44

      phpgrep всё же не столько про инспекции. Вы можете с помощью него упрощать рефакторинг, поиск bad practices по вашим собственным правилам. Ещё можно узнавать ответы на вопросы: "как часто в нашем проекте эта функция вызывается с определённой комбинацией аргументов?" или "есть ли у нас места, где тяжёлая для вычисления функция вызывается в цикле?".


      Есть шанс, что позже добавлю опцию replace, чтобы не нужно было руками всё заменять.


  1. censor2005
    27.08.2019 12:29

    if ( preg_match( '/(wowwiki.com|wikia.com|falloutvault.com)/', $url ) ) {


    По-моему, тут и экранирование не поможет, можно зарегистрировать домен falloutvault.comxxxx.com


    1. Tatikoma
      27.08.2019 14:20

      Здесь ещё хуже. Отсутствует ограничитель начала и конца строки (кстати довольно частая ошибка, нужна обязательно такая инспекция).

      Здесь пройдёт даже lalalalawikia.comlalalala.lala`'DROP TABLE students;

      А вот матчить точку можеть быть не обязательно — валидных доменов без точки в этом контексте не будет. Если поставить ^$ в начале и конце строки — будет вполне достаточно, как по мне.

      Экранирование точки лучше сделать через preg_quote — есть варианты повышения читабельности в этом кейсе.


      1. 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:
          // Опционально обработать остальные урлы.
        }

        Если "слишком много кода", то можно это в функцию завернуть.


        1. 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)) {
              // ...
          }


          1. 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 можно отдельно поведение навесить при необходимости.


            1. ghost404
              28.08.2019 11:55

              В изначальном коде не выполнялась проверка на корректность URL и я полагаю, что валидация URL была сделана где-то ранее. То есть, задача этой строчки кода только проверить допустимый домен в URL, а не проверять корректность URL.


              И здесь неважен strict mode и false который может вернуть функция parse_url(). В этом примере in_array() всегда будет возвращать false для таких случаев.


            1. ghost404
              28.08.2019 12:03

              Я безусловно согласен с тем, что использование strict mode по умолчанию является хорошей практикой, но в данном случае и без него код будет работать корректно.


              И вы забыли еще об одной детали:


              If the requested component doesn't exist within the given URL, NULL will be returned.


              1. tendium
                28.08.2019 15:01

                В изначальном коде не выполнялась проверка на корректность URL

                Если вы о go-коде, то она там таки выполнялась.

                u, err := url.Parse(urlString)
                if err != nil {
                  // URL не валиден.
                }


                И вы забыли еще об одной детали

                Да, вы правы. И это надо тоже учесть.

                В этом примере in_array() всегда будет возвращать false для таких случаев.

                Ключевое здесь «в этом примере». Если не делать необходимые проверки, не использовать повсеместно строгий режим (полагаясь на то, что «конкретно тут это не важно»), то рано или поздно оно вернется бумерангом в самый неожиданный момент (достаточно, чтобы кто-то где-то что-то исправил и внезапно получилось, что проверка/строгий режим уже стали «конкретно тут важны»).


          1. quasilyte Автор
            28.08.2019 15:41
            -1

            Спасибо!


            Я не совсем понимаю, почему в треде пошли минусы, но здорово, что такой подход в PHP требует совсем незначительного количества дополнительного кода.


            Сложно судить, насколько такой подход в PHP распространён, на корпусе из статьи 709 использований, а у preg_match — 5486 (при этом не все из них разбирают URL'ы). Но вроде бы достаточно часто используется. :)


    1. MaximChistov
      27.08.2019 14:29

      del


    1. ghost404
      27.08.2019 20:33

      Точка без звёздочки заменяет только один символ. Как сказали выше, добавление ^$ решит проблему.


  1. reactoranime
    27.08.2019 23:57

    Спасибо за статью! Запустил на своём проекте и был приятно удивлён что только одна ошибка и именно с случаем точки в регулярном выражении.