feat(codegen): add option for choosing quotes; remove slow choose_quot method (#4219)

This commit is contained in:
Boshen 2024-07-12 03:08:22 +00:00
parent 3e56b2bd16
commit 83c2c62f7b
14 changed files with 397 additions and 384 deletions

View file

@ -65,7 +65,7 @@ impl<'a, const MINIFY: bool> Gen<MINIFY> for Directive<'a> {
// A Use Strict Directive may not contain an EscapeSequence or LineContinuation.
// So here should print original `directive` value, the `expression` value is escaped str.
// See https://github.com/babel/babel/blob/main/packages/babel-generator/src/generators/base.ts#L64
p.wrap_quote(self.directive.as_str(), |p, _| {
p.wrap_quote(|p, _| {
p.print_str(self.directive.as_str());
});
p.print_semicolon_after_statement();
@ -1268,7 +1268,7 @@ impl<'a, const MINIFY: bool> Gen<MINIFY> for RegExpLiteral<'a> {
}
}
fn print_unquoted_str<const MINIFY: bool>(s: &str, quote: char, p: &mut Codegen<{ MINIFY }>) {
fn print_unquoted_str<const MINIFY: bool>(s: &str, quote: u8, p: &mut Codegen<{ MINIFY }>) {
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
@ -1308,21 +1308,21 @@ fn print_unquoted_str<const MINIFY: bool>(s: &str, quote: char, p: &mut Codegen<
p.print_str("\\\\");
}
'\'' => {
if quote == '\'' {
if quote == b'\'' {
p.print_str("\\'");
} else {
p.print_str("'");
}
}
'\"' => {
if quote == '"' {
if quote == b'"' {
p.print_str("\\\"");
} else {
p.print_str("\"");
}
}
'`' => {
if quote == '`' {
if quote == b'`' {
p.print_str("\\`");
} else {
p.print_str("`");
@ -1354,7 +1354,7 @@ impl<'a, const MINIFY: bool> Gen<MINIFY> for StringLiteral<'a> {
fn gen(&self, p: &mut Codegen<{ MINIFY }>, _ctx: Context) {
p.add_source_mapping(self.span.start);
let s = self.value.as_str();
p.wrap_quote(s, |p, quote| {
p.wrap_quote(|p, quote| {
print_unquoted_str(s, quote, p);
});
}
@ -2239,7 +2239,7 @@ impl<'a, const MINIFY: bool> Gen<MINIFY> for JSXAttributeValue<'a> {
Self::Element(el) => el.gen(p, ctx),
Self::StringLiteral(lit) => {
p.print_char(b'"');
print_unquoted_str(&lit.value, '"', p);
print_unquoted_str(&lit.value, b'"', p);
p.print_char(b'"');
}
Self::ExpressionContainer(expr_container) => expr_container.gen(p, ctx),

View file

@ -39,6 +39,12 @@ pub type CodeGenerator<'a> = Codegen<'a, false>;
/// Code generator with whitespace removal.
pub type WhitespaceRemover<'a> = Codegen<'a, true>;
#[derive(Default, Clone, Copy)]
pub struct CodegenOptions {
/// Use single quotes instead of double quotes.
pub single_quote: bool,
}
#[derive(Default, Clone, Copy)]
pub struct CommentOptions {
/// Enable preserve annotate comments, like `/* #__PURE__ */` and `/* #__NO_SIDE_EFFECTS__ */`.
@ -51,6 +57,7 @@ pub struct CodegenReturn {
}
pub struct Codegen<'a, const MINIFY: bool> {
options: CodegenOptions,
comment_options: CommentOptions,
source_text: &'a str,
@ -80,6 +87,9 @@ pub struct Codegen<'a, const MINIFY: bool> {
/// Track the current indentation level
indent: u32,
/// Fast path for [CodegenOptions::single_quote]
quote: u8,
// Builders
sourcemap_builder: Option<SourcemapBuilder>,
@ -112,6 +122,7 @@ impl<'a, const MINIFY: bool> Codegen<'a, MINIFY> {
#[must_use]
pub fn new() -> Self {
Self {
options: CodegenOptions::default(),
comment_options: CommentOptions::default(),
source_text: "",
trivias: Trivias::default(),
@ -127,6 +138,7 @@ impl<'a, const MINIFY: bool> Codegen<'a, MINIFY> {
start_of_arrow_expr: 0,
start_of_default_export: 0,
indent: 0,
quote: b'"',
sourcemap_builder: None,
move_comment_map: MoveCommentMap::default(),
}
@ -141,6 +153,13 @@ impl<'a, const MINIFY: bool> Codegen<'a, MINIFY> {
self
}
#[must_use]
pub fn with_options(mut self, options: CodegenOptions) -> Self {
self.options = options;
self.quote = if options.single_quote { b'\'' } else { b'"' };
self
}
#[must_use]
pub fn enable_comment(
mut self,
@ -467,11 +486,10 @@ impl<'a, const MINIFY: bool> Codegen<'a, MINIFY> {
}
#[inline]
fn wrap_quote<F: FnMut(&mut Self, char)>(&mut self, s: &str, mut f: F) {
let quote = Self::choose_quote(s);
self.print_char(quote as u8);
f(self, quote);
self.print_char(quote as u8);
fn wrap_quote<F: FnMut(&mut Self, u8)>(&mut self, mut f: F) {
self.print_char(self.quote);
f(self, self.quote);
self.print_char(self.quote);
}
fn print_directives_and_statements(
@ -512,24 +530,6 @@ impl<'a, const MINIFY: bool> Codegen<'a, MINIFY> {
sourcemap_builder.add_source_mapping_for_name(&self.code, span, name);
}
}
fn choose_quote(s: &str) -> char {
let mut single_cost = 0;
let mut double_cost = 0;
for c in s.chars() {
match c {
'\'' => single_cost += 1,
'"' => double_cost += 1,
_ => {}
}
}
if single_cost > double_cost {
'"'
} else {
'\''
}
}
}
pub(crate) type MoveCommentMap = FxHashMap<u32, Comment>;

View file

@ -1,25 +1,29 @@
use oxc_allocator::Allocator;
use oxc_codegen::{CodeGenerator, CommentOptions};
use oxc_codegen::{CodeGenerator, CodegenOptions, CommentOptions};
use oxc_parser::Parser;
use oxc_span::SourceType;
fn test(source_text: &str, expected: &str) {
fn check(source_text: &str, expected: &str, source_type: SourceType) {
let allocator = Allocator::default();
let source_type = SourceType::default().with_module(true);
let ret = Parser::new(&allocator, source_text, source_type).parse();
let result = CodeGenerator::new().build(&ret.program).source_text;
let result = CodeGenerator::new()
.with_options(CodegenOptions { single_quote: true })
.build(&ret.program)
.source_text;
assert_eq!(expected, result, "for source {source_text}, expect {expected}, got {result}");
}
fn test(source_text: &str, expected: &str) {
let source_type = SourceType::default().with_module(true);
check(source_text, expected, source_type);
}
fn test_ts(source_text: &str, expected: &str, is_typescript_definition: bool) {
let allocator = Allocator::default();
let source_type = SourceType::default()
.with_typescript(true)
.with_typescript_definition(is_typescript_definition)
.with_module(true);
let ret = Parser::new(&allocator, source_text, source_type).parse();
let result = CodeGenerator::new().build(&ret.program).source_text;
assert_eq!(expected, result, "for source {source_text}, expect {expected}, got {result}");
check(source_text, expected, source_type);
}
#[test]
@ -30,7 +34,7 @@ fn string() {
test("let x = '\t'", "let x = '\t';\n");
test(r"let x = '\v'", "let x = '\\v';\n");
test("let x = '\\n'", "let x = '\\n';\n");
test("let x = '\\''", "let x = \"'\";\n");
test("let x = '\\''", "let x = '\\'';\n");
test("let x = '\\\"'", "let x = '\"';\n");
// test( "let x = '\\'''", "let x = `''`;\n");
test("let x = '\\\\'", "let x = '\\\\';\n");

View file

@ -5,8 +5,8 @@ input_file: crates/oxc_isolated_declarations/tests/fixtures/as-const.ts
==================== .D.TS ====================
declare const F: {
readonly string: 'string';
readonly templateLiteral: 'templateLiteral';
readonly string: "string";
readonly templateLiteral: "templateLiteral";
readonly number: 1.23;
readonly bigint: -1_2_3n;
readonly boolean: true;
@ -15,8 +15,8 @@ declare const F: {
readonly function: (a: string) => void;
readonly arrow: (a: string) => void;
readonly object: {
readonly a: 'a';
readonly b: 'b';
readonly a: "a";
readonly b: "b";
};
readonly array: readonly ['a', undefined, { readonly b: '\n'}];
readonly array: readonly ["a", undefined, { readonly b: "\n"}];
};

View file

@ -20,7 +20,7 @@ export declare abstract class Qux {
baz(): void;
}
export declare class Baz {
readonly prop1 = 'some string';
readonly prop1 = "some string";
prop2: string;
private prop3;
private prop4;

View file

@ -4,11 +4,11 @@ input_file: crates/oxc_isolated_declarations/tests/fixtures/eliminate-imports.ts
---
==================== .D.TS ====================
import { AExtend, BExtend, Type, CImplements1, CImplements2, CType, ThisType1, ThisType2 } from 'mod';
import { AExtend, BExtend, Type, CImplements1, CImplements2, CType, ThisType1, ThisType2 } from "mod";
export interface A extends AExtend<Type> {}
export declare class B extends BExtend<Type> {}
export declare class C implements CImplements1<CType>, CImplements2<CType> {}
export declare function foo(this: ThisType1 ): void;
export declare const bar: (this: ThisType2 ) => void;
import { type InferType1, type InferType2 } from 'infer';
import { type InferType1, type InferType2 } from "infer";
export type F<X extends InferType1> = X extends infer U extends InferType2 ? U : never;

View file

@ -4,11 +4,11 @@ input_file: crates/oxc_isolated_declarations/tests/fixtures/infer-template-liter
---
==================== .D.TS ====================
export declare const CSS_VARS_HELPER = 'useCssVars';
export declare const CSS_VARS_HELPER = "useCssVars";
export declare function g(func?: string): void;
export declare const F: {
readonly a: 'a';
readonly b: readonly ['b'];
readonly a: "a";
readonly b: readonly ["b"];
};
export declare let GOOD: string;
export declare const BAD: unknown;

View file

@ -4,8 +4,8 @@ input_file: crates/oxc_isolated_declarations/tests/fixtures/mapped-types.ts
---
==================== .D.TS ====================
import { K } from 'foo';
import { T } from 'bar';
import { K } from "foo";
import { T } from "bar";
export interface I {
prop: { [key in K] : T};
}

View file

@ -1,6 +1,6 @@
use std::borrow::Cow;
use oxc_codegen::Codegen;
use oxc_codegen::{CodeGenerator, CodegenOptions};
use oxc_diagnostics::OxcDiagnostic;
use oxc_span::{GetSpan, Span, SPAN};
@ -219,8 +219,8 @@ impl<'c, 'a: 'c> RuleFixer<'c, 'a> {
}
#[allow(clippy::unused_self)]
pub fn codegen(self) -> Codegen<'a, false> {
Codegen::<false>::new()
pub fn codegen(self) -> CodeGenerator<'a> {
CodeGenerator::new().with_options(CodegenOptions { single_quote: true })
}
#[allow(clippy::unused_self)]

View file

@ -6,7 +6,7 @@ mod oxc;
// mod terser;
use oxc_allocator::Allocator;
use oxc_codegen::WhitespaceRemover;
use oxc_codegen::{CodegenOptions, WhitespaceRemover};
use oxc_minifier::{CompressOptions, Minifier, MinifierOptions};
use oxc_parser::Parser;
use oxc_span::SourceType;
@ -20,7 +20,10 @@ pub(crate) fn minify(
let ret = Parser::new(&allocator, source_text, source_type).parse();
let program = allocator.alloc(ret.program);
Minifier::new(options).build(&allocator, program);
WhitespaceRemover::new().build(program).source_text
WhitespaceRemover::new()
.with_options(CodegenOptions { single_quote: true })
.build(program)
.source_text
}
pub(crate) fn test(source_text: &str, expected: &str) {

View file

@ -1,5 +1,5 @@
use oxc_allocator::Allocator;
use oxc_codegen::CodeGenerator;
use oxc_codegen::{CodeGenerator, CodegenOptions};
use oxc_minifier::RemoveDeadCode;
use oxc_parser::Parser;
use oxc_span::SourceType;
@ -12,7 +12,10 @@ fn print(source_text: &str, remove_dead_code: bool) -> String {
if remove_dead_code {
RemoveDeadCode::new(&allocator).build(program);
}
CodeGenerator::new().build(program).source_text
CodeGenerator::new()
.with_options(CodegenOptions { single_quote: true })
.build(program)
.source_text
}
pub(crate) fn test(source_text: &str, expected: &str) {

View file

@ -1,5 +1,5 @@
use oxc_allocator::Allocator;
use oxc_codegen::WhitespaceRemover;
use oxc_codegen::{CodegenOptions, WhitespaceRemover};
use oxc_minifier::{ReplaceGlobalDefines, ReplaceGlobalDefinesConfig};
use oxc_parser::Parser;
use oxc_span::SourceType;
@ -11,7 +11,10 @@ pub(crate) fn test(source_text: &str, expected: &str, config: ReplaceGlobalDefin
let ret = Parser::new(&allocator, source_text, source_type).parse();
let program = allocator.alloc(ret.program);
ReplaceGlobalDefines::new(&allocator, config).build(program);
WhitespaceRemover::new().build(program).source_text
WhitespaceRemover::new()
.with_options(CodegenOptions { single_quote: true })
.build(program)
.source_text
};
assert_eq!(minified, expected, "for source {source_text}");
}

File diff suppressed because it is too large Load diff

View file

@ -22,9 +22,9 @@ export type A = {
[(globalThis.Symbol).unscopables]: number;
[aliasing.isConcatSpreadable]: number;
[1]: number;
['2']: number;
["2"]: number;
[(missing2)]: number;
[Math.random() > 0.5 ? 'f1' : 'f2']: number;
[Math.random() > 0.5 ? "f1" : "f2"]: number;
};
export interface B {
[missing]: number;
@ -35,9 +35,9 @@ export interface B {
[(globalThis.Symbol).unscopables]: number;
[aliasing.isConcatSpreadable]: number;
[1]: number;
['2']: number;
["2"]: number;
[(missing2)]: number;
[Math.random() > 0.5 ? 'f1' : 'f2']: number;
[Math.random() > 0.5 ? "f1" : "f2"]: number;
}
export class C {
[missing]: number = 1;
@ -48,9 +48,9 @@ export class C {
[(globalThis.Symbol).unscopables]: number = 1;
[aliasing.isConcatSpreadable]: number = 1;
[1]: number = 1;
['2']: number = 1;
["2"]: number = 1;
[(missing2)]: number = 1;
[Math.random() > 0.5 ? 'f1' : 'f2']: number = 1;
[Math.random() > 0.5 ? "f1" : "f2"]: number = 1;
}
export const D = {
[missing]: 1,
@ -61,9 +61,9 @@ export const D = {
[(globalThis.Symbol).unscopables]: 1,
[aliasing.isConcatSpreadable]: 1,
[1]: 1,
['2']: 1,
["2"]: 1,
[(missing2)]: 1,
[Math.random() > 0.5 ? 'f1' : 'f2']: 1
[Math.random() > 0.5 ? "f1" : "f2"]: 1
};
//// [declarationComputedPropertyNames.d.ts] ////
@ -80,9 +80,9 @@ export type A = {
[(globalThis.Symbol).unscopables]: number;
[aliasing.isConcatSpreadable]: number;
[1]: number;
['2']: number;
["2"]: number;
[(missing2)]: number;
[Math.random() > 0.5 ? 'f1' : 'f2']: number;
[Math.random() > 0.5 ? "f1" : "f2"]: number;
};
export interface B {
[missing]: number;
@ -93,17 +93,17 @@ export interface B {
[(globalThis.Symbol).unscopables]: number;
[aliasing.isConcatSpreadable]: number;
[1]: number;
['2']: number;
["2"]: number;
[(missing2)]: number;
[Math.random() > 0.5 ? 'f1' : 'f2']: number;
[Math.random() > 0.5 ? "f1" : "f2"]: number;
}
export declare class C {
[1]: number;
['2']: number;
["2"]: number;
}
export declare const D: {
1: number;
'2': number;
"2": number;
};
export {};
x TS9010: Variable must have an explicit type annotation with