Для получения триального ключа
заполните форму ниже
Team license
Enterprise license
** Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

Запросите информацию о ценах
Новая лицензия
Продление лицензии
--Выберите валюту--
USD
EUR
RUB
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

Бесплатная лицензия PVS-Studio для специалистов Microsoft MVP
** Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

Для получения лицензии для вашего открытого
проекта заполните, пожалуйста, эту форму
** Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

Мне интересно попробовать плагин на:
** Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

Ваше сообщение отправлено.

Мы ответим вам на


Если вы так и не получили ответ, пожалуйста, проверьте папку
Spam/Junk и нажмите на письме кнопку "Не спам".
Так Вы не пропустите ответы от нашей команды.

>
>
Подавление ложноположительных предупреж…
Проверка проектов
Сообщения PVS-Studio
Диагностики общего назначения (General Analysis, C++)
Диагностики общего назначения (General Analysis, C#)
Диагностики общего назначения (General Analysis, Java)
Диагностика микро-оптимизаций (C++)
Диагностика 64-битных ошибок (Viva64, C++)
Реализовано по запросам пользователей (C++)
Cтандарт MISRA
Стандарт AUTOSAR
Стандарт OWASP (C#)
Проблемы при работе анализатора кода
Дополнительная информация
Оглавление

Подавление ложноположительных предупреждений

29 Июл 2022

В данном разделе описаны механизмы подавления ложноположительных предупреждений, выдаваемых анализатором. С помощью описанных здесь механизмов можно управлять как отдельными сообщениями, выдаваемыми на определённые строки кода, так и подавлять множественные срабатывания, возникающие, например, из-за использования C/C++ макросов. Описан способ, как с помощью комментариев указать анализатору выключить то или иное сообщение анализатора или изменить выдаваемый текст сообщения.

Механизмы, описанные в данном разделе, применимы как для C/C++, так и для C# анализаторов PVS-Studio, если явно не указано обратное.

Смотри, а не читай (YouTube)

Механизм подавления отдельных ложных срабатываний (Mark as False Alarm)

Любой анализатор кода всегда выдает помимо полезных сообщений об ошибках еще множество так называемых "ложных срабатываний". Это ситуации, когда программисту совершенно очевидно, что в коде нет ошибки, а анализатору это не очевидно. Такие ложные срабатывания называют False Alarm. Рассмотрим пример кода:

obj.specialFunc(obj);

Анализатор считает подозрительным, что у объекта вызывается метод, в качестве аргумента в который передаётся тот же самый объект, поэтому он выдаст на данный код предупреждение V678. Программист же может знать, что использование метода 'specialFunc' таким образом вполне допустимо, поэтому предупреждение анализатора в данном случае является ложным срабатыванием. О том, что предупреждение V678, выданное на этот код, является ложным, можно сообщить анализатору.

Это можно сделать либо вручную, либо с помощью команды контекстного меню. По умолчанию, ложные срабатывания не отображаются в итоговом отчете или плагине. Для включения отображения размеченных подобным образом сообщений можно воспользоваться настройкой 'PVS-Studio -> Options... -> Specific Analyzer Settings -> DisplayFalseAlarms'.

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

Ручное подавление ложных срабатываний

Обычно в компиляторах для подавления отдельных сообщений об ошибках используют '#pragma'-директивы. Приведем пример кода:

unsigned arraySize = n * sizeof(float);

Компилятор выдает сообщение:

warning C4267: 'initializing' : conversion from 'size_t' to 'unsigned int', possible loss of data x64Sample.cpp 151

Это сообщение можно подавить с помощью следующей конструкции:

#pragma warning (disable:4267)

Точнее, чтобы подавить конкретно это сообщение, лучше оформить код так:

#pragma warning(push)
#pragma warning (disable:4267) 
  unsigned arraySize = n * sizeof(float);
#pragma warning(pop)

Анализатор PVS-Studio в качестве разметки использует комментарии специального вида. Для той же строчки кода подавление сообщения PVS-Studio будет выглядеть так:

unsigned arraySize = n * sizeof(INT_PTR); //-V103

Теперь анализатор пометит предупреждение V103 на эту строку как ложное. Такой подход был выбран для повышения наглядности конечного кода. Дело в том, что PVS-Studio может сообщать о проблемах в середине многострочных выражений, как, например, здесь:

size_t n = 100;
for (unsigned i = 0;
     i < n;          // <= анализатор сообщит о проблеме здесь
     i++)
{
    // ...
}

Чтобы подавить это сообщение при использовании комментария, достаточно написать:

size_t n = 100;
for (unsigned i = 0;
     i < n;          //-V104
     i++)
{
    // ...
}

Если же в это выражение пришлось бы добавлять '#pragma'-директиву, то код выглядел бы значительно менее наглядно.

Хранение разметки в исходном коде позволяет вносить в него модификации без опасения потерять информацию о строках с ошибками.

Можно также использовать отдельную базу, в которой хранить информацию примерно так: код ошибки, имя файла, номер строки. Данный подход отдельно реализован в PVS-Studio и называется "Mass Suppression".

Подавление ложных срабатываний через контекстное меню плагинов

Для работы с ложными срабатываниями пользователю предоставляется две команды, доступные из контекстного меню PVS-Studio (рисунок 1).

SuppressionFalseAlarm_ru/image1.webp

Рисунок 1 - Команды для работы с механизмом подавления ложных предупреждений

Рассмотрим команды, относящиеся к подавлению ложных предупреждений:

1. Mark selected messages as False Alarms. Вы можете выбрать одно или несколько предупреждений в списке (рисунок 2) и воспользоваться этой командой для разметки соответствующего кода, как безопасного.

SuppressionFalseAlarm_ru/image2.webp

Рисунок 2 - Выбор предупреждений перед выполнением команды "Mark selected messages as False Alarms"

2. Remove False Alarm marks from selected messages. Убирает комментарий, помечающий код как безопасный. Функция, например, может быть полезна, если вы поспешили и ошибочно отметили код как безопасный. Как и в предыдущем случае, вы должны выбрать сообщения из списка, которые планируете обработать.

Подавление ложных предупреждений в С/С++ макросах (#define) и для других фрагментов кода

В макросах (#define) анализатор также, разумеется, может находить потенциальные проблемы и выдавать на них диагностические сообщения. Но при этом анализатор будет выдавать сообщения в тех местах, где макрос используется, то есть где фактически происходит подстановка тела макроса в код. Пример:

#define TEST_MACRO \
  int a = 0;       \
  size_t b = 0;    \
  b = a; 

void func1()
{
  TEST_MACRO // V1001 here
}

void func2()
{
  TEST_MACRO // V1001 here
}

Чтобы подавить это сообщение, можно использовать команду "Mark as False Alarm". Тогда код с расставленными командами подавления будет выглядеть так:

#define TEST_MACRO \
  int a = 0;       \
  size_t b = 0;    \
  b = a; 

void func1()
{
  TEST_MACRO //-V1001
}

void func2()
{
  TEST_MACRO //-V1001
}

Однако, если макрос используется очень активно, то везде размечать его как False Alarm не очень удобно. Есть возможность в коде сделать вручную специальную пометку, чтобы анализатор автоматически размечал диагностики в этом макросе как False Alarm. С этой пометкой код будет выглядеть так:

//-V:TEST_MACRO:1001

#define TEST_MACRO \
  int a = 0;       \
  size_t b = 0;    \
  b = a; 

void func1()
{
  TEST_MACRO
}

void func2()
{
  TEST_MACRO
}

При проверке такого кода сообщения о проблемах в макросе уже сразу будут помечены как False Alarm. Причем можно указывать несколько диагностик сразу, через запятую:

//-V:TEST_MACRO:1001, 105, 201

Обратите внимание, что если макрос содержит вложенные макросы, то для автоматической пометки нужно указывать все равно имя макроса самого верхнего уровня:

#define NO_ERROR 0
#define VB_NODATA ((long)(77))
size_t stat;

#define CHECK_ERROR_STAT                        \
    if( stat != NO_ERROR &&  stat != VB_NODATA ) \
      return stat;

size_t testFunc()
{
    {
      CHECK_ERROR_STAT // #1
    }

    {
      CHECK_ERROR_STAT // #2
    }

    return VB_NODATA; // #3
}

В указанном примере диагностика V126 появляется в трех местах. Чтобы автоматически помечать ее как False Alarm в местах #1 и #2 нужно добавить такой код:

//-V:CHECK_ERROR_STAT:126

А чтобы и в #3 это сработало, необходимо указать еще:

//-V:VB_NODATA:126

К сожалению, просто указать "сразу помечать V126 в макросе VB_NODATA" и не указывать про макрос CHECK_ERROR_STAT нельзя из-за технических особенностей механизма препроцессирования.

Всё написанное в этом разделе про макросы справедливо также и для любого фрагмента кода. То есть если, например, вы хотите подавить все срабатывания диагностики V103 на вызов функции 'MyFunction', необходимо добавить такую строку:

//-V:MyFunction:103

Включение и выключение определенных диагностик для блока кода

Этот пункт относится только к анализатору языков C и C++.

Иногда нужно отключить некоторое правило не глобально, а для определенной части кода. Например, нужно отключить диагностику для определенного файла или части файла, при этом остальные диагностики должны продолжать работать. Подавленное предупреждение должно продолжать выдаваться при анализе остального кода.

Анализатор предоставляет механизм подавления с использованием специальных директив 'pragma'. Этот способ аналогичен тому, который используется в компиляторе для управления предупреждениями.

Анализатор использует следующие директивы:

  • #pragma pvs(push) – сохраняет текущие настройки включения/отключения диагностик;
  • #pragma pvs(disable: XXXX, YYYY, ...) – выключает диагностики с номерами из списка;
  • #pragma pvs(enable: XXXX, YYYY, ...) – включает диагностики с номерами из списка;
  • #pragma pvs(pop) – восстанавливает предыдущие сохраненные настройки.

Так же, как и в случае с '#pragma warning', поддерживается вложенность.

Пример:

void func(int* p1, int* p2, int* p3)
{
  if (!p1 || !p2 || !p3)
    return;

#pragma pvs(push)
#pragma pvs(disable: 547)
  if (p1) // V547 off
    do_something();

#pragma pvs(push)
#pragma pvs(enable: 547)
  if (p2) // V547 Expression 'p2' is always true.
    do_something_else();

#pragma pvs(pop)

  if (p3) // V547 off
    do_other();

#pragma pvs(pop)
}

Примечание: хотя компиляторы игнорируют неизвестные директивы 'pragma', в зависимости от настроек, они могут выдавать предупреждения о таких директивах. В этом случае предупреждение можно отключить, передав специальный параметр в командную строку компилятора:

  • для GCC и Clang: -Wno-unknown-pragmas
  • для MSVC: -wd4068

Подавление ложных предупреждений с помощью файлов конфигурации диагностик (.pvsconfig)

Отображением и фильтрацией сообщений можно управлять с помощью комментариев специального вида. Такие комментарии можно писать в специальных файлах конфигурации (.pvsconfig) для всех анализаторов, либо непосредственно в коде проекта (только для C/C++ анализатора).

Файлы конфигурации диагностик анализатора представляют собой простые текстовые файлы, добавляемые в Visual Studio проект либо solution. Для добавления файла конфигурации, выделите интересующий вас проект или solution в окне Solution Explorer среды Visual Studio и выберите пункт контекстного меню 'Add New Item...'. В появившемся окне выберите тип файла 'PVS-Studio Filters File' (рисунок 3):

SuppressionFalseAlarm_ru/image4.webp

Рисунок 3 - Добавление в solution файла конфигурации диагностик анализатора.

Из-за особенностей некоторых версий среды Visual Studio, тип файлов 'PVS-Studio Filters File' может отсутствовать на некоторых версиях и редакциях Visual Studio в окне добавления нового файла для solution и\или проекта. В таком случае, можно добавить в проект обычный текстовый файл, задав ему расширение 'pvsconfig'. В свойствах этого файла (после добавления), должно быть указано, что файл не участвует в сборке.

Файл конфигурации, добавленный в проект, действует на все файлы данного проекта. Файл конфигурации, добавленный в solution, действует на все файлы всех проектов, добавленных в данный solution.

Также можно разместить файл конфигурации .pvsconfig в текущей папке пользовательских данных (%AppData%\PVS-Studio\) - такой файл будет подхвачен автоматически при запуске проверки, без необходимости как-либо модифицировать проектные файлы.

Примечание. '.pvsconfig' файлов в '%AppData%\PVS-Studio\' может быть несколько, и все они будут автоматически подхвачены анализатором. Стоит также учитывать, что конфигурация из '%AppData%\PVS-Studio\' будет глобальна для анализатора и будет безусловно использоваться при каждом запуске.

При использовании инструмента командной строки PVS-Studio_Cmd указать путь к файлу конфигурации .pvsconfig можно через параметр --rulesConfig (-C), например:

PVS-Studio_Cmd.exe -t D:\project\project.sln 
-C D:\project\rules.pvsconfig

Файлы конфигурации диагностик .pvsconfig имеют простой синтаксис. Любая строка, начинающаяся с символа '#' считается комментарием и игнорируется. Фильтры записываются в формате однострочных C++/C# комментариев, т.е. должны начинаться с символов '//'.

Для C/C++ кода, фильтры также могут быть записаны в виде комментариев непосредственно в исходном коде. Обратите внимание, что такой формат записи не поддерживается в C# проектах!

Далее рассмотрим различные варианты фильтров, допустимых в файлах конфигурации диагностик.

Фильтрация сообщений по фрагменту исходного кода (например, имена макросов, переменных и функций)

Предположим, есть следующая структура:

struct MYRGBA
{
  unsigned data;
};

И ряд функций, которые её используют:

void f1(const struct MYRGBA aaa)
{
}

long int f2(int b, const struct MYRGBA aaa)
{
  return int();
}

long int f3(float b, const struct MYRGBA aaa,  char c)
{
  return int();
}

На все эти функции анализатор выдаст три сообщения V801: Decreased performance. It is better to redefine the N function argument as a reference. Сообщение в подобном коде будет ложным, так как компилятор сам оптимизирует код, и проблемы не будет.

Можно, конечно, каждое сообщение пометить как False Alarm с помощью функции Mark As False Alarm. Однако, есть способ лучше. Достаточно добавить в код строку:

//-V:MYRGBA:801

Для C/C++ проектов, мы рекомендуем добавлять такую строку в .h-файл рядом с объявлением структуры, но если это невозможно (например, структура в системном .h-файле), то можно прописать это в stdafx.h.

И тогда, после перепроверки, все три сообщения V801 будут автоматически помечены как False Alarm.

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

Рассмотрим несколько примеров:

//-V:<<:128

Подавит предупреждения V128 в строках, где имеется оператор <<.

buf << my_vector.size();

Если вы хотите подавлять предупреждение V128 только при записи данных в объект с именем 'log', то можно написать так:

//-V:log<<:128
buf << my_vector.size(); // Есть предупреждение
log << my_vector.size(); // Нет предупреждения

Примечание. Обратите внимание, что строка для поиска не должна содержать пробелов.

Правильно: //-V:log<<:128
Неправильно: //-V:log <<:128

При поиске подстроки пробелы игнорируются. Но не беспокойтесь, следующая ситуация обработается корректно:

//-V:ABC:501
AB C = x == x; // Есть предупреждение
AB y = ABC == ABC; // Нет предупреждения

Полное отключение предупреждений

Существует возможность с помощью комментария полностью выключить предупреждение. В этом случае после двух двоеточий указывается номер отключаемой диагностики. Синтаксис:

//-V::(number)

Если требуется проигнорировать предупреждение V122, то можно указать в начале файла:

//-V::122

Для отключения нескольких диагностик можно перечислить их номера через запятую. Синтаксис:

//-V::(number1),(number2),...,(numberN)

Если требуется, например, игнорировать предупреждения V502, V507 и V525, то в начале файла можно указать:

//-V::502,507,525

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

//-V::(number1),(number2),...,(numberN):1,2,3

Например, если требуется игнорировать предупреждения V3161 и V3165 на уровнях 'Medium' и 'Low', можно указать:

//-V::3161,3165:2,3

C# анализатор также поддерживает возможность фильтрации предупреждений по номеру диагностики и подстроке. Синтаксис:

//-V::(number1),(number2),...,(numberN)::{substring}

Например, можно исключить из отчёта все предупреждения V3022 и V3063, содержащие подстроку "always true":

//-V::3022,3063::{always true}

Этот функционал можно комбинировать с фильтрацией по уровню:

//-V::(number1),(number2),...,(numberN):1,2,3:{substring}

Например, можно исключить все срабатывания V5625, имеющие 2 уровень и содержащие подстроку "Google.Protobuf 3.6.1":

//-V::5625:2:{Google.Protobuf 3.6.1}

Существует также возможность отключить группу диагностик. Синтаксис:

//-V::GA
//-V::X64
//-V::OP
//-V::CS
//-V::MISRA

Для отключения сразу нескольких групп диагностик их можно перечислить через запятую. Синтаксис:

//-V::X64,CS,...

Для отключения всех диагностик C++ или C# анализатора следует использовать следующую форму:

//-V::C++
//-V::C#

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

Исключение из анализа файлов по маскам

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

Несколько примеров масок:

//V_EXCLUDE_PATH C:\TheBestProject\thirdParty
//V_EXCLUDE_PATH *\UE4\Engine\*
//V_EXCLUDE_PATH *.autogen.cs

Синтаксис масок идентичен синтаксису для опций 'FileNameMasks' и 'PathMasks', описанному в документе "Настройки: Don't Check Files".

Игнорирование глобальных файлов конфигурации

Перед запуском анализа 'PVS-Studio_Cmd' формирует конфигурацию диагностических правил из:

  • глобальных файлов в '%AppData%\PVS-Studio\';
  • файла, переданного через опцию --rulesConfig (-C);
  • файлов, добавленных в решение;
  • файлов, добавленных в проект.

Если у вас большое количество проектов, для которых конфигурация формируется разными способами, то в некоторых случаях конфигурация из глобальных файлов может приводить к путанице в результирующей конфигурации. Это связано с тем, что конфигурация из глобальных файлов применяется всегда над каким бы проектом вы ни работали. Другими словами, настройки, которые характерны только для проекта X, будут также применены к другим проектам.

Поэтому, если вам необходимо проигнорировать конфигурацию из глобальных файлов, нужно добавить специальный флаг в соответствующий '.pvsconfig' файл:

//IGNORE_GLOBAL_PVSCONFIG

Правила действия флага следующие:

  • если указан в одном из глобальных файлов, то глобальная конфигурация будет игнорироваться всегда.
  • если указан на уровне решения, то глобальная конфигурация игнорируется для конкретного решения;
  • если указан на уровне проекта, то глобальная конфигурация игнорируется для конкретного проекта.

Использование этого флага позволит гибко выключать глобальные настройки для определенных случаев.

Другие способы фильтрации сообщений в анализаторе PVS-Studio (Detectable Errors, Don't Check Files, Keyword Message Filtering)

Возможны ситуации, в которых определённый тип диагностик не актуален для анализируемого проекта, или какая-либо из диагностик анализатора выдаёт предупреждения на код, в корректности которого вы уверены. В таком случае можно воспользоваться системой группового подавления сообщений, основанной на фильтрации полученных результатов анализа. Список доступных режимов фильтрации можно открыть через общее меню PVS-Studio -> Options.

Следует отметить, что применение фильтров для группового подавления ложных срабатываний не требует перезапуска анализа, результаты фильтрации будут сразу отображены в окне вывода PVS-Studio.

Во-первых, можно отключить диагностику тех или иных ошибок по их коду. Это делается с помощью вкладки "Настройки: Detectable Errors". На вкладке обнаруживаемых ошибок можно указать номера ошибок, которые не надо показывать в отчете по анализу. Иногда бывает целесообразно убрать в отчете ошибки с определенными кодами. Например, если вы уверены, что ошибки, связанные с явным приведением типа (коды V201, V202, V203), вас не интересуют, то вы можете скрыть их показ. Также отображение ошибок определённого типа можно отключить с использованием команды контекстного меню "Hide all Vxxx errors". Соответственно, в случае, если необходимо включить отображение обратно, настроить это можно на упоминавшейся выше вкладке "Detectable Errors".

Во-вторых, можно отключить анализ некоторых частей проекта (некоторых папок или файлов проекта). Раздел "Настройки: Don't Check Files". На этой вкладке можно ввести информацию о библиотеках, включения (через директиву #include) из файлов которых анализировать не надо. Это может потребоваться для уменьшения количества лишних диагностических сообщений. Например, в проекте используется библиотека Boost. И хотя на какой-то код из этой библиотеки анализатор выдает диагностические сообщения, вы считаете, что эта библиотека является достаточно надежной и написана хорошо. Поэтому, возможно, не имеет смысла получать диагностические сообщения по поводу кода в этой библиотеке. В этом случае можно отключить анализ файлов из этой библиотеки, указав путь к ней на странице настроек. Кроме того, возможно ввести файловые маски для исключения некоторых файлов из анализа. Анализатор не будет проверять файлы, удовлетворяющие условиям маски. Например, подобным образом можно исключить из анализа автогенерируемые файлы.

Маски путей для файлов, сообщения из которых попали в текущий сгенерированный отчёт, можно автоматически добавить в список Don't Check Files с помощью команды контекстного меню "Don't check files and hide all messages from..." для выделенного в окне PVS-Studio Output сообщения (рисунок 4).

SuppressionFalseAlarm_ru/image6.webp

Рисунок 4 - Добавление масок путей через контекстное меню

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

В-третьих, можно подавлять отдельные сообщения по тексту. На вкладке "Настройки: Keyword Message Filtering" можно настроить фильтрацию ошибок по содержащемуся в них тексту, а не по коду. При необходимости можно скрыть из отчета сообщения о диагностированных ошибках, содержащих определенные слова или фразы. Например, если в отчете есть ошибки, в которых указаны названия функций printf и scanf, а вы считаете, что ошибок, связанных с ними, быть не может, то просто добавьте эти два слова с помощью редактора подавляемых сообщений.

Массовое подавление сообщений анализатора (baselining)

Иногда, особенно на стадии внедрения статического анализа в крупных проектах, может возникнуть необходимость 'подавить' все предупреждения анализатора на имеющуюся кодовую базу, т.к. разработчики могут не иметь необходимых ресурсов для исправления найденных анализатором ошибок в старом коде. В таком случае может быть полезно 'скрыть' все предупреждения, выданные на имеющийся код, чтобы отслеживать только вновь появляющиеся ошибки. Этого можно достичь за счёт использования механизма "массового подавления сообщений анализатора". Использование соответствующего механизма в среде Windows описано в документе "Массовое подавление сообщений анализатора", в среде Linux – в соответствующем разделе документа "Как запустить PVS-Studio в Linux".

Возможные проблемы

В редких случаях автоматически расставленные разметки могут быть поставлены не в том месте, где должны быть. И тогда анализатор вновь выдаст эти же сообщения об ошибках, так как маркер не будет найден. Это проблема препроцессора, связанная с многострочными #pragma-директивами определенного типа, из-за которых также сбивается нумерация строк. Решением проблемы является пометка сообщений, на которых заметен сбой, вручную. PVS-Studio всегда сообщает о подобных ошибках сообщением "V002. Some diagnostic messages may contain incorrect line number".

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

Unicorn with delicious cookie
Мы используем куки, чтобы пользоваться сайтом было удобно. Хотите узнать подробнее?
Принять