Вставка комментариев с помощью плагина SWC Rust

Я пытаюсь добавить комментарий к существующему коду JavaScript.

В Babel есть помощник addComment, который можно вызвать на узле AST.

Вот простое (но довольно глупое) преобразование, написанное на Babel:

console.info`Hello, ${name}!`; -> /*#__PURE__*/console.info("Hello, ", name, "!");

const babel = require("@babel/core");

function run(code) {
  const result = babel.transform(code, {
    plugins: [
      function transform({ types: t }) {
        return {
          visitor: {
            TaggedTemplateExpression(path) {
              const { tag, quasi } = path.node;

              if (
                t.isMemberExpression(tag) &&
                t.isIdentifier(tag.object, { name: "console" }) &&
                t.isIdentifier(tag.property, { name: "log" })
              ) {
                let args = [];

                quasi.quasis.forEach((element, index) => {
                  args.push(t.stringLiteral(element.value.raw));
                  if (index < quasi.expressions.length) {
                    args.push(quasi.expressions[index]);
                  }
                });

                path.replaceWith(
                  t.callExpression(
                    t.addComment(
                      t.memberExpression(
                        t.identifier("console"),
                        t.identifier("log")
                      ),
                      "leading",
                      "#__PURE__"
                    ),
                    args
                  )
                );
              }
            },
          },
        };
      },
    ],
  });
  return result.code;
}

const code = "console.info`Hello, ${name}!`;";
console.info(run(code));
// -> /*#__PURE__*/console.info("Hello, ", name, "!");

Однако в Rust все немного сложнее, поскольку только одна переменная может владеть данными, и на нее может быть не более одной изменяемой ссылки, кроме того, в SWC реализованы некоторые трюки с производительностью.

Таким образом, в SWC вам необходимо использовать PluginCommentsProxy , который описан в текущей версии SWC 0.279.0 с помощью следующей блок-схемы:

 Below diagram shows one reference example how guest does trampoline between
 host's memory space.
┌───────────────────────────────────────┐    ┌─────────────────────────────────────────────┐
│Host (SWC/core)                        │    │Plugin (wasm)                                │
│  ┌────────────────────────────────┐   │    │                                             │
│  │COMMENTS.with()                 │   │    │  ┌──────────────────────────────────────┐   │
│  │                                │   │    │  │PluginCommentsProxy                   │   │
│  │                                │   │    │  │                                      │   │
│  │ ┌────────────────────────────┐ │   │    │  │ ┌────────────────────────────────┐   │   │
│  │ │get_leading_comments_proxy()│◀┼───┼────┼──┼─┤get_leading()                   │   │   │
│  │ │                            │ │   │    │  │ │                                │   │   │
│  │ │                            │ │   │    │  │ │ ┌──────────────────────────┐   │   │   │
│  │ │                            │─┼───┼──┬─┼──┼─┼─▶AllocatedBytesPtr(p,len)  │   │   │   │
│  │ └────────────────────────────┘ │   │  │ │  │ │ │                          │   │   │   │
│  │                                │   │  │ │  │ │ └─────────────┬────────────┘   │   │   │
│  │                                │   │  │ │  │ │               │                │   │   │
│  │                                │   │  │ │  │ │ ┌─────────────▼────────────┐   │   │   │
│  └────────────────────────────────┘   │  │ │  │ │ │Vec<Comments>             │   │   │   │
│                                       │  └─┼──┼─┼─▶                          │   │   │   │
│                                       │    │  │ │ └──────────────────────────┘   │   │   │
│                                       │    │  │ └────────────────────────────────┘   │   │
│                                       │    │  └──────────────────────────────────────┘   │
└───────────────────────────────────────┘    └─────────────────────────────────────────────┘

 1. Plugin calls `PluginCommentsProxy::get_leading()`. PluginCommentsProxy is
 a struct constructed in plugin's memory space.
 2. `get_leading()` internally calls `__get_leading_comments_proxy`, which is
 imported fn `get_leading_comments_proxy` exists in the host.
 3. Host access necessary values in its memory space (COMMENTS)
 4. Host copies value to be returned into plugin's memory space. Memory
 allocation for the value should be manually performed.
 5. Host completes imported fn, `PluginCommentsProxy::get_leading()` now can
 read, deserialize memory host wrote.
 - In case of `get_leading`, returned value is non-deterministic vec
 (`Vec<Comments>`) guest cannot preallocate with specific length. Instead,
 guest passes a fixed size struct (AllocatedBytesPtr), once host allocates
 actual vec into guest it'll write pointer to the vec into the struct.
comments.add_leading(
  node.span.lo,
  Comment {
    kind: swc_core::common::comments::CommentKind::Block,
    span: DUMMY_SP,
    text: "#__PURE__".to_string(),
  },
)

К сожалению, мне не удалось протестировать его должным образом с помощью swc_core::ecma::transforms::testing.

#[cfg(test)]

mod tests {
  use super::*;
  use std::path::PathBuf;
  use swc_core::ecma::transforms::testing::{test_fixture};
  use swc_ecma_transforms_testing::{FixtureTestConfig};

  #[testing::fixture("tests/fixture/**/input.tsx")]
  fn fixture(input: PathBuf) {
    test_fixture(
      Default::default(),
      &|tester| as_folder(TransformVisitor::new(&tester.comments)),
      &input,
      &input.with_file_name("output.tsx"),
      FixtureTestConfig::default(),
    );
  }
}

К сожалению, это не работает, потому что tester.comments относится к типу Rc<SingleThreadedComments>.

Я видел примеры использования <C>, например, трансформатора MillionJs:

fn transform_block<C>(context: &ProgramStateContext, node: &mut CallExpr, comments: C)
where
    C: Comments,
{

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

Есть ли способ лучше?

🤔 А знаете ли вы, что...
JavaScript является одним из трех основных языков веб-разработки, вместе с HTML и CSS.


59
1

Ответ:

Решено

Автор SWC здесь.

Вы можете сделать свой трансформер более универсальным C: Comments, как в официальном проходе pure_annotations. Затем сохраните C так же, как и другие дженерики.

Новый путь

Вы можете использовать PluginCommentProxy из плагина Wasm даже во время тестирования, если запускаете тесты через swc_ecma_transforms_testing или swc_core::ecma::transforms::testing, используя такой метод, как test_fixture.


PluginCommentProxy.add_leading(n.span.lo, Comment {
    // ...fields
});

будет работать только во время тестирования.

Старый способ

Это было необходимо раньше https://github.com/swc-project/swc/pull/9150, но этот способ работает до сих пор.

struct PureAnnotations<C>
where
    C: Comments,
{
    imports: AHashMap<Id, (JsWord, JsWord)>,
    comments: Option<C>,
}

после этого вам следует сделать раздел impl универсальным поверх C.

impl<C> VisitMut for PureAnnotations<C>
where
    C: Comments,
{
    noop_visit_mut_type!();

}

Вы можете добавить подходящие методы посетителей для достижения своей цели.

Альтернативно вы можете принять &dyn Comments или Option<&dyn Comments>. Официальный проход fixer использует этот шаблон для уменьшения размера двоичного файла. В этом случае вам следует добавить '_ между impl и Fold в возвращаемом типе конструктора.

pub fn fixer(comments: Option<&dyn Comments>) -> impl '_ + Fold + VisitMut {
    as_folder(Fixer {
        comments,
        ctx: Default::default(),
        span_map: Default::default(),
        in_for_stmt_head: Default::default(),
        in_opt_chain: Default::default(),
        remove_only: false,
    })
}

Интересные вопросы для изучения