Могу ли я воспроизвести правило eslint «prefer-object-spread» с помощью ast-grep?

Я пытаюсь сравнить разные линтеры и их производительность с помощью специальных правил. 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, чтобы проверить любую свою идею.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
96
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Переписчик 🔁 от 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}'

Посмотрите детскую площадку


Обратите внимание, что мы не оптимизировали переписывание выше. Мы также хотим:

  1. Поместите поля объекта непосредственно в переписанный объект.
  2. Перепишите Object.assign внутри Object.assign рекурсивно

Это слишком долго, чтобы включать его в этот ответ. Хотя это можно сделать в YAML, использование js-api может быть лучшим выбором.

Другие вопросы по теме