Предупреждение EVAL и его последствия

Предупреждение EVAL появляется в Rollup в тех случаях, когда анализируемый код содержит вызовы функции eval() или конструкции, работающие аналогичным образом. Такое предупреждение сигнализирует о том, что сборщик столкнулся с динамическим выполнением строкового JavaScript-кода и не способен полностью проанализировать последствия такого вызова на этапе сборки.

Пример:

const code = 'console.log("Hello")';

eval(code);

При обработке такого файла Rollup может вывести предупреждение вида:

(!) Use of eval is strongly discouraged
https://rollupjs.org/troubleshooting/#avoiding-eval
src/main.js
1: eval(code)

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


Почему Rollup предупреждает об использовании eval

Основная задача Rollup заключается в статическом анализе модулей.

Сборщик пытается заранее определить:

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

Функция eval() нарушает этот подход.

Рассмотрим пример:

const variableName = 'user';

eval(`console.log(${variableName})`);

Для человека результат достаточно очевиден, однако для Rollup содержимое строки является обычным текстом. Сборщик не может гарантированно определить:

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

Фактически после появления eval() статический анализ становится частично невозможным.


Как работает eval

Функция принимает строку и интерпретирует её как JavaScript-код.

Пример:

eval('const x = 10');

Эквивалентно:

const x = 10;

Более сложный пример:

const operation = '+';

const result = eval(`5 ${operation} 10`);

console.log(result);

Результат:

15

Код создаётся динамически во время выполнения программы.

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


Основные последствия использования eval

Нарушение tree shaking

Одним из главных преимуществ Rollup является агрессивное удаление неиспользуемого кода.

Пример:

export function used() {
    console.log('used');
}

export function unused() {
    console.log('unused');
}

Если импортируется только функция used, Rollup удалит функцию unused.

Однако при наличии eval ситуация меняется.

eval('unused()');

Теперь сборщик уже не может уверенно определить, действительно ли функция не используется.

В результате:

  • часть неиспользуемого кода может остаться в бандле;
  • эффективность tree shaking снижается;
  • размер итогового файла увеличивается.

Потеря возможности переименования переменных

Во время оптимизации Rollup может изменять внутренние имена переменных.

Пример:

const longVariableName = 123;

После минификации:

const a = 123;

Если же где-то используется:

eval('console.log(longVariableName)');

Переименование может привести к поломке логики.

Поэтому инструменты минификации вынуждены быть осторожнее при обработке подобных участков.


Непредсказуемое поведение после сборки

Рассмотрим пример:

const moduleName = 'UserModule';

eval(`loadModule("${moduleName}")`);

Во время разработки всё может работать корректно.

После:

  • минификации;
  • объединения файлов;
  • изменения области видимости;
  • преобразования модулей;

результат выполнения может отличаться от ожидаемого.

Чем сложнее сборка, тем выше риск возникновения трудноуловимых ошибок.


Ухудшение производительности

JavaScript-движок оптимизирует обычный код заранее.

Для eval() ситуация иная.

Пример:

for (let i = 0; i < 10000; i++) {
    eval('i + 1');
}

На каждой итерации строка должна:

  1. быть разобрана;
  2. быть преобразована в синтаксическое дерево;
  3. быть скомпилирована;
  4. быть выполнена.

Обычный код работает заметно быстрее.

Поэтому использование eval часто негативно влияет на производительность приложения.


Влияние на безопасность

Одной из главных причин негативного отношения к eval является безопасность.

Опасный пример:

const userInput = getUserInput();

eval(userInput);

Если пользователь введёт:

alert(document.cookie)

код будет выполнен.

Если приложение работает в браузере, злоумышленник может:

  • похищать данные;
  • выполнять произвольный код;
  • изменять содержимое страницы;
  • выполнять XSS-атаки.

По этой причине многие политики безопасности прямо запрещают использование eval.


Проблемы с Content Security Policy

Многие современные сайты используют механизм CSP (Content Security Policy).

Типичная политика:

Content-Security-Policy:
script-src 'self'

В подобных условиях использование eval() может быть заблокировано браузером.

Ошибка выглядит примерно так:

