Я пытаюсь сравнить разные линтеры и их производительность с помощью специальных правил. Ast-grep перспективен и, вероятно, работает намного быстрее, чем eslint, поскольку он написан на Rust, а не на JavaScript. Однако у eslint имеется масса установленных правил, а у ast-grep… не так много. Итак, я пытаюсь получить некоторый опыт и найти общий язык между двумя инструментами, написав типичное правило из каталога eslint: «предпочитаю-объект-распространение».
В общем, «prefer-object-spread» преобразует
Object.assign({},defaultConfig,customizedConfig)
к
{...defaultConfig,...customizedConfig}
В основном я реализовал это правило в ast-grep, но мне трудно сопоставить аргументы верхнего уровня (defaultConfig
и customizedConfig
), не сопоставляя при этом всех их потомков. В приведенном выше простом примере нет узлов-потомков, но рассмотрим следующий пример:
Object.assign({},defaultConfigGenerator(16))
или хуже
Object.assign({},Object.assign(a,b))
Я получаю результат типа {...defaultConfigGenerator(16),...16}
, потому что я сопоставляю узел-потомок 16, а также его предка. Я не могу понять, как использовать метапеременную для ссылки только на узлы, которые являются прямыми дочерними элементами узла arguments
, а не ни одним из их потомков.
Я попробовал следовать примеру https://dev.to/herrington_darkholme/find-patch-a-novel-functional-programming-like-code-rewrite-scheme-3964, где мы можем сопоставить метапеременную с несколько узлов AST и преобразуйте каждый из них.
В примере показано, как выполнить это преобразование.
// from
import {a, b, c} from './barrel';
// to
import a from './barrel/a';
import b from './barrel/b';
import c from './barrel/c';
С помощью этого правила as-grep:
# Example barrel import rewrite rule
rule:
pattern: import {$$$IDENTS} from './barrel'
rewriters:
- id: rewrite-identifer
rule:
pattern: $IDENT
kind: identifier
fix: import $IDENT from './barrel/$IDENT'
transform:
IMPORTS:
rewrite:
rewriters: [rewrite-identifer]
source: $$$IDENTS
joinBy: "\n"
fix: $IMPORTS
Итак, прочитав этот пример много раз, я пытался применить те же принципы, чтобы использовать метапеременную, соответствующую аргументам Object.assign
, и преобразовывать их.
# First part of my rule
rule:
pattern: Object.assign($FIRST_ARG_OBJ_LITERAL,$$$REMAINING_ARGS)
has:
kind: object
stopBy: end
pattern: $FIRST_ARG_OBJ_LITERAL
На данный момент это соответствует правильным узлам и даже правильно обеспечивает то, что первый аргумент должен быть литералом объекта. Остальная часть кода посвящена попытке правильно изменить совпадающие узлы. Вот:
# Second part of my rule
rewriters:
- id: remove_empty_obj
rule:
kind: object
not:
has:
pattern: $ANYCONTENTS
fix:
""
- id: add_ellipsis
rule:
pattern: $ANYCONTENTS
fix:
...$ANYCONTENTS,
- id: argument_spreader
rule:
pattern: $ANY
inside:
kind: arguments
inside:
kind: call_expression
pattern: Object.assign($$$REMAINING_ARGS)
fix:
"...$ANY"
transform:
OPTIONAL_FIRST_OBJ:
rewrite:
rewriters: [remove_empty_obj,add_ellipsis]
source: $FIRST_ARG_OBJ_LITERAL
SPREAD_REMAINDERS:
rewrite:
rewriters: [argument_spreader]
source: $$$REMAINING_ARGS
joinBy: ","
fix:
"{$OPTIONAL_FIRST_OBJ$SPREAD_REMAINDERS}"
Рерайтеры remove_empty_object
и add_ellipsis
просто помогают справиться с первым аргументом. Моя проблема связана с $$$REMAINING_ARGS
, которые подаются в argument_spreader
переписчик. Я ожидал, что $$$REMAINING_ARGS
будет состоять только из братьев и сестер первого аргумента, но потомки этих узлов также включены.
Я могу уменьшить некоторые из этих лишних узлов, настаивая на том, что мы должны сопоставлять только те узлы, которые являются arguments
и даже arguments
в вызове Object.assign
, но это не обрабатывает случай, если у нас есть вложенные Object.assign({},Object.assign(a,b))
. Кажется, нет никакой связи между переменной $$$REMAINING_ARGS
внутри argument_spreader
и остальной частью правила. В противном случае, я думаю, это сработало бы.
Вы можете поиграть с этим правилом на игровой площадке ast-grep, чтобы проверить любую свою идею.
Переписчик 🔁 от ast-grep — это новая экспериментальная функция! Спасибо за попытку, и это вполне естественно, если вы не можете в этом разобраться. Давайте разберем проблему и посмотрим, как это можно сделать.
Во-первых, давайте уточним нашу цель. Если в JavaScript есть вызов Object.assign
, использующий литерал объекта в качестве первого аргумента, мы хотим изменить его на синтаксис расширения объекта.
Приведенный ниже шаблон может достичь этой цели.
rule:
pattern: Object.assign({$$$FIELDS}, $$$REMAINING_ARGS)
В приведенном выше шаблоне используется то, что {$$$FIELDS}
— это выражение, которое может быть легко проанализировано с помощью Tree-Sitter. Он соответствует только литералу объекта, а поля внутри фиксируются метапеременной $FIELDS
.
Теперь попробуем добавить add_ellipsis
rewriters:
- id: add_ellipsis
rule:
pattern: $ANYCONTENTS
fix:
...$ANYCONTENTS,
а затем примените его к $$$REMAINING_ARGS
transform:
SPREAD_REMAINDERS:
rewrite:
rewriters: [argument_spreader]
source: $$$REMAINING_ARGS
Я ожидал, что $$$REMAINING_ARGS будет состоять только из одноуровневых узлов первого аргумента, но потомки этих узлов также включены.
Рерайтер будет применен ко всем потомкам. Однако переписчик должен сопоставлять только первый узел в дереве и прекращать перезапись его дочерних элементов. Есть ошибка, заключающаяся в том, что узлы-потомки неоднократно перезаписываются. И это исправлено здесь.
Теперь давайте соберем эти детали вместе! Самый простой способ — просто разложить все объекты.
fix: '{...{$$$FIELDS}, $SPREAD_REMAINDERS}'
Посмотрите детскую площадку
Обратите внимание, что мы не оптимизировали переписывание выше. Мы также хотим:
Object.assign
внутри Object.assign
рекурсивноЭто слишком долго, чтобы включать его в этот ответ. Хотя это можно сделать в YAML, использование js-api может быть лучшим выбором.