feat(es): Add an option to preserve all comments (#3815)

This commit is contained in:
David Campion 2022-03-10 23:25:59 -08:00 committed by GitHub
parent 6257f0f990
commit c5a0c9a0ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 408 additions and 2 deletions

View File

@ -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)
}
}

View 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(),
);
}
}

View File

@ -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;

View 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
}
}

View 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

View File

@ -0,0 +1,5 @@
/* istanbul ignore next */ // preserved comment
var x = 1;
// Stacked Comment
// Another comment
var a = ""; // trailing comment

View 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
}

View 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');
};

View 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;

View 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
}

View File

@ -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
}
}

View 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
;

View File

@ -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,

View 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")
});
})

View File

@ -518,6 +518,8 @@ export interface JscConfig {
}
minify?: JsMinifyOptions;
preserveAllComments?: boolean;
}
export type JscTarget =

View 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
}

View 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");

View File

@ -0,0 +1,7 @@
// input 2 comment 1
type Z = number;
const x = 1;
/* input 2 comment 2 */
type Y = string;
const a = "";