Refused to evaluate a string as JavaScript because
'unsafe-eval' is not an allowed source.

Для разрешения работы пришлось бы добавить:

unsafe-eval

Однако это существенно ослабляет защиту приложения.

Поэтому корпоративные проекты часто полностью запрещают eval.


Косвенные формы eval

Не всегда проблема связана с прямым вызовом функции.

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

Конструктор Function

Пример:

const sum = new Function(
    'a',
    'b',
    'return a + b'
);

Фактически происходит динамическая компиляция кода.

Хотя Rollup обычно отдельно не маркирует такой код как eval, проблемы остаются схожими.


Динамическая генерация выражений

Пример:

const expression = '5 + 10';

const result = Function(
    `return ${expression}`
)();

Код создаётся во время выполнения.

Статический анализ становится невозможным.


Типичные источники предупреждения EVAL

Старые библиотеки

Некоторые устаревшие пакеты активно используют динамическое выполнение кода.

Особенно часто это встречается в:

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

Пример зависимости:

eval(compiledTemplate);

Даже если собственный код не содержит eval, предупреждение может появляться из-за стороннего пакета.


Генераторы шаблонов

Некоторые шаблонизаторы компилируют шаблоны в строки JavaScript.

Например:

const renderer = eval(generatedCode);

Подобная архитектура была распространена до появления современных инструментов сборки.


Legacy-код

В старых проектах можно встретить конструкции:

eval('obj.' + propertyName);

или

eval('myFunction()');

Такие решения часто создавались до появления современных возможностей JavaScript.


Как найти источник предупреждения

Rollup обычно показывает путь к файлу.

Пример:

(!) Use of eval is strongly discouraged
node_modules/legacy-lib/index.js

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

grep -r "eval(" node_modules

или выполнить поиск по проекту:

rg "eval\("

Результат позволяет быстро определить проблемный участок.


Замена eval на безопасные альтернативы

Использование доступа по ключу

Вместо:

eval('user.name');

лучше:

user.name

или:

user['name']

Использование объектов-таблиц

Вместо:

eval(functionName + '()');

лучше:

const handlers = {
    create,
    update,
    remove
};

handlers[functionName]();

Такой код:

  • безопаснее;
  • быстрее;
  • понятнее;
  • корректно анализируется Rollup.

Использование switch

Вместо:

eval(command);

можно использовать:

switch (command) {
    case 'start':
        start();
        break;

    case 'stop':
        stop();
        break;
}

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

Вместо генерации имени модуля через eval:

eval(`import("${moduleName}")`);

следует использовать:

await import(`./modules/${moduleName}.js`);

Динамический импорт поддерживается Rollup значительно лучше и позволяет создавать отдельные чанки.


Когда предупреждение можно игнорировать

Иногда предупреждение появляется внутри сторонней зависимости.

Например:

node_modules/some-library

Если библиотека:

  • давно используется;
  • работает корректно;
  • не влияет на безопасность проекта;
  • не вызывает проблем при сборке;

предупреждение можно оставить без изменений.

Однако рекомендуется понимать источник проблемы и её потенциальные последствия.

Игнорирование предупреждения допустимо только после анализа того, почему используется eval и какие риски это создаёт.


Подавление предупреждения

При необходимости предупреждение можно фильтровать через обработчик onwarn.

Пример:

export default {
    input: 'src/main.js',

    onwarn(warning, warn) {
        if (warning.code === 'EVAL') {
            return;
        }

        warn(warning);
    }
};

После этого сообщения типа EVAL не будут отображаться.

Следует учитывать, что фильтрация скрывает предупреждение, но не устраняет саму проблему.


Практические рекомендации

Использование eval() в современных проектах считается нежелательной практикой.

Основные причины:

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

Для проектов на Rollup предпочтительно использовать:

  • обычные вызовы функций;
  • объекты-диспетчеры;
  • динамические импорты;
  • словари обработчиков;
  • механизмы рефлексии JavaScript без выполнения строкового кода.

В большинстве случаев любой сценарий применения eval() может быть реализован более безопасным, быстрым и предсказуемым способом, что позволяет Rollup выполнять оптимизации максимально эффективно.