mirror of
https://github.com/swc-project/swc.git
synced 2024-09-19 04:38:13 +03:00
feat(es): Add an option to preserve all comments (#3815)
This commit is contained in:
parent
6257f0f990
commit
c5a0c9a0ab
@ -65,6 +65,7 @@ use swc_ecma_visit::{Fold, VisitMutWith};
|
||||
use self::util::BoolOrObject;
|
||||
use crate::{
|
||||
builder::PassBuilder,
|
||||
dropped_comments_preserver::dropped_comments_preserver,
|
||||
plugin::{PluginConfig, PluginContext},
|
||||
SwcImportResolver,
|
||||
};
|
||||
@ -296,6 +297,7 @@ impl Options {
|
||||
minify: mut js_minify,
|
||||
experimental,
|
||||
lints,
|
||||
preserve_all_comments,
|
||||
..
|
||||
} = config.jsc;
|
||||
|
||||
@ -368,7 +370,11 @@ impl Options {
|
||||
|
||||
let regenerator = transform.regenerator.clone();
|
||||
|
||||
let preserve_comments = js_minify.as_ref().map(|v| v.format.comments.clone());
|
||||
let preserve_comments = if preserve_all_comments {
|
||||
Some(BoolOrObject::from(true))
|
||||
} else {
|
||||
js_minify.as_ref().map(|v| v.format.comments.clone())
|
||||
};
|
||||
|
||||
if syntax.typescript() {
|
||||
transform.legacy_decorator = true;
|
||||
@ -498,7 +504,11 @@ impl Options {
|
||||
syntax.jsx()
|
||||
),
|
||||
pass,
|
||||
Optional::new(jest::jest(), transform.hidden.jest)
|
||||
Optional::new(jest::jest(), transform.hidden.jest),
|
||||
Optional::new(
|
||||
dropped_comments_preserver(comments.cloned()),
|
||||
preserve_all_comments
|
||||
),
|
||||
);
|
||||
|
||||
Ok(BuiltInput {
|
||||
@ -1031,6 +1041,9 @@ pub struct JscConfig {
|
||||
|
||||
#[serde(default)]
|
||||
pub lints: LintConfig,
|
||||
|
||||
#[serde(default)]
|
||||
pub preserve_all_comments: bool,
|
||||
}
|
||||
|
||||
/// `jsc.experimental` in `.swcrc`
|
||||
@ -1567,6 +1580,8 @@ impl Merge for JscConfig {
|
||||
self.paths.merge(&from.paths);
|
||||
self.minify.merge(&from.minify);
|
||||
self.experimental.merge(&from.experimental);
|
||||
self.preserve_all_comments
|
||||
.merge(&from.preserve_all_comments)
|
||||
}
|
||||
}
|
||||
|
||||
|
138
crates/swc/src/dropped_comments_preserver.rs
Normal file
138
crates/swc/src/dropped_comments_preserver.rs
Normal file
@ -0,0 +1,138 @@
|
||||
use swc_common::{
|
||||
comments::{Comment, Comments, SingleThreadedComments},
|
||||
BytePos, Span, DUMMY_SP,
|
||||
};
|
||||
use swc_ecma_ast::{Module, Script};
|
||||
use swc_ecma_visit::{as_folder, noop_visit_mut_type, Fold, VisitMut, VisitMutWith};
|
||||
|
||||
/// Preserves comments that would otherwise be dropped.
|
||||
///
|
||||
/// If during compilation an ast node associated with
|
||||
/// a comment is dropped, the comment will not appear in the final emitted
|
||||
/// output. This can create problems in the JavaScript ecosystem, particularly
|
||||
/// around instanbul coverage and other tooling that relies on comment
|
||||
/// directives.
|
||||
///
|
||||
/// This transformer shifts orphaned comments to the next closest known span
|
||||
/// while making a best-effort to preserve the "general orientation" of
|
||||
/// comments.
|
||||
|
||||
pub fn dropped_comments_preserver(
|
||||
comments: Option<SingleThreadedComments>,
|
||||
) -> impl Fold + VisitMut {
|
||||
as_folder(DroppedCommentsPreserver {
|
||||
comments,
|
||||
is_first_span: true,
|
||||
known_spans: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
struct DroppedCommentsPreserver {
|
||||
comments: Option<SingleThreadedComments>,
|
||||
is_first_span: bool,
|
||||
known_spans: Vec<Span>,
|
||||
}
|
||||
|
||||
type CommentEntries = Vec<(BytePos, Vec<Comment>)>;
|
||||
|
||||
impl VisitMut for DroppedCommentsPreserver {
|
||||
noop_visit_mut_type!();
|
||||
|
||||
fn visit_mut_module(&mut self, module: &mut Module) {
|
||||
module.visit_mut_children_with(self);
|
||||
self.shift_comments_to_known_spans();
|
||||
}
|
||||
|
||||
fn visit_mut_script(&mut self, script: &mut Script) {
|
||||
script.visit_mut_children_with(self);
|
||||
self.shift_comments_to_known_spans();
|
||||
}
|
||||
|
||||
fn visit_mut_span(&mut self, span: &mut Span) {
|
||||
if span.is_dummy() || self.is_first_span {
|
||||
self.is_first_span = false;
|
||||
return;
|
||||
}
|
||||
|
||||
self.known_spans.push(*span);
|
||||
span.visit_mut_children_with(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl DroppedCommentsPreserver {
|
||||
fn shift_comments_to_known_spans(&self) {
|
||||
if let Some(comments) = &self.comments {
|
||||
let trailing_comments = self.shift_leading_comments(comments);
|
||||
|
||||
self.shift_trailing_comments(trailing_comments);
|
||||
}
|
||||
}
|
||||
|
||||
/// We'll be shifting all comments to known span positions, so drain the
|
||||
/// current comments first to limit the amount of look ups needed into
|
||||
/// the hashmaps.
|
||||
///
|
||||
/// This way, we only need to take the comments once, and then add them back
|
||||
/// once.
|
||||
fn collect_existing_comments(&self, comments: &SingleThreadedComments) -> CommentEntries {
|
||||
let (mut leading_comments, mut trailing_comments) = comments.borrow_all_mut();
|
||||
let mut existing_comments: CommentEntries = leading_comments
|
||||
.drain()
|
||||
.chain(trailing_comments.drain())
|
||||
.collect();
|
||||
|
||||
existing_comments.sort_by(|(bp_a, _), (bp_b, _)| bp_a.cmp(bp_b));
|
||||
|
||||
existing_comments
|
||||
}
|
||||
|
||||
/// Shift all comments to known leading positions.
|
||||
/// This prevents trailing comments from ending up associated with
|
||||
/// nodes that will not emit trailing comments, while
|
||||
/// preserving any comments that might show up after all code positions.
|
||||
///
|
||||
/// This maintains the highest fidelity between existing comment positions
|
||||
/// of pre and post compiled code.
|
||||
fn shift_leading_comments(&self, comments: &SingleThreadedComments) -> CommentEntries {
|
||||
let mut existing_comments = self.collect_existing_comments(comments);
|
||||
|
||||
for span in self.known_spans.iter() {
|
||||
let (comments_to_move, next_byte_positions): (CommentEntries, CommentEntries) =
|
||||
existing_comments
|
||||
.drain(..)
|
||||
.partition(|(bp, _)| *bp <= span.lo);
|
||||
|
||||
existing_comments.extend(next_byte_positions);
|
||||
|
||||
let collected_comments = comments_to_move.into_iter().flat_map(|(_, c)| c).collect();
|
||||
|
||||
self.comments
|
||||
.add_leading_comments(span.lo, collected_comments)
|
||||
}
|
||||
|
||||
existing_comments
|
||||
}
|
||||
|
||||
/// These comments trail all known span lo byte positions.
|
||||
/// Therefore, by shifting them to trail the highest known hi position, we
|
||||
/// ensure that any remaining trailing comments are emitted in a
|
||||
/// similar location
|
||||
fn shift_trailing_comments(&self, remaining_comment_entries: CommentEntries) {
|
||||
let last_trailing = self
|
||||
.known_spans
|
||||
.iter()
|
||||
.copied()
|
||||
.fold(
|
||||
DUMMY_SP,
|
||||
|acc, span| if span.hi > acc.hi { span } else { acc },
|
||||
);
|
||||
|
||||
self.comments.add_trailing_comments(
|
||||
last_trailing.hi,
|
||||
remaining_comment_entries
|
||||
.into_iter()
|
||||
.flat_map(|(_, c)| c)
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
@ -170,6 +170,7 @@ use crate::config::{
|
||||
|
||||
mod builder;
|
||||
pub mod config;
|
||||
mod dropped_comments_preserver;
|
||||
mod plugin;
|
||||
pub mod resolver {
|
||||
use std::path::PathBuf;
|
||||
|
16
crates/swc/tests/fixture/issue-2964.case-1/input/.swcrc
Normal file
16
crates/swc/tests/fixture/issue-2964.case-1/input/.swcrc
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"tsx": false,
|
||||
"decorators": false,
|
||||
"dynamicImport": false
|
||||
},
|
||||
"transform": null,
|
||||
"target": "es5",
|
||||
"loose": false,
|
||||
"externalHelpers": false,
|
||||
"keepClassNames": false,
|
||||
"preserveAllComments": true
|
||||
}
|
||||
}
|
11
crates/swc/tests/fixture/issue-2964.case-1/input/index.ts
Normal file
11
crates/swc/tests/fixture/issue-2964.case-1/input/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/* istanbul ignore next */
|
||||
type Z = number;
|
||||
// preserved comment
|
||||
const x = 1;
|
||||
|
||||
// Stacked Comment
|
||||
// Another comment
|
||||
type Y = string;
|
||||
const a = "";
|
||||
|
||||
// trailing comment
|
@ -0,0 +1,5 @@
|
||||
/* istanbul ignore next */ // preserved comment
|
||||
var x = 1;
|
||||
// Stacked Comment
|
||||
// Another comment
|
||||
var a = ""; // trailing comment
|
33
crates/swc/tests/fixture/issue-2964.case-2/input/.swcrc
Normal file
33
crates/swc/tests/fixture/issue-2964.case-2/input/.swcrc
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"sourceMaps": false,
|
||||
"module": {
|
||||
"type": "commonjs"
|
||||
},
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"tsx": true,
|
||||
"dynamicImport": true,
|
||||
"decorators": false
|
||||
},
|
||||
"transform": {
|
||||
"react": {
|
||||
"pragma": "React.createElement",
|
||||
"pragmaFrag": "React.Fragment",
|
||||
"throwIfNamespace": true,
|
||||
"development": false,
|
||||
"useBuiltins": false
|
||||
}
|
||||
},
|
||||
"target": "es2015",
|
||||
"loose": false,
|
||||
"externalHelpers": false,
|
||||
"keepClassNames": false,
|
||||
"minify": {
|
||||
"compress": false,
|
||||
"mangle": false
|
||||
},
|
||||
"preserveAllComments": true
|
||||
},
|
||||
"minify": false
|
||||
}
|
12
crates/swc/tests/fixture/issue-2964.case-2/input/index.ts
Normal file
12
crates/swc/tests/fixture/issue-2964.case-2/input/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
//top comment
|
||||
export const noop = () => {};
|
||||
/* istanbul ignore next */
|
||||
export const badIstanbul = (test: Record<string, unknown>) => {
|
||||
const { value, ...pixelParams } = test;
|
||||
console.log('fail');
|
||||
};
|
||||
|
||||
/* istanbul ignore next: UI-5137 */
|
||||
export const downloadDocument = (): void => {
|
||||
console.log('fail');
|
||||
};
|
20
crates/swc/tests/fixture/issue-2964.case-2/output/index.ts
Normal file
20
crates/swc/tests/fixture/issue-2964.case-2/output/index.ts
Normal file
@ -0,0 +1,20 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.downloadDocument = exports.badIstanbul = exports.noop = void 0;
|
||||
var swcHelpers = require("@swc/helpers");
|
||||
//top comment
|
||||
const noop = ()=>{};
|
||||
exports.noop = noop;
|
||||
var /* istanbul ignore next */ badIstanbul = (test)=>{
|
||||
const { value } = test, pixelParams = swcHelpers.objectWithoutProperties(test, [
|
||||
"value"
|
||||
]);
|
||||
console.log('fail');
|
||||
};
|
||||
exports.badIstanbul = badIstanbul;
|
||||
/* istanbul ignore next: UI-5137 */ const downloadDocument = ()=>{
|
||||
console.log('fail');
|
||||
};
|
||||
exports.downloadDocument = downloadDocument;
|
32
crates/swc/tests/fixture/issue-2964.case-3/input/.swcrc
Normal file
32
crates/swc/tests/fixture/issue-2964.case-3/input/.swcrc
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"sourceMaps": false,
|
||||
"module": {
|
||||
"type": "commonjs"
|
||||
},
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"tsx": false,
|
||||
"dynamicImport": true,
|
||||
"decorators": false
|
||||
},
|
||||
"transform": {
|
||||
"react": {
|
||||
"pragma": "React.createElement",
|
||||
"pragmaFrag": "React.Fragment",
|
||||
"throwIfNamespace": true,
|
||||
"development": false,
|
||||
"useBuiltins": false
|
||||
},
|
||||
"hidden": {
|
||||
"jest": true
|
||||
}
|
||||
},
|
||||
"target": "es2015",
|
||||
"loose": false,
|
||||
"externalHelpers": false,
|
||||
"keepClassNames": false,
|
||||
"preserveAllComments": true
|
||||
},
|
||||
"minify": false
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
// single line comment
|
||||
const x = ({y, ...rest}: /*todo: refactor any type*/ any) => {
|
||||
return {
|
||||
y, // another comment
|
||||
z: rest.z // final comment
|
||||
}
|
||||
}
|
14
crates/swc/tests/fixture/issue-2964.case-3/output/index.ts
Normal file
14
crates/swc/tests/fixture/issue-2964.case-3/output/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
"use strict";
|
||||
var swcHelpers = require("@swc/helpers");
|
||||
// single line comment
|
||||
const x = (_param)=>/*todo: refactor any type*/ {
|
||||
var { y } = _param, rest = swcHelpers.objectWithoutProperties(_param, [
|
||||
"y"
|
||||
]);
|
||||
return {
|
||||
y,
|
||||
// another comment
|
||||
z: rest.z
|
||||
};
|
||||
} // final comment
|
||||
;
|
@ -149,6 +149,7 @@ fn shopify_2_same_opt() {
|
||||
experimental: Default::default(),
|
||||
lints: Default::default(),
|
||||
assumptions: Default::default(),
|
||||
preserve_all_comments: false,
|
||||
},
|
||||
module: None,
|
||||
minify: false,
|
||||
|
51
node-swc/__tests__/preserve_comments.mjs
Normal file
51
node-swc/__tests__/preserve_comments.mjs
Normal file
@ -0,0 +1,51 @@
|
||||
import swc from "../..";
|
||||
import path from "path";
|
||||
import {fileURLToPath} from "url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
describe("Should preserve comments", () => {
|
||||
it("Should preserve comments preceding types", async () => {
|
||||
const input = `/*comment*/ type X = number; const x: X = 1`;
|
||||
const result = swc.transformSync(input, {
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
},
|
||||
"preserveAllComments": true
|
||||
}
|
||||
});
|
||||
expect(result.code).toBe('/*comment*/ var x = 1;\n');
|
||||
});
|
||||
|
||||
it("Should preserve comments preceding shifted functions", () => {
|
||||
const filename = path.resolve(
|
||||
__dirname + "/../tests/issue-2964/input1.ts"
|
||||
);
|
||||
|
||||
const {code} = swc.transformFileSync(filename);
|
||||
|
||||
expect(code).toContain("/* input 1 comment 1 */ var tail =")
|
||||
expect(code).toContain(`// input 1 comment 2\nvar saysHello =`)
|
||||
});
|
||||
|
||||
it("Should not share comments between modules", () => {
|
||||
const filename1 = path.resolve(
|
||||
__dirname + "/../tests/issue-2964/input1.ts"
|
||||
);
|
||||
const filename2 = path.resolve(
|
||||
__dirname + "/../tests/issue-2964/input2.ts"
|
||||
);
|
||||
|
||||
swc.transformFileSync(filename1);
|
||||
|
||||
const result2 = swc.transformFileSync(filename2);
|
||||
const result1 = swc.transformFileSync(filename1);
|
||||
|
||||
expect(result1.code).toMatch("input 1");
|
||||
expect(result1.code).not.toMatch("input 2");
|
||||
|
||||
expect(result2.code).toMatch("input 2");
|
||||
expect(result2.code).not.toMatch("input 1")
|
||||
});
|
||||
})
|
@ -518,6 +518,8 @@ export interface JscConfig {
|
||||
}
|
||||
|
||||
minify?: JsMinifyOptions;
|
||||
|
||||
preserveAllComments?: boolean;
|
||||
}
|
||||
|
||||
export type JscTarget =
|
||||
|
36
node-swc/tests/issue-2964/.swcrc
Normal file
36
node-swc/tests/issue-2964/.swcrc
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"sourceMaps": true,
|
||||
"module": {
|
||||
"type": "commonjs"
|
||||
},
|
||||
"jsc": {
|
||||
"preserveAllComments": true,
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"tsx": false,
|
||||
"dynamicImport": true,
|
||||
"decorators": false
|
||||
},
|
||||
"transform": {
|
||||
"react": {
|
||||
"pragma": "React.createElement",
|
||||
"pragmaFrag": "React.Fragment",
|
||||
"throwIfNamespace": true,
|
||||
"development": false,
|
||||
"useBuiltins": false
|
||||
},
|
||||
"hidden": {
|
||||
"jest": true
|
||||
}
|
||||
},
|
||||
"target": "es3",
|
||||
"loose": false,
|
||||
"externalHelpers": false,
|
||||
"keepClassNames": false,
|
||||
"minify": {
|
||||
"compress": false,
|
||||
"mangle": false
|
||||
}
|
||||
},
|
||||
"minify": false
|
||||
}
|
5
node-swc/tests/issue-2964/input1.ts
Normal file
5
node-swc/tests/issue-2964/input1.ts
Normal file
@ -0,0 +1,5 @@
|
||||
/* input 1 comment 1 */
|
||||
const tail = <T extends Array<unknown>>([_, ...tail]: T) => tail;
|
||||
|
||||
// input 1 comment 2
|
||||
export const saysHello = () => console.log("hello");
|
7
node-swc/tests/issue-2964/input2.ts
Normal file
7
node-swc/tests/issue-2964/input2.ts
Normal file
@ -0,0 +1,7 @@
|
||||
// input 2 comment 1
|
||||
type Z = number;
|
||||
const x = 1;
|
||||
|
||||
/* input 2 comment 2 */
|
||||
type Y = string;
|
||||
const a = "";
|
Loading…
Reference in New Issue
Block a user