diff --git a/Cargo.lock b/Cargo.lock index 5e879d6b6..44e140594 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2637,6 +2637,25 @@ dependencies = [ "triomphe", ] +[[package]] +name = "swc_ast_grep" +version = "0.1.0" +dependencies = [ + "default-from-serde", + "rustc-hash 2.1.0", + "serde", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_codegen", + "swc_ecma_parser", + "swc_ecma_transforms_base", + "swc_ecma_transforms_testing", + "swc_ecma_utils", + "swc_ecma_visit", + "testing", +] + [[package]] name = "swc_atoms" version = "5.0.0" @@ -3373,6 +3392,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "swc_plugin_ast_grep" +version = "0.1.0" +dependencies = [ + "serde_json", + "swc_ast_grep", + "swc_core", +] + [[package]] name = "swc_plugin_emotion" version = "0.18.5" diff --git a/packages/swc-ast-grep/.npmignore b/packages/swc-ast-grep/.npmignore new file mode 100644 index 000000000..1ed674f50 --- /dev/null +++ b/packages/swc-ast-grep/.npmignore @@ -0,0 +1,2 @@ +transform/ +tests/ \ No newline at end of file diff --git a/packages/swc-ast-grep/Cargo.toml b/packages/swc-ast-grep/Cargo.toml new file mode 100644 index 000000000..d67390b35 --- /dev/null +++ b/packages/swc-ast-grep/Cargo.toml @@ -0,0 +1,25 @@ +[package] + +description = "SWC Plugin for @swc/sdk" + + +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +name = "swc_plugin_ast_grep" +publish = false +repository = { workspace = true } +rust-version = { workspace = true } +version = "0.1.0" + + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +serde_json = { workspace = true } +swc_core = { workspace = true, features = ["ecma_plugin_transform"] } + + +swc_ast_grep = { path = "./transform" } diff --git a/packages/swc-ast-grep/README.tmpl.md b/packages/swc-ast-grep/README.tmpl.md new file mode 100644 index 000000000..9c1d40f87 --- /dev/null +++ b/packages/swc-ast-grep/README.tmpl.md @@ -0,0 +1,22 @@ +# @swc/plugin-swc-ast-grep + +## Usage + +.swcrc: + +```json +{ + "jsc": { + "experimental": { + "plugins": [ + "@swc/plugin-swc-ast-grep", + { + "rules": "rules.yaml" + } + ] + } + } +} +``` + +${CHANGELOG} diff --git a/packages/swc-ast-grep/package.json b/packages/swc-ast-grep/package.json new file mode 100644 index 000000000..4075a3247 --- /dev/null +++ b/packages/swc-ast-grep/package.json @@ -0,0 +1,25 @@ +{ + "name": "@swc/plugin-ast-grep", + "version": "1.0.0", + "description": "SWC plugin for ast-grep", + "main": "swc_plugin_ast_grep.wasm", + "scripts": { + "prepack": "cargo build --release -p swc_plugin_ast_grep --target wasm32-wasip1 && cp ../../target/wasm32-wasip1/release/swc_plugin_ast_grep.wasm ." + }, + "homepage": "https://swc.rs", + "repository": { + "type": "git", + "url": "git+https://github.com/swc-project/plugins.git", + "directory": "packages/swc-ast-grep" + }, + "bugs": { + "url": "https://github.com/swc-project/plugins/issues" + }, + "author": "강동윤 ", + "keywords": [], + "license": "Apache-2.0", + "preferUnplugged": true, + "dependencies": { + "@swc/counter": "^0.1.3" + } +} diff --git a/packages/swc-ast-grep/src/lib.rs b/packages/swc-ast-grep/src/lib.rs new file mode 100644 index 000000000..3528a32f9 --- /dev/null +++ b/packages/swc-ast-grep/src/lib.rs @@ -0,0 +1,2 @@ +#[plugin_transform] +fn swc_ast_grep_plugin(mut program: Program, data: TransformPluginProgramMetadata) -> Program {} diff --git a/packages/swc-ast-grep/transform/Cargo.toml b/packages/swc-ast-grep/transform/Cargo.toml new file mode 100644 index 000000000..139c63e81 --- /dev/null +++ b/packages/swc-ast-grep/transform/Cargo.toml @@ -0,0 +1,32 @@ +[package] + +description = "SWC plugin to invoke ast-grep" + + +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +name = "swc_ast_grep" +publish = false +repository = { workspace = true } +rust-version = { workspace = true } +version = "0.1.0" + + +[dependencies] +default-from-serde = { workspace = true } +rustc-hash = { workspace = true } +serde = { workspace = true, features = ["derive"] } +swc_atoms = { workspace = true } +swc_common = { workspace = true } +swc_ecma_ast = { workspace = true } +swc_ecma_codegen = { workspace = true } +swc_ecma_utils = { workspace = true } +swc_ecma_visit = { workspace = true } + +[dev-dependencies] +swc_ecma_parser = { workspace = true } +swc_ecma_transforms_base = { workspace = true } +swc_ecma_transforms_testing = { workspace = true } +testing = { workspace = true } diff --git a/packages/swc-ast-grep/transform/src/config.rs b/packages/swc-ast-grep/transform/src/config.rs new file mode 100644 index 000000000..cda3d4aad --- /dev/null +++ b/packages/swc-ast-grep/transform/src/config.rs @@ -0,0 +1,42 @@ +use default_from_serde::SerdeDefault; +use serde::Deserialize; +use swc_atoms::Atom; + +#[derive(Debug, Clone, Deserialize, SerdeDefault)] +pub struct Config { + #[serde(default)] + pub flag: FlagConfig, + + /// Drop imports from the following modules. + #[serde(default = "default_remove_imports_from")] + pub remove_imports_from: Vec, +} + +#[derive(Debug, Clone, Deserialize, SerdeDefault)] +pub struct FlagConfig { + /// If true, + /// + /// - the variable name must be an identifier. + #[serde(default)] + pub strict: bool, + + #[serde(default = "default_flag_import_source")] + pub import_sources: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct ImportItem { + pub module: Atom, + pub name: Atom, +} + +fn default_remove_imports_from() -> Vec { + vec![Atom::new("@swc/sdk/annotations")] +} + +fn default_flag_import_source() -> Vec { + vec![ImportItem { + module: Atom::new("@swc/sdk/flag"), + name: Atom::new("flag"), + }] +} diff --git a/packages/swc-ast-grep/transform/src/flag.rs b/packages/swc-ast-grep/transform/src/flag.rs new file mode 100644 index 000000000..02309ea82 --- /dev/null +++ b/packages/swc-ast-grep/transform/src/flag.rs @@ -0,0 +1,158 @@ +use swc_common::{comments::Comments, errors::HANDLER, Spanned}; +use swc_ecma_ast::{ + Expr, KeyValueProp, ObjectLit, Pat, Prop, PropName, PropOrSpread, VarDeclarator, +}; +use swc_ecma_utils::ExprFactory; + +use crate::SwcSdkTransform; + +impl SwcSdkTransform +where + C: Comments, +{ + /// + /// ## Cases + /// + /// ### Empty arugments + /// + /// ```js + /// + /// import { flag } from "@swc/sdk/flag"; + /// + /// const foo = flag(); + /// ``` + /// + /// becomes + /// + /// ```js + /// import { flag } from "@swc/sdk/flag"; + /// + /// const foo = flag({ + /// key: "foo", + /// }); + /// ``` + /// + /// ### With arguments + /// + /// ```js + /// import { flag } from "@swc/sdk/flag"; + /// + /// const foo = flag({ + /// decide: () => false, + /// }); + /// ``` + /// + /// becomes + /// + /// ```js + /// import { flag } from "@swc/sdk/flag"; + /// + /// const foo = flag({ + /// key: "foo", + /// decide: () => false, + /// }); + /// ``` + /// + /// ### With custom adapter + /// + /// + /// ```js + /// import { flag } from "@swc/sdk/flag"; + /// + /// const foo = flag(someAdapter({ + /// decide: () => false, + /// }); + /// ``` + /// + /// becomes + /// + /// ```js + /// import { flag } from "@swc/sdk/flag"; + /// + /// const foo = flag(someAdapter({ + /// key: "foo", + /// decide: () => false, + /// })); + /// ``` + pub(super) fn transform_flag(&mut self, v: &mut VarDeclarator) -> Option { + let init = v.init.as_deref_mut()?; + let call_expr = init.as_mut_call()?; + + let callee = call_expr.callee.as_mut_expr()?; + + let import_of_flag_callee = self + .imports + .is_in_import_items(callee, &self.config.flag.import_sources)?; + + let name = match &v.name { + Pat::Ident(i) => i.clone(), + _ => { + if self.config.flag.strict { + HANDLER.with(|handler| { + handler + .struct_span_err( + v.name.span(), + "The variable name for the `flag()` calls must be an identifier", + ) + .span_note(import_of_flag_callee, "flag() is imported here") + .emit(); + }); + } + return None; + } + }; + + let prop = PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { + key: PropName::Ident("key".into()), + value: name.sym.clone().into(), + }))); + + if call_expr.args.is_empty() { + call_expr.args.push( + ObjectLit { + props: vec![prop], + ..Default::default() + } + .as_arg(), + ); + } else if let Some(obj) = find_object(&mut call_expr.args[0].expr) { + if obj + .props + .iter() + .filter_map(|p| p.as_prop()) + .any(|p| match &**p { + Prop::KeyValue(KeyValueProp { key, .. }) => { + matches!(key, PropName::Ident(i) if i.sym == "key") + } + _ => false, + }) + { + return None; + } + + obj.props.push(prop); + } + + None + } +} + +fn find_object(arg: &mut Expr) -> Option<&mut ObjectLit> { + match arg { + Expr::Object(obj) => Some(obj), + Expr::Call(call) => { + if call.args.is_empty() { + call.args.push( + ObjectLit { + ..Default::default() + } + .as_arg(), + ); + } + + let arg = call.args.get_mut(0)?; + find_object(&mut arg.expr) + } + _ => None, + } +} diff --git a/packages/swc-ast-grep/transform/src/import_analyzer.rs b/packages/swc-ast-grep/transform/src/import_analyzer.rs new file mode 100644 index 000000000..fc3c191ee --- /dev/null +++ b/packages/swc-ast-grep/transform/src/import_analyzer.rs @@ -0,0 +1,109 @@ +use rustc_hash::{FxHashMap, FxHashSet}; +use swc_atoms::Atom; +use swc_common::Span; +use swc_ecma_ast::*; +use swc_ecma_visit::{noop_visit_type, Visit, VisitWith}; + +use crate::config::ImportItem; + +#[derive(Debug, Default)] +pub(crate) struct ImportMap { + /// Map from module name to (module path, exported symbol, span) + imports: FxHashMap, + + namespace_imports: FxHashMap, + + imported_modules: FxHashSet, +} + +impl ImportMap { + /// Returns true if `e` is an import of `orig_name` from `module`. + pub fn is_import(&self, e: &Expr, module: &Atom, orig_name: &Atom) -> Option { + match e { + Expr::Ident(i) => { + if let Some((i_src, i_sym, i_span)) = self.imports.get(&i.to_id()) { + if i_src == module && i_sym == orig_name { + Some(*i_span) + } else { + None + } + } else { + None + } + } + + Expr::Member(MemberExpr { + obj: box Expr::Ident(obj), + prop: MemberProp::Ident(prop), + .. + }) => { + if let Some((obj_src, obj_span)) = self.namespace_imports.get(&obj.to_id()) { + if obj_src == module && prop.sym == *orig_name { + Some(*obj_span) + } else { + None + } + } else { + None + } + } + + _ => None, + } + } + + pub fn is_in_import_items(&self, e: &Expr, import_items: &[ImportItem]) -> Option { + import_items + .iter() + .find_map(|item| self.is_import(e, &item.module, &item.name)) + } + + pub fn analyze(m: &Module) -> Self { + let mut data = ImportMap::default(); + + m.visit_with(&mut Analyzer { data: &mut data }); + + data + } +} + +struct Analyzer<'a> { + data: &'a mut ImportMap, +} + +impl Visit for Analyzer<'_> { + noop_visit_type!(); + + fn visit_import_decl(&mut self, import: &ImportDecl) { + self.data.imported_modules.insert(import.src.value.clone()); + + for s in &import.specifiers { + let (local, orig_sym) = match s { + ImportSpecifier::Named(ImportNamedSpecifier { + local, imported, .. + }) => match imported { + Some(imported) => (local.to_id(), orig_name(imported)), + _ => (local.to_id(), local.sym.clone()), + }, + ImportSpecifier::Default(s) => (s.local.to_id(), "default".into()), + ImportSpecifier::Namespace(s) => { + self.data + .namespace_imports + .insert(s.local.to_id(), (import.src.value.clone(), s.local.span)); + continue; + } + }; + + self.data + .imports + .insert(local, (import.src.value.clone(), orig_sym, import.span)); + } + } +} + +fn orig_name(n: &ModuleExportName) -> Atom { + match n { + ModuleExportName::Ident(v) => v.sym.clone(), + ModuleExportName::Str(v) => v.value.clone(), + } +} diff --git a/packages/swc-ast-grep/transform/src/lib.rs b/packages/swc-ast-grep/transform/src/lib.rs new file mode 100644 index 000000000..86fe41725 --- /dev/null +++ b/packages/swc-ast-grep/transform/src/lib.rs @@ -0,0 +1,70 @@ +#![feature(box_patterns)] +#![feature(never_type)] + +use swc_common::{comments::Comments, util::take::Take, Mark}; +use swc_ecma_ast::{Module, ModuleDecl, ModuleItem, VarDeclarator}; +use swc_ecma_visit::{VisitMut, VisitMutWith}; + +use crate::{config::Config, import_analyzer::ImportMap}; + +pub mod config; +mod flag; +mod import_analyzer; + +#[derive(Debug, Clone)] +pub struct Env { + pub unresolved_mark: Mark, +} + +pub fn swc_sdk(env: Env, config: Config, comments: C) -> impl VisitMut +where + C: Comments, +{ + SwcSdkTransform { + env, + config, + comments, + imports: Default::default(), + } +} + +/// Handles functions from `@swc/sdk`. +struct SwcSdkTransform +where + C: Comments, +{ + #[allow(unused)] + env: Env, + config: Config, + #[allow(unused)] + comments: C, + imports: ImportMap, +} + +impl VisitMut for SwcSdkTransform +where + C: Comments, +{ + fn visit_mut_var_declarator(&mut self, node: &mut VarDeclarator) { + self.transform_flag(node); + + node.visit_mut_children_with(self); + } + + fn visit_mut_module(&mut self, m: &mut Module) { + self.imports = ImportMap::analyze(m); + + m.visit_mut_children_with(self); + } + + fn visit_mut_module_item(&mut self, m: &mut ModuleItem) { + if let ModuleItem::ModuleDecl(ModuleDecl::Import(import)) = m { + if self.config.remove_imports_from.contains(&import.src.value) { + m.take(); + return; + } + } + + m.visit_mut_children_with(self); + } +} diff --git a/packages/swc-ast-grep/transform/tests/fixture.rs b/packages/swc-ast-grep/transform/tests/fixture.rs new file mode 100644 index 000000000..503b88e69 --- /dev/null +++ b/packages/swc-ast-grep/transform/tests/fixture.rs @@ -0,0 +1,31 @@ +use std::path::PathBuf; + +use swc_common::Mark; +use swc_ecma_parser::Syntax; +use swc_ecma_transforms_base::resolver; +use swc_ecma_transforms_testing::test_fixture; +use swc_ecma_visit::visit_mut_pass; + +#[testing::fixture("tests/fixture/**/input.js")] +fn pure(input: PathBuf) { + let output = input.parent().unwrap().join("output.js"); + test_fixture( + Syntax::default(), + &|tr| { + let unresolved_mark = Mark::new(); + let top_level_mark = Mark::new(); + + ( + resolver(unresolved_mark, top_level_mark, false), + visit_mut_pass(swc_sdk::swc_sdk( + swc_sdk::Env { unresolved_mark }, + swc_sdk::config::Config::default(), + tr.comments.clone(), + )), + ) + }, + &input, + &output, + Default::default(), + ); +} diff --git a/packages/swc-ast-grep/transform/tests/fixture/flag/arg-1/input.js b/packages/swc-ast-grep/transform/tests/fixture/flag/arg-1/input.js new file mode 100644 index 000000000..3f8ee128c --- /dev/null +++ b/packages/swc-ast-grep/transform/tests/fixture/flag/arg-1/input.js @@ -0,0 +1,7 @@ +import { flag } from "@swc/sdk/flag"; + +const foo = flag({ + decide: () => false, +}); + +console.log(foo); \ No newline at end of file diff --git a/packages/swc-ast-grep/transform/tests/fixture/flag/arg-1/output.js b/packages/swc-ast-grep/transform/tests/fixture/flag/arg-1/output.js new file mode 100644 index 000000000..c638dbc70 --- /dev/null +++ b/packages/swc-ast-grep/transform/tests/fixture/flag/arg-1/output.js @@ -0,0 +1,6 @@ +import { flag } from "@swc/sdk/flag"; +const foo = flag({ + decide: ()=>false, + key: "foo" +}); +console.log(foo); diff --git a/packages/swc-ast-grep/transform/tests/fixture/flag/custom-adapter/input.js b/packages/swc-ast-grep/transform/tests/fixture/flag/custom-adapter/input.js new file mode 100644 index 000000000..3f8ee128c --- /dev/null +++ b/packages/swc-ast-grep/transform/tests/fixture/flag/custom-adapter/input.js @@ -0,0 +1,7 @@ +import { flag } from "@swc/sdk/flag"; + +const foo = flag({ + decide: () => false, +}); + +console.log(foo); \ No newline at end of file diff --git a/packages/swc-ast-grep/transform/tests/fixture/flag/custom-adapter/output.js b/packages/swc-ast-grep/transform/tests/fixture/flag/custom-adapter/output.js new file mode 100644 index 000000000..c638dbc70 --- /dev/null +++ b/packages/swc-ast-grep/transform/tests/fixture/flag/custom-adapter/output.js @@ -0,0 +1,6 @@ +import { flag } from "@swc/sdk/flag"; +const foo = flag({ + decide: ()=>false, + key: "foo" +}); +console.log(foo); diff --git a/packages/swc-ast-grep/transform/tests/fixture/flag/duplicate/input.js b/packages/swc-ast-grep/transform/tests/fixture/flag/duplicate/input.js new file mode 100644 index 000000000..49355de27 --- /dev/null +++ b/packages/swc-ast-grep/transform/tests/fixture/flag/duplicate/input.js @@ -0,0 +1,8 @@ +import { flag } from "@swc/sdk/flag"; + +const foo = flag({ + decide: () => false, + key: 'custom' +}); + +console.log(foo); \ No newline at end of file diff --git a/packages/swc-ast-grep/transform/tests/fixture/flag/duplicate/output.js b/packages/swc-ast-grep/transform/tests/fixture/flag/duplicate/output.js new file mode 100644 index 000000000..588adc579 --- /dev/null +++ b/packages/swc-ast-grep/transform/tests/fixture/flag/duplicate/output.js @@ -0,0 +1,6 @@ +import { flag } from "@swc/sdk/flag"; +const foo = flag({ + decide: ()=>false, + key: 'custom' +}); +console.log(foo); diff --git a/packages/swc-ast-grep/transform/tests/fixture/flag/no-arg/input.js b/packages/swc-ast-grep/transform/tests/fixture/flag/no-arg/input.js new file mode 100644 index 000000000..3f8ee128c --- /dev/null +++ b/packages/swc-ast-grep/transform/tests/fixture/flag/no-arg/input.js @@ -0,0 +1,7 @@ +import { flag } from "@swc/sdk/flag"; + +const foo = flag({ + decide: () => false, +}); + +console.log(foo); \ No newline at end of file diff --git a/packages/swc-ast-grep/transform/tests/fixture/flag/no-arg/output.js b/packages/swc-ast-grep/transform/tests/fixture/flag/no-arg/output.js new file mode 100644 index 000000000..c638dbc70 --- /dev/null +++ b/packages/swc-ast-grep/transform/tests/fixture/flag/no-arg/output.js @@ -0,0 +1,6 @@ +import { flag } from "@swc/sdk/flag"; +const foo = flag({ + decide: ()=>false, + key: "foo" +}); +console.log(foo); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64ffd756e..b333f14c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -99,6 +99,12 @@ importers: specifier: ^0.1.3 version: 0.1.3 + packages/swc-ast-grep: + dependencies: + '@swc/counter': + specifier: ^0.1.3 + version: 0.1.3 + packages/swc-confidential: dependencies: '@swc/counter':