Skip to content

Commit

Permalink
Add pure annotation comments for React functions + JSX
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett committed Apr 10, 2021
1 parent 2211a99 commit d732dcd
Show file tree
Hide file tree
Showing 17 changed files with 512 additions and 14 deletions.
21 changes: 20 additions & 1 deletion common/src/comments.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
pos::Spanned,
syntax_pos::{BytePos, Span},
syntax_pos::{BytePos, Span, DUMMY_SP},
};
use fxhash::FxHashMap;
use std::{
Expand Down Expand Up @@ -37,6 +37,8 @@ pub trait Comments {
fn has_trailing(&self, pos: BytePos) -> bool;
fn move_trailing(&self, from: BytePos, to: BytePos);
fn take_trailing(&self, pos: BytePos) -> Option<Vec<Comment>>;

fn add_pure_comment(&self, pos: BytePos);
}

macro_rules! delegate {
Expand Down Expand Up @@ -80,6 +82,10 @@ macro_rules! delegate {
fn take_trailing(&self, pos: BytePos) -> Option<Vec<Comment>> {
(**self).take_trailing(pos)
}

fn add_pure_comment(&self, pos: BytePos) {
(**self).add_pure_comment(pos)
}
};
}

Expand Down Expand Up @@ -181,6 +187,19 @@ impl Comments for SingleThreadedComments {
fn take_trailing(&self, pos: BytePos) -> Option<Vec<Comment>> {
self.trailing.borrow_mut().remove(&pos)
}

fn add_pure_comment(&self, pos: BytePos) {
let mut trailing = self.leading.borrow_mut();
let comments = trailing.entry(pos).or_default();
match comments.iter().find(|&c| c.text == "#__PURE__") {
Some(_) => {}
None => comments.push(Comment {
kind: CommentKind::Block,
span: DUMMY_SP,
text: "#__PURE__".into(),
}),
}
}
}

impl SingleThreadedComments {
Expand Down
14 changes: 13 additions & 1 deletion ecmascript/jsdoc/tests/fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use dashmap::DashMap;
use std::{env, path::PathBuf};
use swc_common::{
comments::{Comment, CommentKind, Comments},
BytePos,
BytePos, DUMMY_SP,
};
use swc_ecma_parser::{lexer::Lexer, EsConfig, Parser, StringInput, Syntax};
use test::{test_main, DynTestFn, ShouldPanic::No, TestDesc, TestDescAndFn, TestName, TestType};
Expand Down Expand Up @@ -182,4 +182,16 @@ impl Comments for SwcComments {
fn take_trailing(&self, pos: BytePos) -> Option<Vec<Comment>> {
self.trailing.remove(&pos).map(|v| v.1)
}

fn add_pure_comment(&self, pos: BytePos) {
let mut comments = self.leading.entry(pos).or_default();
match comments.iter().find(|&c| c.text == "#__PURE__") {
Some(_) => {}
None => comments.push(Comment {
kind: CommentKind::Block,
span: DUMMY_SP,
text: "#__PURE__".into(),
}),
}
}
}
1 change: 1 addition & 0 deletions ecmascript/transforms/react/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ swc_ecma_visit = {version = "0.29.0", path = "../../visit"}
swc_ecma_transforms_compat = {version = "0.13.0", path = "../compat/"}
swc_ecma_transforms_module = {version = "0.13.0", path = "../module"}
swc_ecma_transforms_testing = {version = "0.12.0", path = "../testing/"}
swc_ecma_codegen = {version = "0.52.0", path = "../../codegen/"}
testing = {version = "0.10.3", path = "../../../testing"}
8 changes: 8 additions & 0 deletions ecmascript/transforms/react/src/jsx/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,10 @@ where

let use_jsxs = count_children(&el.children) > 1;

if let Some(comments) = &self.comments {
comments.add_pure_comment(span.lo);
}

match self.runtime {
Runtime::Automatic => {
let jsx = if use_jsxs {
Expand Down Expand Up @@ -325,6 +329,10 @@ where

let name = self.jsx_name(el.opening.name);

if let Some(comments) = &self.comments {
comments.add_pure_comment(span.lo);
}

match self.runtime {
Runtime::Automatic => {
// function jsx(tagName: string, props: { children: Node[], ... }, key: string)
Expand Down
3 changes: 3 additions & 0 deletions ecmascript/transforms/react/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub use self::{
jsx::{jsx, Options},
jsx_self::jsx_self,
jsx_src::jsx_src,
pure_annotations::pure_annotations,
refresh::refresh,
};
use swc_common::{chain, comments::Comments, sync::Lrc, SourceMap};
Expand All @@ -14,6 +15,7 @@ mod display_name;
mod jsx;
mod jsx_self;
mod jsx_src;
mod pure_annotations;
mod refresh;

/// `@babel/preset-react`
Expand All @@ -32,6 +34,7 @@ where
display_name(),
jsx_src(development, cm.clone()),
jsx_self(development),
pure_annotations(comments.clone()),
refresh(development, refresh_options, cm.clone(), comments)
)
}
134 changes: 134 additions & 0 deletions ecmascript/transforms/react/src/pure_annotations/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
use std::collections::HashMap;
use swc_atoms::{js_word, JsWord};
use swc_common::comments::Comments;
use swc_ecma_ast::*;
use swc_ecma_utils::{id, Id};
use swc_ecma_visit::{Fold, FoldWith};

#[cfg(test)]
mod tests;

/// This pass adds a /*#__PURE__#/ annotation to calls to known pure top-level
/// React methods, so that terser and other minifiers can safely remove them
/// during dead code elimination.
/// See https://reactjs.org/docs/react-api.html
pub fn pure_annotations<C>(comments: Option<C>) -> impl Fold
where
C: Comments,
{
PureAnnotations {
imports: HashMap::new(),
comments,
}
}

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

impl<C> Fold for PureAnnotations<C>
where
C: Comments,
{
fn fold_module(&mut self, module: Module) -> Module {
// Pass 1: collect imports
for item in &module.body {
match item {
ModuleItem::ModuleDecl(ModuleDecl::Import(import)) => {
let src_str = import.src.value.to_string();
if src_str != "react" && src_str != "react-dom" {
continue;
}

for specifier in &import.specifiers {
let src = import.src.value.clone();
match specifier {
ImportSpecifier::Named(named) => {
let imported = match &named.imported {
Some(imported) => imported.sym.clone(),
None => named.local.sym.clone(),
};
self.imports.insert(id(&named.local), (src, imported));
}
ImportSpecifier::Default(default) => {
self.imports
.insert(id(&default.local), (src, js_word!("default")));
}
ImportSpecifier::Namespace(ns) => {
self.imports.insert(id(&ns.local), (src, "*".into()));
}
}
}
}
_ => {}
}
}

// Pass 2: add pure annotations.
module.fold_children_with(self)
}

fn fold_call_expr(&mut self, call: CallExpr) -> CallExpr {
let is_react_call = match &call.callee {
ExprOrSuper::Expr(expr) => match &**expr {
Expr::Ident(ident) => {
if let Some((src, specifier)) = self.imports.get(&id(&ident)) {
is_pure(src, specifier)
} else {
false
}
}
Expr::Member(member) => match &member.obj {
ExprOrSuper::Expr(expr) => match &**expr {
Expr::Ident(ident) => {
if let Some((src, specifier)) = self.imports.get(&id(&ident)) {
let specifier_str = specifier.to_string();
if specifier_str == "default" || specifier_str == "*" {
match &*member.prop {
Expr::Ident(ident) => is_pure(src, &ident.sym),
_ => false,
}
} else {
false
}
} else {
false
}
}
_ => false,
},
_ => false,
},
_ => false,
},
_ => false,
};

if is_react_call {
if let Some(comments) = &self.comments {
comments.add_pure_comment(call.span.lo);
}
}

call.fold_children_with(self)
}
}

fn is_pure(src: &JsWord, specifier: &JsWord) -> bool {
match src.to_string().as_str() {
"react" => match specifier.to_string().as_str() {
"cloneElement" | "createContext" | "createElement" | "createFactory" | "createRef"
| "forwardRef" | "isValidElement" | "memo" | "lazy" => true,
_ => false,
},
"react-dom" => match specifier.to_string().as_str() {
"createPortal" => true,
_ => false,
},
_ => false,
}
}
Loading

0 comments on commit d732dcd

Please sign in to comment.