diff --git a/crates/oxc_linter/src/lib.rs b/crates/oxc_linter/src/lib.rs index db0a174f886d0..38c221f27bb98 100644 --- a/crates/oxc_linter/src/lib.rs +++ b/crates/oxc_linter/src/lib.rs @@ -31,7 +31,7 @@ pub use crate::{ fixer::FixKind, frameworks::FrameworkFlags, options::{AllowWarnDeny, LintOptions}, - rule::{RuleCategory, RuleMeta, RuleWithSeverity}, + rule::{RuleCategory, RuleFixMeta, RuleMeta, RuleWithSeverity}, service::{LintService, LintServiceOptions}, }; use crate::{ @@ -146,7 +146,7 @@ impl Linter { pub fn print_rules(writer: &mut W) { let table = RuleTable::new(); for section in table.sections { - writeln!(writer, "{}", section.render_markdown_table()).unwrap(); + writeln!(writer, "{}", section.render_markdown_table(None)).unwrap(); } writeln!(writer, "Default: {}", table.turned_on_by_default_count).unwrap(); writeln!(writer, "Total: {}", table.total).unwrap(); diff --git a/crates/oxc_linter/src/rule.rs b/crates/oxc_linter/src/rule.rs index 2d240170fa6f4..3fb081fb1315d 100644 --- a/crates/oxc_linter/src/rule.rs +++ b/crates/oxc_linter/src/rule.rs @@ -117,7 +117,7 @@ impl fmt::Display for RuleCategory { // NOTE: this could be packed into a single byte if we wanted. I don't think // this is needed, but we could do it if it would have a performance impact. -/// Describes the auto-fixing capabilities of a [`Rule`]. +/// Describes the auto-fixing capabilities of a `Rule`. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub enum RuleFixMeta { /// An auto-fix is not available. @@ -132,7 +132,12 @@ pub enum RuleFixMeta { } impl RuleFixMeta { - /// Does this [`Rule`] have some kind of auto-fix available? + #[inline] + pub fn is_none(self) -> bool { + matches!(self, Self::None) + } + + /// Does this `Rule` have some kind of auto-fix available? /// /// Also returns `true` for suggestions. #[inline] @@ -140,6 +145,11 @@ impl RuleFixMeta { matches!(self, Self::Fixable(_) | Self::Conditional(_)) } + #[inline] + pub fn is_pending(self) -> bool { + matches!(self, Self::FixPending) + } + pub fn supports_fix(self, kind: FixKind) -> bool { matches!(self, Self::Fixable(fix_kind) | Self::Conditional(fix_kind) if fix_kind.can_apply(kind)) } @@ -163,9 +173,10 @@ impl RuleFixMeta { let mut message = if kind.is_dangerous() { format!("dangerous {noun}") } else { noun.into() }; - let article = match message.chars().next().unwrap() { - 'a' | 'e' | 'i' | 'o' | 'u' => "An", - _ => "A", + let article = match message.chars().next() { + Some('a' | 'e' | 'i' | 'o' | 'u') => "An", + Some(_) => "A", + None => unreachable!(), }; if matches!(self, Self::Conditional(_)) { diff --git a/crates/oxc_linter/src/rules/import/namespace.rs b/crates/oxc_linter/src/rules/import/namespace.rs index 9209b11b20d44..af1264baec0db 100644 --- a/crates/oxc_linter/src/rules/import/namespace.rs +++ b/crates/oxc_linter/src/rules/import/namespace.rs @@ -40,10 +40,12 @@ pub struct Namespace { declare_oxc_lint!( /// ### What it does - /// Enforces names exist at the time they are dereferenced, when imported as a full namespace (i.e. import * as foo from './foo'; foo.bar(); will report if bar is not exported by ./foo.). - /// Will report at the import declaration if there are no exported names found. - /// Also, will report for computed references (i.e. foo["bar"]()). - /// Reports on assignment to a member of an imported namespace. + /// Enforces names exist at the time they are dereferenced, when imported as + /// a full namespace (i.e. `import * as foo from './foo'; foo.bar();` will + /// report if bar is not exported by `./foo.`). Will report at the import + /// declaration if there are no exported names found. Also, will report for + /// computed references (i.e. `foo["bar"]()`). Reports on assignment to a + /// member of an imported namespace. Namespace, correctness ); diff --git a/crates/oxc_linter/src/rules/jsx_a11y/anchor_is_valid.rs b/crates/oxc_linter/src/rules/jsx_a11y/anchor_is_valid.rs index 858e5d7c2ae63..5818637998967 100644 --- a/crates/oxc_linter/src/rules/jsx_a11y/anchor_is_valid.rs +++ b/crates/oxc_linter/src/rules/jsx_a11y/anchor_is_valid.rs @@ -56,7 +56,7 @@ declare_oxc_lint!( /// /// Consider the following: /// - /// ```javascript + /// ```jsx /// Perform action /// Perform action /// Perform action @@ -64,7 +64,7 @@ declare_oxc_lint!( /// /// All these anchor implementations indicate that the element is only used to execute JavaScript code. All the above should be replaced with: /// - /// ```javascript + /// ```jsx /// /// ``` /// ` @@ -78,33 +78,19 @@ declare_oxc_lint!( /// /// #### Valid /// - /// ```javascript + /// ```jsx /// navigate here - /// ``` - /// - /// ```javascript /// navigate here - /// ``` - /// - /// ```javascript /// navigate here /// ``` /// /// #### Invalid /// - /// ```javascript + /// ```jsx /// navigate here - /// ``` - /// ```javascript /// navigate here - /// ``` - /// ```javascript /// navigate here - /// ``` - /// ```javascript /// navigate here - /// ``` - /// ```javascript /// navigate here /// ``` /// diff --git a/crates/oxc_linter/src/rules/jsx_a11y/lang.rs b/crates/oxc_linter/src/rules/jsx_a11y/lang.rs index a16e28a21dd41..4c894be11e8bd 100644 --- a/crates/oxc_linter/src/rules/jsx_a11y/lang.rs +++ b/crates/oxc_linter/src/rules/jsx_a11y/lang.rs @@ -26,7 +26,7 @@ pub struct Lang; declare_oxc_lint!( /// ### What it does /// - /// The lang prop on the element must be a valid IETF's BCP 47 language tag. + /// The lang prop on the `` element must be a valid IETF's BCP 47 language tag. /// /// ### Why is this bad? /// @@ -39,13 +39,13 @@ declare_oxc_lint!( /// ### Example /// /// // good - /// ```javascript + /// ```jsx /// /// /// ``` /// /// // bad - /// ```javascript + /// ```jsx /// /// /// ```` diff --git a/crates/oxc_linter/src/rules/jsx_a11y/no_distracting_elements.rs b/crates/oxc_linter/src/rules/jsx_a11y/no_distracting_elements.rs index faf44d9c20ffe..88bfa3de54efc 100644 --- a/crates/oxc_linter/src/rules/jsx_a11y/no_distracting_elements.rs +++ b/crates/oxc_linter/src/rules/jsx_a11y/no_distracting_elements.rs @@ -22,15 +22,17 @@ declare_oxc_lint!( /// /// ### Why is this necessary? /// - /// Elements that can be visually distracting can cause accessibility issues with visually impaired users. - /// Such elements are most likely deprecated, and should be avoided. By default, and elements are visually distracting. + /// Elements that can be visually distracting can cause accessibility issues + /// with visually impaired users. Such elements are most likely deprecated, + /// and should be avoided. By default, `` and `` elements + /// are visually distracting. /// /// ### What it checks /// /// This rule checks for marquee and blink element. /// /// ### Example - /// ```javascript + /// ```jsx /// // Bad /// /// diff --git a/crates/oxc_linter/src/rules/jsx_a11y/scope.rs b/crates/oxc_linter/src/rules/jsx_a11y/scope.rs index ec7662aa6422a..5b0d22ed23427 100644 --- a/crates/oxc_linter/src/rules/jsx_a11y/scope.rs +++ b/crates/oxc_linter/src/rules/jsx_a11y/scope.rs @@ -23,7 +23,7 @@ pub struct Scope; declare_oxc_lint!( /// ### What it does /// - /// The scope prop should be used only on elements. + /// The scope prop should be used only on `` elements. /// /// ### Why is this bad? /// The scope attribute makes table navigation much easier for screen reader users, provided that it is used correctly. @@ -31,7 +31,7 @@ declare_oxc_lint!( /// A screen reader operates under the assumption that a table has a header and that this header specifies a scope. Because of the way screen readers function, having an accurate header makes viewing a table far more accessible and more efficient for people who use the device. /// /// ### Example - /// ```javascript + /// ```jsx /// // Bad ///
/// diff --git a/crates/oxc_linter/src/rules/nextjs/no_duplicate_head.rs b/crates/oxc_linter/src/rules/nextjs/no_duplicate_head.rs index ef05f50fdad25..2c6cb5fbc51d3 100644 --- a/crates/oxc_linter/src/rules/nextjs/no_duplicate_head.rs +++ b/crates/oxc_linter/src/rules/nextjs/no_duplicate_head.rs @@ -10,13 +10,13 @@ pub struct NoDuplicateHead; declare_oxc_lint!( /// ### What it does - /// Prevent duplicate usage of in pages/_document.js. + /// Prevent duplicate usage of `` in `pages/_document.js``. /// /// ### Why is this bad? /// This can cause unexpected behavior in your application. /// /// ### Example - /// ```javascript + /// ```jsx /// import Document, { Html, Head, Main, NextScript } from 'next/document' /// class MyDocument extends Document { /// static async getInitialProps(ctx) { diff --git a/crates/oxc_linter/src/rules/typescript/no_this_alias.rs b/crates/oxc_linter/src/rules/typescript/no_this_alias.rs index 3f7eb0b78e5f0..9d8f618950d88 100644 --- a/crates/oxc_linter/src/rules/typescript/no_this_alias.rs +++ b/crates/oxc_linter/src/rules/typescript/no_this_alias.rs @@ -59,12 +59,14 @@ declare_oxc_lint!( /// /// ### Why is this bad? /// - /// Generic type parameters () in TypeScript may be "constrained" with an extends keyword. - /// When no extends is provided, type parameters default a constraint to unknown. It is therefore redundant to extend from any or unknown. + /// Generic type parameters (``) in TypeScript may be "constrained" with + /// an extends keyword. When no extends is provided, type parameters + /// default a constraint to unknown. It is therefore redundant to extend + /// from any or unknown. /// - /// the rule doesn't allow const {allowedName} = this + /// the rule doesn't allow `const {allowedName} = this` /// this is to keep 1:1 with eslint implementation - /// sampe with obj. = this + /// sampe with `obj. = this` /// ``` NoThisAlias, correctness diff --git a/crates/oxc_linter/src/rules/typescript/no_unnecessary_type_constraint.rs b/crates/oxc_linter/src/rules/typescript/no_unnecessary_type_constraint.rs index be98cad392d1a..f4289633c4fcb 100644 --- a/crates/oxc_linter/src/rules/typescript/no_unnecessary_type_constraint.rs +++ b/crates/oxc_linter/src/rules/typescript/no_unnecessary_type_constraint.rs @@ -28,11 +28,11 @@ declare_oxc_lint!( /// /// ### Why is this bad? /// - /// Generic type parameters () in TypeScript may be "constrained" with an extends keyword. + /// Generic type parameters (``) in TypeScript may be "constrained" with an extends keyword. /// When no extends is provided, type parameters default a constraint to unknown. It is therefore redundant to extend from any or unknown. /// /// ### Example - /// ```javascript + /// ```typescript /// interface FooAny {} /// interface FooUnknown {} /// type BarAny = {}; diff --git a/crates/oxc_linter/src/table.rs b/crates/oxc_linter/src/table.rs index e59c488da0737..ab45d824a6bd7 100644 --- a/crates/oxc_linter/src/table.rs +++ b/crates/oxc_linter/src/table.rs @@ -1,8 +1,8 @@ -use std::fmt::Write; +use std::{borrow::Cow, fmt::Write}; use rustc_hash::{FxHashMap, FxHashSet}; -use crate::{rules::RULES, Linter, RuleCategory}; +use crate::{rules::RULES, Linter, RuleCategory, RuleFixMeta}; pub struct RuleTable { pub sections: Vec, @@ -23,6 +23,7 @@ pub struct RuleTableRow { pub category: RuleCategory, pub documentation: Option<&'static str>, pub turned_on_by_default: bool, + pub autofix: RuleFixMeta, } impl Default for RuleTable { @@ -49,6 +50,7 @@ impl RuleTable { plugin: rule.plugin_name().to_string(), category: rule.category(), turned_on_by_default: default_rules.contains(name), + autofix: rule.fix(), } }) .collect::>(); @@ -88,7 +90,11 @@ impl RuleTable { } impl RuleTableSection { - pub fn render_markdown_table(&self) -> String { + /// Renders all the rules in this section as a markdown table. + /// + /// Provide [`Some`] prefix to render the rule name as a link. Provide + /// [`None`] to just display the rule name as text. + pub fn render_markdown_table(&self, link_prefix: Option<&str>) -> String { let mut s = String::new(); let category = &self.category; let rows = &self.rows; @@ -108,7 +114,12 @@ impl RuleTableSection { let plugin_name = &row.plugin; let (default, default_width) = if row.turned_on_by_default { ("✅", 6) } else { ("", 7) }; - writeln!(s, "| {rule_name:( parent_kind: Some(KnownMemberExpressionParentKind::Call), grandparent_kind: None, }; - let chain = get_node_chain(¶ms); + let mut chain = get_node_chain(¶ms); let all_member_expr_except_last = chain.iter().rev().skip(1).all(|member| { matches!(member.parent_kind, Some(KnownMemberExpressionParentKind::Member)) @@ -57,16 +57,14 @@ pub fn parse_jest_fn_call<'a>( let name = resolved.original.unwrap_or(resolved.local); let kind = JestFnKind::from(name); - let mut members = Vec::new(); - let mut iter = chain.into_iter(); - let head = iter.next()?; - let rest = iter; // every member node must have a member expression as their parent // in order to be part of the call chain we're parsing - for member in rest { - members.push(member); - } + let (head, members) = { + let rest = chain.split_off(1); + let head = chain.into_iter().next().unwrap(); + (head, rest) + }; if matches!(kind, JestFnKind::Expect | JestFnKind::ExpectTypeOf) { let options = ExpectFnCallOptions { @@ -465,6 +463,14 @@ struct NodeChainParams<'a> { /// Port from [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest/blob/a058f22f94774eeea7980ea2d1f24c6808bf3e2c/src/rules/utils/parseJestFnCall.ts#L36-L51) fn get_node_chain<'a>(params: &NodeChainParams<'a>) -> Vec> { let mut chain = Vec::new(); + recurse_extend_node_chain(params, &mut chain); + chain +} + +fn recurse_extend_node_chain<'a>( + params: &NodeChainParams<'a>, + chain: &mut Vec>, +) { let NodeChainParams { expr, parent, parent_kind, grandparent_kind } = params; match expr { @@ -477,7 +483,7 @@ fn get_node_chain<'a>(params: &NodeChainParams<'a>) -> Vec(params: &NodeChainParams<'a>) -> Vec { let params = NodeChainParams { @@ -514,9 +519,7 @@ fn get_node_chain<'a>(params: &NodeChainParams<'a>) -> Vec { chain.push(KnownMemberExpressionProperty { @@ -538,8 +541,6 @@ fn get_node_chain<'a>(params: &NodeChainParams<'a>) -> Vec {} }; - - chain } // sorted list for binary search. diff --git a/crates/oxc_minifier/src/lib.rs b/crates/oxc_minifier/src/lib.rs index 94cdbff5cc2eb..d987a0d8ddf02 100644 --- a/crates/oxc_minifier/src/lib.rs +++ b/crates/oxc_minifier/src/lib.rs @@ -19,7 +19,7 @@ pub use crate::{ ast_passes::{CompressorPass, RemoveDeadCode, RemoveSyntax}, compressor::Compressor, options::CompressOptions, - plugins::{ReplaceGlobalDefines, ReplaceGlobalDefinesConfig}, + plugins::*, }; #[derive(Debug, Clone, Copy)] diff --git a/crates/oxc_minifier/src/plugins/inject_global_variables.rs b/crates/oxc_minifier/src/plugins/inject_global_variables.rs new file mode 100644 index 0000000000000..54988e28db38d --- /dev/null +++ b/crates/oxc_minifier/src/plugins/inject_global_variables.rs @@ -0,0 +1,226 @@ +use std::sync::Arc; + +use oxc_allocator::Allocator; +use oxc_ast::{ast::*, visit::walk_mut, AstBuilder, VisitMut}; +use oxc_semantic::{ScopeTree, SymbolTable}; +use oxc_span::{CompactStr, SPAN}; + +use super::replace_global_defines::{DotDefine, ReplaceGlobalDefines}; + +#[derive(Debug, Clone)] +pub struct InjectGlobalVariablesConfig { + injects: Arc<[InjectImport]>, +} + +impl InjectGlobalVariablesConfig { + pub fn new(injects: Vec) -> Self { + Self { injects: Arc::from(injects) } + } +} + +#[derive(Debug, Clone)] +pub struct InjectImport { + /// `import _ from `source` + source: CompactStr, + specifier: InjectImportSpecifier, + /// value to be replaced for `specifier.local` if it's a `StaticMemberExpression` in the form of `foo.bar.baz`. + replace_value: Option, +} + +impl InjectImport { + pub fn named_specifier(source: &str, imported: Option<&str>, local: &str) -> InjectImport { + InjectImport { + source: CompactStr::from(source), + specifier: InjectImportSpecifier::Specifier { + imported: imported.map(CompactStr::from), + local: CompactStr::from(local), + }, + replace_value: Self::replace_name(local), + } + } + + pub fn namespace_specifier(source: &str, local: &str) -> InjectImport { + InjectImport { + source: CompactStr::from(source), + specifier: InjectImportSpecifier::NamespaceSpecifier { local: CompactStr::from(local) }, + replace_value: Self::replace_name(local), + } + } + + pub fn default_specifier(source: &str, local: &str) -> InjectImport { + InjectImport { + source: CompactStr::from(source), + specifier: InjectImportSpecifier::DefaultSpecifier { local: CompactStr::from(local) }, + replace_value: Self::replace_name(local), + } + } + + fn replace_name(local: &str) -> Option { + local + .contains('.') + .then(|| CompactStr::from(format!("$inject_{}", local.replace('.', "_")))) + } +} + +#[derive(Debug, Clone)] +pub enum InjectImportSpecifier { + /// `import { local } from "source"` + /// `import { default as local } from "source"` when `imported` is `None` + Specifier { imported: Option, local: CompactStr }, + /// import * as local from "source" + NamespaceSpecifier { local: CompactStr }, + /// import local from "source" + DefaultSpecifier { local: CompactStr }, +} + +impl InjectImportSpecifier { + fn local(&self) -> &CompactStr { + match self { + Self::Specifier { local, .. } + | Self::NamespaceSpecifier { local, .. } + | Self::DefaultSpecifier { local, .. } => local, + } + } +} + +impl From<&InjectImport> for DotDefine { + fn from(inject: &InjectImport) -> Self { + let parts = inject.specifier.local().split('.').map(CompactStr::from).collect::>(); + let value = inject.replace_value.clone().unwrap(); + Self { parts, value } + } +} + +/// Injects import statements for global variables. +/// +/// References: +/// +/// * +pub struct InjectGlobalVariables<'a> { + ast: AstBuilder<'a>, + config: InjectGlobalVariablesConfig, + + // states + /// Dot defines derived from the config. + dot_defines: Vec, + + /// Identifiers for which dot define replaced a member expression. + replaced_dot_defines: + Vec<(/* identifier of member expression */ CompactStr, /* local */ CompactStr)>, +} + +impl<'a> VisitMut<'a> for InjectGlobalVariables<'a> { + fn visit_expression(&mut self, expr: &mut Expression<'a>) { + self.replace_dot_defines(expr); + walk_mut::walk_expression(self, expr); + } +} + +impl<'a> InjectGlobalVariables<'a> { + pub fn new(allocator: &'a Allocator, config: InjectGlobalVariablesConfig) -> Self { + Self { + ast: AstBuilder::new(allocator), + config, + dot_defines: vec![], + replaced_dot_defines: vec![], + } + } + + pub fn build( + &mut self, + _symbols: &mut SymbolTable, // will be used to keep symbols in sync + scopes: &mut ScopeTree, + program: &mut Program<'a>, + ) { + // Step 1: slow path where visiting the AST is required to replace dot defines. + let dot_defines = self + .config + .injects + .iter() + .filter(|i| i.replace_value.is_some()) + .map(DotDefine::from) + .collect::>(); + + if !dot_defines.is_empty() { + self.dot_defines = dot_defines; + self.visit_program(program); + } + + // Step 2: find all the injects that are referenced. + let injects = self + .config + .injects + .iter() + .filter(|i| { + // remove replaced `Buffer` for `Buffer` + Buffer.isBuffer` combo. + if let Some(replace_value) = &i.replace_value { + self.replaced_dot_defines.iter().any(|d| d.1 == replace_value) + } else if self.replaced_dot_defines.iter().any(|d| d.0 == i.specifier.local()) { + false + } else { + scopes.root_unresolved_references().contains_key(i.specifier.local()) + } + }) + .cloned() + .collect::>(); + + if injects.is_empty() { + return; + } + + self.inject_imports(&injects, program); + } + + fn inject_imports(&self, injects: &[InjectImport], program: &mut Program<'a>) { + let imports = injects.iter().map(|inject| { + let specifiers = Some(self.ast.vec1(self.inject_import_to_specifier(inject))); + let source = self.ast.string_literal(SPAN, inject.source.as_str()); + let kind = ImportOrExportKind::Value; + let import_decl = self + .ast + .module_declaration_import_declaration(SPAN, specifiers, source, None, kind); + self.ast.statement_module_declaration(import_decl) + }); + program.body.splice(0..0, imports); + } + + fn inject_import_to_specifier(&self, inject: &InjectImport) -> ImportDeclarationSpecifier<'a> { + match &inject.specifier { + InjectImportSpecifier::Specifier { imported, local } => { + let imported = imported.as_deref().unwrap_or("default"); + let local = inject.replace_value.as_ref().unwrap_or(local).as_str(); + self.ast.import_declaration_specifier_import_specifier( + SPAN, + self.ast.module_export_name_identifier_name(SPAN, imported), + self.ast.binding_identifier(SPAN, local), + ImportOrExportKind::Value, + ) + } + InjectImportSpecifier::DefaultSpecifier { local } => { + let local = inject.replace_value.as_ref().unwrap_or(local).as_str(); + let local = self.ast.binding_identifier(SPAN, local); + self.ast.import_declaration_specifier_import_default_specifier(SPAN, local) + } + InjectImportSpecifier::NamespaceSpecifier { local } => { + let local = inject.replace_value.as_ref().unwrap_or(local).as_str(); + let local = self.ast.binding_identifier(SPAN, local); + self.ast.import_declaration_specifier_import_namespace_specifier(SPAN, local) + } + } + } + + fn replace_dot_defines(&mut self, expr: &mut Expression<'a>) { + if let Expression::StaticMemberExpression(member) = expr { + for dot_define in &self.dot_defines { + if ReplaceGlobalDefines::is_dot_define(dot_define, member) { + let value = + self.ast.expression_identifier_reference(SPAN, dot_define.value.as_str()); + *expr = value; + self.replaced_dot_defines + .push((dot_define.parts[0].clone(), dot_define.value.clone())); + break; + } + } + } + } +} diff --git a/crates/oxc_minifier/src/plugins/mod.rs b/crates/oxc_minifier/src/plugins/mod.rs index 6dddd71e9301f..fdf6d786e022c 100644 --- a/crates/oxc_minifier/src/plugins/mod.rs +++ b/crates/oxc_minifier/src/plugins/mod.rs @@ -1,3 +1,5 @@ +mod inject_global_variables; mod replace_global_defines; -pub use replace_global_defines::{ReplaceGlobalDefines, ReplaceGlobalDefinesConfig}; +pub use inject_global_variables::*; +pub use replace_global_defines::*; diff --git a/crates/oxc_minifier/src/plugins/replace_global_defines.rs b/crates/oxc_minifier/src/plugins/replace_global_defines.rs index bfe4118e520b2..ecdf31acd464f 100644 --- a/crates/oxc_minifier/src/plugins/replace_global_defines.rs +++ b/crates/oxc_minifier/src/plugins/replace_global_defines.rs @@ -4,7 +4,7 @@ use oxc_allocator::Allocator; use oxc_ast::{ast::*, visit::walk_mut, AstBuilder, VisitMut}; use oxc_diagnostics::OxcDiagnostic; use oxc_parser::Parser; -use oxc_span::SourceType; +use oxc_span::{CompactStr, SourceType}; use oxc_syntax::identifier::is_identifier_name; /// Configuration for [ReplaceGlobalDefines]. @@ -18,13 +18,26 @@ pub struct ReplaceGlobalDefinesConfig(Arc); #[derive(Debug)] struct ReplaceGlobalDefinesConfigImpl { - identifier_defines: Vec<(/* key */ String, /* value */ String)>, - dot_defines: Vec<(/* member expression parts */ Vec, /* value */ String)>, + identifier_defines: Vec<(/* key */ CompactStr, /* value */ CompactStr)>, + dot_defines: Vec, +} + +#[derive(Debug)] +pub struct DotDefine { + /// Member expression parts + pub parts: Vec, + pub value: CompactStr, +} + +impl DotDefine { + fn new(parts: Vec, value: CompactStr) -> Self { + Self { parts, value } + } } enum IdentifierType { Identifier, - DotDefines(Vec), + DotDefines(Vec), } impl ReplaceGlobalDefinesConfig { @@ -44,10 +57,10 @@ impl ReplaceGlobalDefinesConfig { match Self::check_key(key)? { IdentifierType::Identifier => { - identifier_defines.push((key.to_string(), value.to_string())); + identifier_defines.push((CompactStr::new(key), CompactStr::new(value))); } IdentifierType::DotDefines(parts) => { - dot_defines.push((parts, value.to_string())); + dot_defines.push(DotDefine::new(parts, CompactStr::new(value))); } } } @@ -73,7 +86,7 @@ impl ReplaceGlobalDefinesConfig { } } - Ok(IdentifierType::DotDefines(parts.iter().map(ToString::to_string).collect())) + Ok(IdentifierType::DotDefines(parts.iter().map(|s| CompactStr::new(s)).collect())) } fn check_value(allocator: &Allocator, source_text: &str) -> Result<(), Vec> { @@ -94,6 +107,14 @@ pub struct ReplaceGlobalDefines<'a> { config: ReplaceGlobalDefinesConfig, } +impl<'a> VisitMut<'a> for ReplaceGlobalDefines<'a> { + fn visit_expression(&mut self, expr: &mut Expression<'a>) { + self.replace_identifier_defines(expr); + self.replace_dot_defines(expr); + walk_mut::walk_expression(self, expr); + } +} + impl<'a> ReplaceGlobalDefines<'a> { pub fn new(allocator: &'a Allocator, config: ReplaceGlobalDefinesConfig) -> Self { Self { ast: AstBuilder::new(allocator), config } @@ -125,53 +146,50 @@ impl<'a> ReplaceGlobalDefines<'a> { } } - fn replace_dot_defines(&self, expr: &mut Expression<'a>) { + fn replace_dot_defines(&mut self, expr: &mut Expression<'a>) { if let Expression::StaticMemberExpression(member) = expr { - 'outer: for (parts, value) in &self.config.0.dot_defines { - assert!(parts.len() > 1); - - let mut current_part_member_expression = Some(&*member); - let mut cur_part_name = &member.property.name; + for dot_define in &self.config.0.dot_defines { + if Self::is_dot_define(dot_define, member) { + let value = self.parse_value(&dot_define.value); + *expr = value; + break; + } + } + } + } - for (i, part) in parts.iter().enumerate().rev() { - if cur_part_name.as_str() != part { - continue 'outer; - } + pub fn is_dot_define(dot_define: &DotDefine, member: &StaticMemberExpression<'a>) -> bool { + debug_assert!(dot_define.parts.len() > 1); - if i == 0 { - break; - } + let mut current_part_member_expression = Some(member); + let mut cur_part_name = &member.property.name; - current_part_member_expression = - if let Some(member) = current_part_member_expression { - match &member.object.without_parenthesized() { - Expression::StaticMemberExpression(member) => { - cur_part_name = &member.property.name; - Some(member) - } - Expression::Identifier(ident) => { - cur_part_name = &ident.name; - None - } - _ => None, - } - } else { - continue 'outer; - }; - } + for (i, part) in dot_define.parts.iter().enumerate().rev() { + if cur_part_name.as_str() != part { + return false; + } - let value = self.parse_value(value); - *expr = value; + if i == 0 { break; } + + current_part_member_expression = if let Some(member) = current_part_member_expression { + match &member.object { + Expression::StaticMemberExpression(member) => { + cur_part_name = &member.property.name; + Some(member) + } + Expression::Identifier(ident) => { + cur_part_name = &ident.name; + None + } + _ => None, + } + } else { + return false; + }; } - } -} -impl<'a> VisitMut<'a> for ReplaceGlobalDefines<'a> { - fn visit_expression(&mut self, expr: &mut Expression<'a>) { - self.replace_identifier_defines(expr); - self.replace_dot_defines(expr); - walk_mut::walk_expression(self, expr); + true } } diff --git a/crates/oxc_minifier/tests/plugins/inject_global_variables.rs b/crates/oxc_minifier/tests/plugins/inject_global_variables.rs new file mode 100644 index 0000000000000..8e2be3a8b28b4 --- /dev/null +++ b/crates/oxc_minifier/tests/plugins/inject_global_variables.rs @@ -0,0 +1,341 @@ +//! References +//! +//! * + +use oxc_allocator::Allocator; +use oxc_codegen::{CodeGenerator, CodegenOptions}; +use oxc_minifier::{InjectGlobalVariables, InjectGlobalVariablesConfig, InjectImport}; +use oxc_parser::Parser; +use oxc_semantic::SemanticBuilder; +use oxc_span::SourceType; + +use crate::run; + +pub(crate) fn test(source_text: &str, expected: &str, config: InjectGlobalVariablesConfig) { + let source_type = SourceType::default(); + let allocator = Allocator::default(); + let ret = Parser::new(&allocator, source_text, source_type).parse(); + let program = allocator.alloc(ret.program); + let (mut symbols, mut scopes) = SemanticBuilder::new(source_text, source_type) + .build(program) + .semantic + .into_symbol_table_and_scope_tree(); + InjectGlobalVariables::new(&allocator, config).build(&mut symbols, &mut scopes, program); + let result = CodeGenerator::new() + .with_options(CodegenOptions { single_quote: true }) + .build(program) + .source_text; + let expected = run(expected, source_type, None); + assert_eq!(result, expected, "for source {source_text}"); +} + +fn test_same(source_text: &str, config: InjectGlobalVariablesConfig) { + test(source_text, source_text, config); +} + +#[test] +fn default() { + let config = + InjectGlobalVariablesConfig::new(vec![InjectImport::named_specifier("jquery", None, "$")]); + test( + " + $(() => { + console.log('ready'); + }); + ", + " + import { default as $ } from 'jquery' + $(() => { + console.log('ready'); + }); + ", + config, + ); +} + +#[test] +fn basic() { + // inserts a default import statement + let config = + InjectGlobalVariablesConfig::new(vec![InjectImport::named_specifier("jquery", None, "$")]); + test( + " + $(() => { + console.log('ready'); + }); + ", + " + import { default as $ } from 'jquery' + $(() => { + console.log('ready'); + }); + ", + config, + ); + // inserts a default import statement + let config = + InjectGlobalVariablesConfig::new(vec![InjectImport::named_specifier("d'oh", None, "$")]); + test( + " + $(() => { + console.log('ready'); + }); + ", + r#" + import { default as $ } from "d\'oh" + $(() => { + console.log('ready'); + }); + "#, + config, + ); +} + +#[test] +fn named() { + // inserts a named import statement + let config = InjectGlobalVariablesConfig::new(vec![InjectImport::named_specifier( + "es6-promise", + Some("Promise"), + "Promise", + )]); + test( + "Promise.all([thisThing, thatThing]).then(() => someOtherThing);", + " + import { Promise as Promise } from 'es6-promise'; + Promise.all([thisThing, thatThing]).then(() => someOtherThing); + ", + config, + ); +} + +#[test] +fn keypaths() { + // overwrites keypaths + let config = InjectGlobalVariablesConfig::new(vec![InjectImport::named_specifier( + "fixtures/keypaths/polyfills/object-assign.js", + None, + "Object.assign", + )]); + test( + " + const original = { foo: 'bar' }; + const clone = Object.assign({}, original); + export default clone; + ", + " + import { default as $inject_Object_assign } from 'fixtures/keypaths/polyfills/object-assign.js' + const original = { foo: 'bar' }; + const clone = $inject_Object_assign({}, original); + export default clone; + ", + config, + ); +} + +#[test] +fn existing() { + // ignores existing imports + let config = + InjectGlobalVariablesConfig::new(vec![InjectImport::named_specifier("jquery", None, "$")]); + test_same( + " + import $ from 'jquery'; + $(() => { + console.log('ready'); + }); + ", + config, + ); +} + +#[test] +fn shadowing() { + // handles shadowed variables + let config = + InjectGlobalVariablesConfig::new(vec![InjectImport::named_specifier("jquery", None, "$")]); + test_same( + " + function launch($) { + $(() => { + console.log('ready'); + }); + } + launch((fn) => fn()); + ", + config, + ); +} + +#[test] +fn shorthand() { + // handles shorthand properties + let config = InjectGlobalVariablesConfig::new(vec![InjectImport::named_specifier( + "es6-promise", + Some("Promise"), + "Promise", + )]); + test( + " + const polyfills = { Promise }; +polyfills.Promise.resolve().then(() => 'it works'); + ", + " + import { Promise as Promise } from 'es6-promise'; + const polyfills = { Promise }; +polyfills.Promise.resolve().then(() => 'it works'); + ", + config, + ); +} + +#[test] +fn shorthand_assignment() { + // handles shorthand properties (as assignment) + let config = InjectGlobalVariablesConfig::new(vec![InjectImport::named_specifier( + "es6-promise", + Some("Promise"), + "Promise", + )]); + test_same( + " + const { Promise = 'fallback' } = foo; + console.log(Promise); + ", + config, + ); +} + +#[test] +fn shorthand_func() { + // handles shorthand properties in function + let config = InjectGlobalVariablesConfig::new(vec![InjectImport::named_specifier( + "es6-promise", + Some("Promise"), + "Promise", + )]); + test_same( + " + function foo({Promise}) { + console.log(Promise); + } + foo(); + ", + config, + ); +} + +#[test] +fn shorthand_func_fallback() { + // handles shorthand properties in function (as fallback value)' + let config = InjectGlobalVariablesConfig::new(vec![InjectImport::named_specifier( + "es6-promise", + Some("Promise"), + "Promise", + )]); + test( + " + function foo({bar = Promise}) { + console.log(bar); + } + foo(); + ", + " + import { Promise as Promise } from 'es6-promise'; + function foo({bar = Promise}) { + console.log(bar); + } + foo(); + ", + config, + ); +} + +#[test] +fn redundant_keys() { + // handles redundant keys + let config = InjectGlobalVariablesConfig::new(vec![ + InjectImport::named_specifier("Buffer", None, "Buffer"), + InjectImport::named_specifier("is-buffer", None, "Buffer.isBuffer"), + ]); + test( + "Buffer.isBuffer('foo');", + " + import { default as $inject_Buffer_isBuffer } from 'is-buffer'; + $inject_Buffer_isBuffer('foo'); + ", + config.clone(), + ); + + // not found + test_same("Foo.Bar('foo');", config); +} + +#[test] +fn import_namespace() { + // generates * imports + let config = + InjectGlobalVariablesConfig::new(vec![InjectImport::namespace_specifier("foo", "foo")]); + test( + " + console.log(foo.bar); + console.log(foo.baz); + ", + " + import * as foo from 'foo'; + console.log(foo.bar); + console.log(foo.baz); + ", + config, + ); +} + +#[test] +fn non_js() { + // transpiles non-JS files but handles failures to parse + let config = InjectGlobalVariablesConfig::new(vec![InjectImport::named_specifier( + "path", + Some("relative"), + "relative", + )]); + test_same( + " + import './styles.css'; + import foo from './foo.es6'; + assert.equal(foo, path.join('..', 'baz')); + ", + config, + ); +} + +#[test] +fn is_reference() { + // ignores check isReference is false + let config = + InjectGlobalVariablesConfig::new(vec![InjectImport::named_specifier("path", None, "bar")]); + test( + " + import { bar as foo } from 'path'; + console.log({ bar: foo }); + class Foo { + bar() { + console.log(this); + } + } + export { Foo }; + export { foo as bar }; + ", + " + import { bar as foo } from 'path'; + console.log({ bar: foo }); + class Foo { + bar() { + console.log(this); + } + } + export { Foo }; + export { foo as bar }; + ", + config, + ); +} diff --git a/crates/oxc_minifier/tests/plugins/mod.rs b/crates/oxc_minifier/tests/plugins/mod.rs index ae8a94f22eaf4..f43e90253926e 100644 --- a/crates/oxc_minifier/tests/plugins/mod.rs +++ b/crates/oxc_minifier/tests/plugins/mod.rs @@ -1 +1,2 @@ +mod inject_global_variables; mod replace_global_defines; diff --git a/tasks/website/src/linter/rules/doc_page.rs b/tasks/website/src/linter/rules/doc_page.rs new file mode 100644 index 0000000000000..2edfd76587b2a --- /dev/null +++ b/tasks/website/src/linter/rules/doc_page.rs @@ -0,0 +1,57 @@ +//! Create documentation pages for each rule. Pages are printed as Markdown and +//! get added to the website. + +use oxc_linter::{table::RuleTableRow, RuleFixMeta}; +use std::fmt::{self, Write}; + +use crate::linter::rules::html::HtmlWriter; + +pub fn render_rule_docs_page(rule: &RuleTableRow) -> Result { + const APPROX_FIX_CATEGORY_AND_PLUGIN_LEN: usize = 512; + let RuleTableRow { name, documentation, plugin, turned_on_by_default, autofix, .. } = rule; + + let mut page = HtmlWriter::with_capacity( + documentation.map_or(0, str::len) + name.len() + APPROX_FIX_CATEGORY_AND_PLUGIN_LEN, + ); + + writeln!( + page, + "\n", + file!() + )?; + writeln!(page, "# {plugin}/{name}\n")?; + + // rule metadata + page.div(r#"class="rule-meta""#, |p| { + if *turned_on_by_default { + p.span(r#"class="default-on""#, |p| { + p.writeln("✅ This rule is turned on by default.") + })?; + } + + if let Some(emoji) = fix_emoji(*autofix) { + p.span(r#"class="fix""#, |p| { + p.writeln(format!("{} {}", emoji, autofix.description())) + })?; + } + + Ok(()) + })?; + + // rule documentation + if let Some(docs) = documentation { + writeln!(page, "\n{}", *docs)?; + } + + // TODO: link to rule source + + Ok(page.into()) +} + +fn fix_emoji(fix: RuleFixMeta) -> Option<&'static str> { + match fix { + RuleFixMeta::None => None, + RuleFixMeta::FixPending => Some("🚧"), + RuleFixMeta::Conditional(_) | RuleFixMeta::Fixable(_) => Some("🛠️"), + } +} diff --git a/tasks/website/src/linter/rules/html.rs b/tasks/website/src/linter/rules/html.rs new file mode 100644 index 0000000000000..9441ad5dff622 --- /dev/null +++ b/tasks/website/src/linter/rules/html.rs @@ -0,0 +1,121 @@ +use std::{ + cell::RefCell, + fmt::{self, Write}, +}; + +#[derive(Debug)] +pub(crate) struct HtmlWriter { + inner: RefCell, +} + +impl fmt::Write for HtmlWriter { + #[inline] + fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result { + self.inner.get_mut().write_fmt(args) + } + + #[inline] + fn write_char(&mut self, c: char) -> fmt::Result { + self.inner.get_mut().write_char(c) + } + + #[inline] + fn write_str(&mut self, s: &str) -> fmt::Result { + self.inner.get_mut().write_str(s) + } +} + +impl From for String { + #[inline] + fn from(html: HtmlWriter) -> Self { + html.into_inner() + } +} + +impl HtmlWriter { + pub fn new() -> Self { + Self { inner: RefCell::new(String::new()) } + } + + pub fn with_capacity(capacity: usize) -> Self { + Self { inner: RefCell::new(String::with_capacity(capacity)) } + } + + pub fn writeln>(&self, line: S) -> fmt::Result { + writeln!(self.inner.borrow_mut(), "{}", line.as_ref()) + } + + pub fn into_inner(self) -> String { + self.inner.into_inner() + } + + pub fn html(&self, tag: &'static str, attrs: &str, inner: F) -> fmt::Result + where + F: FnOnce(&Self) -> fmt::Result, + { + // Allocate space for the HTML being printed + let write_amt_guess = { + // opening tag. 2 extra for '<' and '>' + 2 + tag.len() + attrs.len() + + // approximate inner content length + 256 + + // closing tag. 3 extra for '' + 3 + tag.len() + }; + let mut s = self.inner.borrow_mut(); + s.reserve(write_amt_guess); + + // Write the opening tag + write!(s, "<{tag}")?; + if attrs.is_empty() { + writeln!(s, ">")?; + } else { + writeln!(s, " {attrs}>")?; + } + + // Callback produces the inner content + drop(s); + inner(self)?; + + // Write the closing tag + writeln!(self.inner.borrow_mut(), "")?; + + Ok(()) + } +} + +macro_rules! make_tag { + ($name:ident) => { + impl HtmlWriter { + #[inline] + pub fn $name(&self, attrs: &str, inner: F) -> fmt::Result + where + F: FnOnce(&Self) -> fmt::Result, + { + self.html(stringify!($name), attrs, inner) + } + } + }; +} + +make_tag!(div); +make_tag!(span); + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_div() { + let html = HtmlWriter::new(); + html.div("", |html| html.writeln("Hello, world!")).unwrap(); + + assert_eq!( + html.into_inner().as_str(), + "
+Hello, world! +
+" + ); + } +} diff --git a/tasks/website/src/linter/rules/mod.rs b/tasks/website/src/linter/rules/mod.rs new file mode 100644 index 0000000000000..151a658d8c0a8 --- /dev/null +++ b/tasks/website/src/linter/rules/mod.rs @@ -0,0 +1,128 @@ +mod doc_page; +mod html; +mod table; + +use std::{ + borrow::Cow, + env, fs, + path::{Path, PathBuf}, + process, +}; + +use doc_page::render_rule_docs_page; +use oxc_linter::table::RuleTable; +use pico_args::Arguments; +use table::render_rules_table; + +const HELP: &str = " +usage: linter-rules [args] + +Arguments: + -t,--table Path to file where rule markdown table will be saved. + -r,--rule-docs Path to directory where rule doc pages will be saved. + A directory will be created if one doesn't exist. + -h,--help Show this help message. + +"; + +/// `cargo run -p website linter-rules --table +/// /path/to/oxc/oxc-project.github.io/src/docs/guide/usage/linter/generated-rules.md +/// --rule-docs /path/to/oxc/oxc-project.github.io/src/docs/guide/usage/linter/rules +/// ` +/// +pub fn print_rules(mut args: Arguments) { + let pwd = PathBuf::from(env::var("PWD").unwrap()); + if args.contains(["-h", "--help"]) { + println!("{HELP}"); + return; + } + + let table = RuleTable::new(); + let table_path = args.opt_value_from_str::<_, PathBuf>(["-t", "--table"]).unwrap(); + let rules_dir = args.opt_value_from_str::<_, PathBuf>(["-r", "--rule-docs"]).unwrap(); + + let (prefix, root) = rules_dir.as_ref().and_then(|p| p.as_os_str().to_str()).map_or( + (Cow::Borrowed(""), None), + |p| { + if p.contains("src/docs") { + let split = p.split("src/docs").collect::>(); + assert!(split.len() > 1); + let root = split[0]; + let root = pwd.join(root).canonicalize().unwrap(); + let prefix = Cow::Owned("/docs".to_string() + split.last().unwrap()); + (prefix, Some(root)) + } else { + (Cow::Borrowed(p), None) + } + }, + ); + + if let Some(table_path) = table_path { + let table_path = pwd.join(table_path).canonicalize().unwrap(); + + println!("Rendering rules table..."); + let rules_table = render_rules_table(&table, prefix.as_ref()); + fs::write(table_path, rules_table).unwrap(); + } + + if let Some(rules_dir) = rules_dir { + println!("Rendering rule doc pages..."); + let rules_dir = pwd.join(rules_dir); + if !rules_dir.exists() { + fs::create_dir_all(&rules_dir).unwrap(); + } + let rules_dir = rules_dir.canonicalize().unwrap(); + assert!( + !rules_dir.is_file(), + "Cannot write rule docs to a file. Please specify a directory." + ); + write_rule_doc_pages(&table, &rules_dir); + + // auto-fix code and natural language issues + if let Some(root) = root { + println!("Formatting rule doc pages..."); + prettier(&root, &rules_dir); + println!("Fixing textlint issues..."); + textlint(&root); + } + } + + println!("Done."); +} + +fn write_rule_doc_pages(table: &RuleTable, outdir: &Path) { + for rule in table.sections.iter().flat_map(|section| §ion.rows) { + let plugin_path = outdir.join(&rule.plugin); + fs::create_dir_all(&plugin_path).unwrap(); + let page_path = plugin_path.join(format!("{}.md", rule.name)); + println!("{}", page_path.display()); + let docs = render_rule_docs_page(rule).unwrap(); + fs::write(&page_path, docs).unwrap(); + } +} + +/// Run prettier and fix style issues in generated rule doc pages. +fn prettier(website_root: &Path, rule_docs_path: &Path) { + assert!(rule_docs_path.is_dir(), "Rule docs path must be a directory."); + assert!(rule_docs_path.is_absolute(), "Rule docs path must be an absolute path."); + let relative_path = rule_docs_path.strip_prefix(website_root).unwrap(); + let path_str = + relative_path.to_str().expect("Invalid rule docs path: could not convert to str"); + let generated_md_glob = format!("{path_str}/**/*.md"); + + process::Command::new("pnpm") + .current_dir(website_root) + .args(["run", "fmt", "--write", &generated_md_glob]) + .status() + .unwrap(); +} + +/// Run textlint and fix any issues it finds. +fn textlint(website_root: &Path) { + assert!(website_root.is_dir(), "Rule docs path must be a directory."); + process::Command::new("pnpm") + .current_dir(website_root) + .args(["run", "textlint:fix"]) + .status() + .unwrap(); +} diff --git a/tasks/website/src/linter/rules.rs b/tasks/website/src/linter/rules/table.rs similarity index 71% rename from tasks/website/src/linter/rules.rs rename to tasks/website/src/linter/rules/table.rs index b0a9d45ee2ec0..963f899328d1e 100644 --- a/tasks/website/src/linter/rules.rs +++ b/tasks/website/src/linter/rules/table.rs @@ -2,20 +2,20 @@ use oxc_linter::table::RuleTable; // `cargo run -p website linter-rules > /path/to/oxc/oxc-project.github.io/src/docs/guide/usage/linter/generated-rules.md` // -pub fn print_rules() { - let table = RuleTable::new(); - +/// `docs_prefix` is a path prefix to the base URL all rule documentation pages +/// share in common. +pub fn render_rules_table(table: &RuleTable, docs_prefix: &str) -> String { let total = table.total; let turned_on_by_default_count = table.turned_on_by_default_count; let body = table .sections - .into_iter() - .map(|section| section.render_markdown_table()) + .iter() + .map(|s| s.render_markdown_table(Some(docs_prefix))) .collect::>() .join("\n"); - println!(" + format!(" # Rules The progress of all rule implementations is tracked [here](https://github.com/oxc-project/oxc/issues/481). @@ -29,5 +29,5 @@ The progress of all rule implementations is tracked [here](https://github.com/ox -"); +") } diff --git a/tasks/website/src/main.rs b/tasks/website/src/main.rs index 48b872f02f742..d0b1c654c1e29 100644 --- a/tasks/website/src/main.rs +++ b/tasks/website/src/main.rs @@ -12,7 +12,7 @@ fn main() { "linter-schema-json" => linter::print_schema_json(), "linter-schema-markdown" => linter::print_schema_markdown(), "linter-cli" => linter::print_cli(), - "linter-rules" => linter::print_rules(), + "linter-rules" => linter::print_rules(args), _ => println!("Missing task command."), } }