Skip to main content

rspack_plugin_javascript/
magic_comment.rs

1use std::{borrow::Cow, fmt, sync::LazyLock};
2
3use regex::Regex;
4use rspack_core::{ContextMode, DependencyRange};
5use rspack_error::{Diagnostic, Error, Severity};
6use rspack_regex::RspackRegex;
7use rspack_util::SpanExt;
8use rustc_hash::{FxHashMap, FxHashSet};
9use swc_experimental_allocator::Allocator;
10use swc_experimental_ecma_ast::{
11  Comment, CommentKind, EsVersion, Expr, GetSpan, Lit, Prop, PropName, PropOrSpread, Span, UnaryOp,
12};
13use swc_experimental_ecma_parser::{EsSyntax, Syntax, parse_file_as_expr};
14use swc_experimental_ecma_transforms_base::remove_paren::remove_paren;
15
16use crate::{
17  utils::object_properties::FromAstExpr,
18  visitors::{JavascriptParser, create_traceable_error},
19};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum RspackComment {
23  ChunkName,
24  Prefetch,
25  Preload,
26  Ignore,
27  FetchPriority,
28  IncludeRegexp,
29  ExcludeRegexp,
30  Mode,
31  Exports,
32}
33
34impl RspackComment {
35  fn prefixed_name(self, prefix: MagicCommentPrefix) -> String {
36    format!("{prefix}{self}")
37  }
38}
39
40impl fmt::Display for RspackComment {
41  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42    f.write_str(match self {
43      Self::ChunkName => "ChunkName",
44      Self::Prefetch => "Prefetch",
45      Self::Preload => "Preload",
46      Self::Ignore => "Ignore",
47      Self::FetchPriority => "FetchPriority",
48      Self::IncludeRegexp => "Include",
49      Self::ExcludeRegexp => "Exclude",
50      Self::Mode => "Mode",
51      Self::Exports => "Exports",
52    })
53  }
54}
55
56impl TryFrom<&str> for RspackComment {
57  type Error = ();
58
59  fn try_from(value: &str) -> Result<Self, Self::Error> {
60    match value {
61      "ChunkName" => Ok(Self::ChunkName),
62      "Prefetch" => Ok(Self::Prefetch),
63      "Preload" => Ok(Self::Preload),
64      "Ignore" => Ok(Self::Ignore),
65      "FetchPriority" => Ok(Self::FetchPriority),
66      "Include" => Ok(Self::IncludeRegexp),
67      "Exclude" => Ok(Self::ExcludeRegexp),
68      "Mode" => Ok(Self::Mode),
69      "Exports" => Ok(Self::Exports),
70      _ => Err(()),
71    }
72  }
73}
74
75#[derive(Debug, PartialEq, Eq)]
76pub enum MagicCommentValue {
77  Bool(bool),
78  String(String),
79  Number(String),
80  RegExp { source: String, flags: String },
81  Array(Vec<String>),
82  Unknown,
83}
84
85#[derive(Debug)]
86pub struct RspackCommentMap(FxHashMap<RspackComment, MagicCommentItem>);
87
88#[derive(Debug, Clone, Copy)]
89pub struct RawMagicComment<'a> {
90  pub text: &'a str,
91  pub span: DependencyRange,
92}
93
94impl RspackCommentMap {
95  fn new() -> Self {
96    Self(Default::default())
97  }
98
99  fn insert(&mut self, key: RspackComment, value: MagicCommentItem) {
100    self.0.insert(key, value);
101  }
102
103  fn push_conflict_warning(
104    source: &str,
105    ignored_comment_name: impl fmt::Display,
106    preferred_comment_name: impl fmt::Display,
107    item: &MagicCommentItem,
108    warning_diagnostics: &mut Vec<Diagnostic>,
109  ) {
110    let mut error: Error = create_traceable_error(
111      "Magic comments conflict".into(),
112      format!(
113        "`{ignored_comment_name}` is ignored because `{preferred_comment_name}` is also specified. Prefer `{preferred_comment_name}`."
114      ),
115      source.to_owned(),
116      item.span,
117    );
118    error.severity = Severity::Warning;
119    error.hide_stack = Some(true);
120    warning_diagnostics.push(error.into())
121  }
122
123  fn insert_with_conflict_warning(
124    &mut self,
125    source: &str,
126    rspack_comment: RspackComment,
127    item: MagicCommentItem,
128    warning_diagnostics: &mut Vec<Diagnostic>,
129  ) {
130    if let Some(existing) = self.0.get_mut(&rspack_comment) {
131      match (existing.prefix, item.prefix) {
132        (MagicCommentPrefix::Rspack, MagicCommentPrefix::Webpack) => {
133          Self::push_conflict_warning(
134            source,
135            rspack_comment.prefixed_name(MagicCommentPrefix::Webpack),
136            rspack_comment.prefixed_name(MagicCommentPrefix::Rspack),
137            &item,
138            warning_diagnostics,
139          );
140        }
141        (MagicCommentPrefix::Webpack, MagicCommentPrefix::Rspack) => {
142          Self::push_conflict_warning(
143            source,
144            rspack_comment.prefixed_name(MagicCommentPrefix::Webpack),
145            rspack_comment.prefixed_name(MagicCommentPrefix::Rspack),
146            existing,
147            warning_diagnostics,
148          );
149          *existing = item;
150        }
151        _ => {
152          Self::push_conflict_warning(
153            source,
154            rspack_comment.prefixed_name(item.prefix),
155            rspack_comment.prefixed_name(existing.prefix),
156            &item,
157            warning_diagnostics,
158          );
159        }
160      }
161    } else {
162      self.insert(rspack_comment, item);
163    }
164  }
165
166  pub fn get_ignore_value(&self) -> Option<&MagicCommentValue> {
167    self.0.get(&RspackComment::Ignore).map(|item| &item.value)
168  }
169
170  pub fn get_mode(&self) -> Option<&String> {
171    match self.0.get(&RspackComment::Mode).map(|item| &item.value) {
172      Some(MagicCommentValue::String(value)) => Some(value),
173      _ => None,
174    }
175  }
176
177  pub fn get_chunk_name(&self) -> Option<&String> {
178    match self
179      .0
180      .get(&RspackComment::ChunkName)
181      .map(|item| &item.value)
182    {
183      Some(MagicCommentValue::String(value)) => Some(value),
184      _ => None,
185    }
186  }
187
188  pub fn get_prefetch(&self) -> Option<Cow<'_, str>> {
189    match self.0.get(&RspackComment::Prefetch).map(|item| &item.value) {
190      Some(MagicCommentValue::Bool(true)) => Some(Cow::Borrowed("true")),
191      Some(MagicCommentValue::Number(value)) => Some(Cow::Borrowed(value.as_str())),
192      _ => None,
193    }
194  }
195
196  pub fn get_preload(&self) -> Option<Cow<'_, str>> {
197    match self.0.get(&RspackComment::Preload).map(|item| &item.value) {
198      Some(MagicCommentValue::Bool(true)) => Some(Cow::Borrowed("true")),
199      Some(MagicCommentValue::Number(value)) => Some(Cow::Borrowed(value.as_str())),
200      _ => None,
201    }
202  }
203
204  pub fn get_ignore(&self) -> Option<bool> {
205    match self.0.get(&RspackComment::Ignore).map(|item| &item.value) {
206      Some(MagicCommentValue::Bool(value)) => Some(*value),
207      _ => None,
208    }
209  }
210
211  pub fn get_fetch_priority(&self) -> Option<&String> {
212    match self
213      .0
214      .get(&RspackComment::FetchPriority)
215      .map(|item| &item.value)
216    {
217      Some(MagicCommentValue::String(value)) => Some(value),
218      _ => None,
219    }
220  }
221
222  pub fn get_include(&self) -> Option<RspackRegex> {
223    self
224      .0
225      .get(&RspackComment::IncludeRegexp)
226      .and_then(|item| match &item.value {
227        MagicCommentValue::RegExp { source, flags } => {
228          Some(RspackRegex::with_flags(source, flags).unwrap_or_else(|_| {
229            // test when capture
230            unreachable!();
231          }))
232        }
233        _ => None,
234      })
235  }
236
237  pub fn get_exclude(&self) -> Option<RspackRegex> {
238    self
239      .0
240      .get(&RspackComment::ExcludeRegexp)
241      .and_then(|item| match &item.value {
242        MagicCommentValue::RegExp { source, flags } => {
243          Some(RspackRegex::with_flags(source, flags).unwrap_or_else(|_| {
244            // test when capture
245            unreachable!();
246          }))
247        }
248        _ => None,
249      })
250  }
251
252  pub fn get_exports(&self) -> Option<Vec<String>> {
253    match self.0.get(&RspackComment::Exports).map(|item| &item.value) {
254      Some(MagicCommentValue::String(value)) => Some(vec![value.clone()]),
255      Some(MagicCommentValue::Array(value)) => Some(value.clone()),
256      _ => None,
257    }
258  }
259}
260
261fn push_magic_comment_parse_warning(
262  source: &str,
263  comment_name: impl fmt::Display,
264  comment_type: &str,
265  received: &str,
266  warning_diagnostics: &mut Vec<Diagnostic>,
267  span: DependencyRange,
268) {
269  let mut error: Error = create_traceable_error(
270    "Magic comments parse failed".into(),
271    format!("`{comment_name}` expected {comment_type}, but received: {received}."),
272    source.to_owned(),
273    span,
274  );
275  error.severity = Severity::Warning;
276  error.hide_stack = Some(true);
277  warning_diagnostics.push(error.into())
278}
279
280pub fn try_extract_magic_comment(
281  parser: &mut JavascriptParser,
282  error_span: Span,
283  span: Span,
284) -> RspackCommentMap {
285  let mut result = RspackCommentMap::new();
286  let mut warning_diagnostics = Vec::new();
287  let allocator = parser.ast.allocator;
288  if let Some(comments) = parser.ast.comments.leading.get(&span.start) {
289    analyze_comments(
290      allocator,
291      parser.source,
292      comments,
293      error_span,
294      &mut warning_diagnostics,
295      &mut result,
296    );
297  }
298  if let Some(comments) = parser.ast.comments.trailing.get(&span.end) {
299    analyze_comments(
300      allocator,
301      parser.source,
302      comments,
303      error_span,
304      &mut warning_diagnostics,
305      &mut result,
306    );
307  }
308  parser.add_warnings(warning_diagnostics);
309  result
310}
311
312/// Convert a value span from the synthetic object literal into a source span.
313///
314/// Block comment text does not include `/*` and `*/`. We parse it by wrapping
315/// it as `({<comment_text>})`, so synthetic expression spans are offset by the
316/// two-byte `({` prefix.
317fn value_span_to_error_span_with_offset(
318  comment_span: DependencyRange,
319  value_span: Span,
320  comment_offset: usize,
321) -> Option<DependencyRange> {
322  // Block comment format: /* comment_text */
323  // The comment_text doesn't include the "/*" and "*/" delimiters
324  // So we need to add 2 bytes for "/*" to get the actual position in source
325  const BLOCK_COMMENT_START_LEN: usize = 2; // Length of "/*"
326  const OBJECT_LITERAL_PREFIX_LEN: usize = 2; // Length of "({"
327
328  let value_start = value_span.real_lo() as usize;
329  let value_end = value_span.real_hi() as usize;
330  if value_start < OBJECT_LITERAL_PREFIX_LEN || value_end < OBJECT_LITERAL_PREFIX_LEN {
331    return None;
332  }
333
334  let comment_start = comment_span.start as usize;
335  let start = comment_start + BLOCK_COMMENT_START_LEN + comment_offset + value_start
336    - OBJECT_LITERAL_PREFIX_LEN;
337  let end = comment_start + BLOCK_COMMENT_START_LEN + comment_offset + value_end
338    - OBJECT_LITERAL_PREFIX_LEN;
339
340  Some(DependencyRange::new(start as u32, end as u32))
341}
342
343fn value_span_to_comment_offsets_with_offset(
344  comment_text: &str,
345  value_span: Span,
346  comment_offset: usize,
347) -> Option<(usize, usize)> {
348  const OBJECT_LITERAL_PREFIX_LEN: usize = 2; // Length of "({"
349
350  let start = comment_offset
351    + value_span
352      .real_lo()
353      .checked_sub(OBJECT_LITERAL_PREFIX_LEN as u32)? as usize;
354  let end = comment_offset
355    + value_span
356      .real_hi()
357      .checked_sub(OBJECT_LITERAL_PREFIX_LEN as u32)? as usize;
358
359  (start <= end && end <= comment_text.len()).then_some((start, end))
360}
361
362fn raw_value_with_offset<'a>(
363  comment_text: &'a str,
364  value: &Expr,
365  comment_offset: usize,
366) -> Option<&'a str> {
367  let (start, end) =
368    value_span_to_comment_offsets_with_offset(comment_text, value.span(), comment_offset)?;
369  comment_text.get(start..end).map(str::trim)
370}
371
372fn parse_magic_comment_object<'a>(
373  allocator: &'a Allocator,
374  comment_text: &str,
375) -> Option<(Expr<'a>, usize)> {
376  let magic_comment_start = find_magic_comment_start(comment_text)?;
377  let comment_text = comment_text.get(magic_comment_start..)?;
378  let source = format!("({{{comment_text}}})");
379  let source = allocator.alloc_str(&source);
380  let mut expr = parse_file_as_expr(
381    allocator,
382    source,
383    Syntax::Es(EsSyntax::default()),
384    EsVersion::EsNext,
385    None,
386  )
387  .ok()?;
388  remove_paren(&mut expr, allocator, None);
389  Some((expr, magic_comment_start))
390}
391
392static WEBPACK_COMMENT_REGEXP: LazyLock<Regex> = LazyLock::new(|| {
393  Regex::new(
394    r#"(^|[^\w])(?P<key>(?:webpack|rspack)[A-Z][A-Za-z]+|"(?:webpack|rspack)[A-Z][A-Za-z]+"|'(?:webpack|rspack)[A-Z][A-Za-z]+')\s*:"#,
395  )
396  .expect("invalid regex")
397});
398
399fn find_magic_comment_start(comment_text: &str) -> Option<usize> {
400  WEBPACK_COMMENT_REGEXP
401    .captures(comment_text)
402    .and_then(|captures| captures.name("key").map(|matched| matched.start()))
403}
404
405fn prop_name_to_str<'a>(name: &'a PropName<'a>) -> Option<Cow<'a, str>> {
406  match name {
407    PropName::Ident(ident) => Some(Cow::Borrowed(ident.sym.as_str())),
408    PropName::Str(str) => Some(str.value.to_string_lossy()),
409    _ => None,
410  }
411}
412
413fn expr_to_str<'a>(expr: &'a Expr<'a>) -> Option<Cow<'a, str>> {
414  match expr {
415    Expr::Lit(lit) => match &**lit {
416      Lit::Str(str) => Some(str.value.to_string_lossy()),
417      _ => None,
418    },
419    Expr::Tpl(tpl) if tpl.exprs.is_empty() && tpl.quasis.len() == 1 => {
420      tpl.quasis.first().map(|el| Cow::Borrowed(el.raw.as_ref()))
421    }
422    _ => None,
423  }
424}
425
426fn expr_to_bool(expr: &Expr) -> Option<bool> {
427  match expr {
428    Expr::Lit(lit) => match &**lit {
429      Lit::Bool(bool) => Some(bool.value),
430      _ => None,
431    },
432    _ => None,
433  }
434}
435
436fn is_number_expr(expr: &Expr) -> bool {
437  match expr {
438    Expr::Lit(lit) => matches!(&**lit, Lit::Num(_)),
439    Expr::Unary(unary) if matches!(unary.op, UnaryOp::Minus) => {
440      matches!(&unary.arg, Expr::Lit(lit) if matches!(&**lit, Lit::Num(_)))
441    }
442    _ => false,
443  }
444}
445
446#[cfg(test)]
447fn expr_to_order_str<'a>(comment_text: &'a str, expr: &Expr) -> Option<&'a str> {
448  expr_to_order_str_with_offset(comment_text, expr, 0)
449}
450
451fn expr_to_order_str_with_offset<'a>(
452  comment_text: &'a str,
453  expr: &Expr,
454  comment_offset: usize,
455) -> Option<&'a str> {
456  if expr_to_bool(expr).is_some() || is_number_expr(expr) {
457    raw_value_with_offset(comment_text, expr, comment_offset)
458  } else {
459    None
460  }
461}
462
463fn expr_to_regexp<'a>(expr: &'a Expr<'a>) -> Option<(&'a str, &'a str)> {
464  match expr {
465    Expr::Lit(lit) => match &**lit {
466      Lit::Regex(regex) => Some((regex.exp.as_str(), regex.flags.as_str())),
467      _ => None,
468    },
469    _ => None,
470  }
471}
472
473fn expr_to_magic_comment_value_with_offset(
474  comment_text: &str,
475  expr: &Expr,
476  comment_offset: usize,
477) -> Option<MagicCommentValue> {
478  if let Some(value) = expr_to_bool(expr) {
479    return Some(MagicCommentValue::Bool(value));
480  }
481
482  if let Some(value) = expr_to_str(expr) {
483    return Some(MagicCommentValue::String(value.into_owned()));
484  }
485
486  if is_number_expr(expr) {
487    return raw_value_with_offset(comment_text, expr, comment_offset)
488      .map(|value| MagicCommentValue::Number(value.to_string()));
489  }
490
491  if let Some((source, flags)) = expr_to_regexp(expr) {
492    return Some(MagicCommentValue::RegExp {
493      source: source.to_string(),
494      flags: flags.to_string(),
495    });
496  }
497
498  let Expr::Array(array) = expr else {
499    return None;
500  };
501  let mut items = Vec::new();
502  for elem in &array.elems {
503    let elem = elem.as_ref()?;
504    if elem.spread.is_some() {
505      return None;
506    }
507    items.push(expr_to_str(&elem.expr)?.into_owned());
508  }
509  Some(MagicCommentValue::Array(items))
510}
511
512fn expr_to_exports(expr: &Expr) -> Option<MagicCommentValue> {
513  if let Some(string) = expr_to_str(expr) {
514    let trimmed = string.trim();
515    if trimmed.len() == string.len() {
516      return Some(MagicCommentValue::String(string.into_owned()));
517    }
518    return Some(MagicCommentValue::String(trimmed.to_string()));
519  }
520
521  let Expr::Array(array) = expr else {
522    return None;
523  };
524
525  let mut exports = Vec::new();
526  for elem in &array.elems {
527    let elem = elem.as_ref()?;
528    if elem.spread.is_some() {
529      return None;
530    }
531    exports.push(expr_to_str(&elem.expr)?.into_owned());
532  }
533
534  Some(MagicCommentValue::Array(exports))
535}
536
537#[derive(Debug, Clone, Copy, PartialEq, Eq)]
538enum MagicCommentPrefix {
539  Rspack,
540  Webpack,
541}
542
543impl fmt::Display for MagicCommentPrefix {
544  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
545    f.write_str(match self {
546      Self::Rspack => "rspack",
547      Self::Webpack => "webpack",
548    })
549  }
550}
551
552#[derive(Debug)]
553struct MagicCommentItem {
554  prefix: MagicCommentPrefix,
555  value: MagicCommentValue,
556  span: DependencyRange,
557}
558
559fn parse_magic_comment_name(name: &str) -> Option<(RspackComment, MagicCommentPrefix)> {
560  let (name, prefix) = if let Some(name) = name.strip_prefix("rspack") {
561    (name, MagicCommentPrefix::Rspack)
562  } else if let Some(name) = name.strip_prefix("webpack") {
563    (name, MagicCommentPrefix::Webpack)
564  } else {
565    return None;
566  };
567
568  Some((RspackComment::try_from(name).ok()?, prefix))
569}
570
571fn analyze_comments(
572  allocator: &Allocator,
573  source: &str,
574  comments: &[Comment],
575  error_span: Span,
576  warning_diagnostics: &mut Vec<Diagnostic>,
577  result: &mut RspackCommentMap,
578) {
579  let comments = comments
580    .iter()
581    .filter(|comment| matches!(comment.kind, CommentKind::Block))
582    .map(|comment| RawMagicComment {
583      text: &comment.text,
584      span: comment.span.into(),
585    })
586    .collect::<Vec<_>>();
587  analyze_raw_comments(
588    allocator,
589    source,
590    &comments,
591    error_span.into(),
592    warning_diagnostics,
593    result,
594    true,
595    false,
596  );
597}
598
599#[allow(clippy::too_many_arguments)]
600fn analyze_raw_comments(
601  allocator: &Allocator,
602  source: &str,
603  comments: &[RawMagicComment<'_>],
604  error_span: DependencyRange,
605  warning_diagnostics: &mut Vec<Diagnostic>,
606  result: &mut RspackCommentMap,
607  warn_on_same_prefix: bool,
608  warn_on_parse_error: bool,
609) {
610  let mut parsed_comment = FxHashSet::<DependencyRange>::default();
611  for comment in comments.iter().rev() {
612    if !parsed_comment.insert(comment.span) {
613      continue;
614    }
615    let Some((expr, comment_offset)) = parse_magic_comment_object(allocator, comment.text) else {
616      if warn_on_parse_error && find_magic_comment_start(comment.text).is_some() {
617        let mut error: Error = create_traceable_error(
618          "Magic comments parse failed".into(),
619          format!(
620            "Compilation error while processing magic comment(-s): /*{}*/",
621            comment.text
622          ),
623          source.to_owned(),
624          comment.span,
625        );
626        error.severity = Severity::Warning;
627        error.hide_stack = Some(true);
628        warning_diagnostics.push(error.into());
629      }
630      continue;
631    };
632    let Expr::Object(object) = &expr else {
633      continue;
634    };
635    for prop in &object.props {
636      let PropOrSpread::Prop(prop) = prop else {
637        continue;
638      };
639      let Prop::KeyValue(prop) = &**prop else {
640        continue;
641      };
642      let Some(item_name) = prop_name_to_str(&prop.key) else {
643        continue;
644      };
645      let Some((rspack_comment, prefix)) = parse_magic_comment_name(item_name.as_ref()) else {
646        continue;
647      };
648      let value = &prop.value;
649      let item_name = rspack_comment.prefixed_name(prefix);
650      let received = raw_value_with_offset(comment.text, value, comment_offset).unwrap_or_default();
651      let item_span =
652        value_span_to_error_span_with_offset(comment.span, value.span(), comment_offset)
653          .unwrap_or(error_span);
654      let push_parse_warning = |comment_type| {
655        push_magic_comment_parse_warning(
656          source,
657          item_name,
658          comment_type,
659          received,
660          warning_diagnostics,
661          item_span,
662        );
663      };
664
665      let value = match rspack_comment {
666        RspackComment::ChunkName => {
667          if let Some(value) = expr_to_str(value) {
668            MagicCommentValue::String(value.into_owned())
669          } else {
670            push_parse_warning("a string");
671            continue;
672          }
673        }
674        RspackComment::Prefetch => {
675          if let Some(value) = expr_to_order_str_with_offset(comment.text, value, comment_offset) {
676            if value == "true" {
677              MagicCommentValue::Bool(true)
678            } else {
679              MagicCommentValue::Number(value.to_string())
680            }
681          } else {
682            push_parse_warning("true or a number");
683            continue;
684          }
685        }
686        RspackComment::Preload => {
687          if let Some(value) = expr_to_order_str_with_offset(comment.text, value, comment_offset) {
688            if value == "true" {
689              MagicCommentValue::Bool(true)
690            } else {
691              MagicCommentValue::Number(value.to_string())
692            }
693          } else {
694            push_parse_warning("true or a number");
695            continue;
696          }
697        }
698        RspackComment::Ignore => {
699          if let Some(value) = expr_to_bool(value) {
700            MagicCommentValue::Bool(value)
701          } else {
702            let value =
703              expr_to_magic_comment_value_with_offset(comment.text, value, comment_offset)
704                .unwrap_or(MagicCommentValue::Unknown);
705            push_parse_warning("a boolean");
706            value
707          }
708        }
709        RspackComment::Mode => {
710          if let Some(mode) = ContextMode::from_ast_expr(value)
711            .ok()
712            .flatten()
713            .filter(|mode| {
714              matches!(
715                mode,
716                ContextMode::Lazy | ContextMode::LazyOnce | ContextMode::Eager | ContextMode::Weak
717              )
718            })
719          {
720            MagicCommentValue::String(mode.as_str().to_string())
721          } else {
722            push_parse_warning(r#""lazy", "lazy-once", "eager" or "weak""#);
723            continue;
724          }
725        }
726        RspackComment::FetchPriority => {
727          if let Some(priority) = expr_to_str(value)
728            && matches!(priority.as_ref(), "low" | "high" | "auto")
729          {
730            MagicCommentValue::String(priority.into_owned())
731          } else {
732            push_parse_warning(r#""low", "high" or "auto""#);
733            continue;
734          }
735        }
736        RspackComment::IncludeRegexp => {
737          if let Some((regexp, flags)) = expr_to_regexp(value)
738            && RspackRegex::with_flags(regexp, flags).is_ok()
739          {
740            MagicCommentValue::RegExp {
741              source: regexp.to_string(),
742              flags: flags.to_string(),
743            }
744          } else {
745            push_parse_warning(r#"a regular expression"#);
746            continue;
747          }
748        }
749        RspackComment::ExcludeRegexp => {
750          if let Some((regexp, flags)) = expr_to_regexp(value)
751            && RspackRegex::with_flags(regexp, flags).is_ok()
752          {
753            MagicCommentValue::RegExp {
754              source: regexp.to_string(),
755              flags: flags.to_string(),
756            }
757          } else {
758            push_parse_warning(r#"a regular expression"#);
759            continue;
760          }
761        }
762        RspackComment::Exports => {
763          if let Some(exports) = expr_to_exports(value) {
764            exports
765          } else {
766            push_parse_warning(r#"a string or an array of strings"#);
767            continue;
768          }
769        }
770      };
771      let item = MagicCommentItem {
772        prefix,
773        value,
774        span: item_span,
775      };
776      if !warn_on_same_prefix
777        && let Some(existing) = result.0.get(&rspack_comment)
778        && existing.prefix == item.prefix
779      {
780        continue;
781      }
782      result.insert_with_conflict_warning(source, rspack_comment, item, warning_diagnostics);
783    }
784  }
785}
786
787pub fn try_extract_magic_comment_from_comments(
788  source: &str,
789  comments: &[RawMagicComment<'_>],
790  error_span: DependencyRange,
791) -> (RspackCommentMap, Vec<Diagnostic>) {
792  let allocator = Allocator::new();
793  let mut result = RspackCommentMap::new();
794  let mut warning_diagnostics = Vec::new();
795  analyze_raw_comments(
796    &allocator,
797    source,
798    comments,
799    error_span,
800    &mut warning_diagnostics,
801    &mut result,
802    false,
803    true,
804  );
805  (result, warning_diagnostics)
806}
807
808#[cfg(test)]
809mod tests_extract_magic_comment_object {
810  use swc_experimental_ecma_ast::DUMMY_SP;
811
812  use super::*;
813
814  fn with_value<R>(raw: &str, name: &str, f: impl FnOnce(&Expr<'_>) -> Option<R>) -> Option<R> {
815    let allocator = Allocator::new();
816    let (expr, _) = parse_magic_comment_object(&allocator, raw)?;
817    let Expr::Object(object) = &expr else {
818      return None;
819    };
820    for prop in &object.props {
821      let PropOrSpread::Prop(prop) = prop else {
822        continue;
823      };
824      let Prop::KeyValue(prop) = &**prop else {
825        continue;
826      };
827      if prop_name_to_str(&prop.key).as_deref() == Some(name) {
828        return f(&prop.value);
829      }
830    }
831    None
832  }
833
834  fn extract(raw: &str) -> (RspackCommentMap, Vec<Diagnostic>) {
835    let mut result = RspackCommentMap::new();
836    let mut warning_diagnostics = Vec::new();
837    let allocator = Allocator::new();
838    analyze_comments(
839      &allocator,
840      "",
841      &[Comment {
842        kind: CommentKind::Block,
843        span: DUMMY_SP,
844        text: swc_experimental_allocator::atom::Atom::new_in(raw, &allocator),
845      }],
846      DUMMY_SP,
847      &mut warning_diagnostics,
848      &mut result,
849    );
850    (result, warning_diagnostics)
851  }
852
853  fn try_match_string(raw: &str) -> Option<(String, String)> {
854    let name = "webpackInclude";
855    with_value(raw, name, |value| {
856      Some((name.to_string(), expr_to_str(value)?.into_owned()))
857    })
858  }
859
860  fn try_match_order(raw: &str) -> Option<(String, String)> {
861    let name = "webpackInclude";
862    with_value(raw, name, |value| {
863      Some((name.to_string(), expr_to_order_str(raw, value)?.to_string()))
864    })
865  }
866
867  fn try_match_regex(raw: &str) -> Option<(String, String, String)> {
868    let name = "webpackInclude";
869    with_value(raw, name, |value| {
870      let (regexp, flags) = expr_to_regexp(value)?;
871      Some((name.to_string(), regexp.to_string(), flags.to_string()))
872    })
873  }
874
875  fn test_extract_string() {
876    assert_eq!(
877      try_match_string("webpackInclude: \"abc\""),
878      Some(("webpackInclude".to_string(), "abc".to_string()))
879    );
880    assert_eq!(
881      try_match_string("webpackInclude: 'abc'"),
882      Some(("webpackInclude".to_string(), "abc".to_string()))
883    );
884    assert_eq!(
885      try_match_string("webpackInclude: `abc`"),
886      Some(("webpackInclude".to_string(), "abc".to_string()))
887    );
888    assert_eq!(
889      try_match_string("webpackInclude: \"abc_-|123\""),
890      Some(("webpackInclude".to_string(), "abc_-|123".to_string()))
891    );
892  }
893
894  fn test_extract_number() {
895    assert_eq!(
896      try_match_order("webpackInclude: 123"),
897      Some(("webpackInclude".to_string(), "123".to_string()))
898    );
899    assert_eq!(
900      try_match_order("webpackInclude: 123.456"),
901      Some(("webpackInclude".to_string(), "123.456".to_string()))
902    );
903    assert_eq!(
904      try_match_order("webpackInclude: -123.456"),
905      Some(("webpackInclude".to_string(), "-123.456".to_string()))
906    );
907  }
908
909  fn test_extract_boolean() {
910    assert_eq!(
911      try_match_order("webpackInclude: true"),
912      Some(("webpackInclude".to_string(), "true".to_string()))
913    );
914    assert_eq!(
915      try_match_order("webpackInclude: false"),
916      Some(("webpackInclude".to_string(), "false".to_string()))
917    );
918  }
919
920  fn test_extract_array() {
921    assert_eq!(
922      with_value(
923        "webpackExports: [\"a\", `b`, 'c']",
924        "webpackExports",
925        expr_to_exports
926      ),
927      Some(MagicCommentValue::Array(vec![
928        "a".to_string(),
929        "b".to_string(),
930        "c".to_string()
931      ]))
932    );
933  }
934
935  fn test_extract_regexp() {
936    assert_eq!(
937      try_match_regex("webpackInclude: /abc/"),
938      Some((
939        "webpackInclude".to_string(),
940        "abc".to_string(),
941        String::new()
942      ))
943    );
944    assert_eq!(
945      try_match_regex("webpackInclude: /abc/ig"),
946      Some((
947        "webpackInclude".to_string(),
948        "abc".to_string(),
949        "ig".to_string()
950      ))
951    );
952    assert_eq!(
953      try_match_regex("webpackInclude: /[^,+]/ig"),
954      Some((
955        "webpackInclude".to_string(),
956        "[^,+]".to_string(),
957        "ig".to_string()
958      ))
959    );
960    assert_eq!(
961      try_match_regex("webpackInclude: /a\\/b\\/c/ig"),
962      Some((
963        "webpackInclude".to_string(),
964        "a\\/b\\/c".to_string(),
965        "ig".to_string()
966      ))
967    );
968    assert_eq!(
969      try_match_regex("webpackInclude: /components[\\/][^\\/]+\\.vue$/"),
970      Some((
971        "webpackInclude".to_string(),
972        "components[\\/][^\\/]+\\.vue$".to_string(),
973        String::new()
974      ))
975    );
976    assert_eq!(
977      try_match_regex(r#"webpackInclude: /components[/\\][^/\\]+\.vue$/"#),
978      Some((
979        "webpackInclude".to_string(),
980        r#"components[/\\][^/\\]+\.vue$"#.to_string(),
981        String::new()
982      ))
983    );
984    assert_eq!(
985      try_match_regex("webpackInclude: /^.{2,}$/"),
986      Some((
987        "webpackInclude".to_string(),
988        "^.{2,}$".to_string(),
989        String::new()
990      ))
991    );
992    assert_eq!(
993      try_match_regex("webpackInclude: /^.{2,}$/, webpackExclude: /^.{3,}$/"),
994      Some((
995        "webpackInclude".to_string(),
996        "^.{2,}$".to_string(),
997        String::new()
998      ))
999    );
1000    // https://github.com/web-infra-dev/rspack/issues/10195
1001    assert_eq!(
1002      try_match_regex(
1003        "webpackInclude: /(?!.*node_modules)(?:\\/src\\/(?!\\.)(?=.)[^/]*?\\.stories\\.tsx)$/"
1004      ),
1005      Some((
1006        "webpackInclude".to_string(),
1007        "(?!.*node_modules)(?:\\/src\\/(?!\\.)(?=.)[^/]*?\\.stories\\.tsx)$".to_string(),
1008        String::new()
1009      ))
1010    );
1011  }
1012
1013  #[test]
1014  fn test_rspack_magic_comment_name_aliases() {
1015    assert_eq!(
1016      parse_magic_comment_name("rspackChunkName").map(|(comment, _)| comment),
1017      Some(RspackComment::ChunkName)
1018    );
1019    assert_eq!(
1020      parse_magic_comment_name("rspackPrefetch").map(|(comment, _)| comment),
1021      Some(RspackComment::Prefetch)
1022    );
1023    assert_eq!(
1024      parse_magic_comment_name("rspackPreload").map(|(comment, _)| comment),
1025      Some(RspackComment::Preload)
1026    );
1027    assert_eq!(
1028      parse_magic_comment_name("rspackIgnore").map(|(comment, _)| comment),
1029      Some(RspackComment::Ignore)
1030    );
1031    assert_eq!(
1032      parse_magic_comment_name("rspackMode").map(|(comment, _)| comment),
1033      Some(RspackComment::Mode)
1034    );
1035    assert_eq!(
1036      parse_magic_comment_name("rspackFetchPriority").map(|(comment, _)| comment),
1037      Some(RspackComment::FetchPriority)
1038    );
1039    assert_eq!(
1040      parse_magic_comment_name("rspackInclude").map(|(comment, _)| comment),
1041      Some(RspackComment::IncludeRegexp)
1042    );
1043    assert_eq!(
1044      parse_magic_comment_name("rspackExclude").map(|(comment, _)| comment),
1045      Some(RspackComment::ExcludeRegexp)
1046    );
1047    assert_eq!(
1048      parse_magic_comment_name("rspackExports").map(|(comment, _)| comment),
1049      Some(RspackComment::Exports)
1050    );
1051  }
1052
1053  #[test]
1054  fn test_extract_rspack_prefixed_magic_comments() {
1055    let (comments, warnings) = extract(
1056      r#"
1057        rspackChunkName: "chunk",
1058        rspackPrefetch: 1,
1059        rspackPreload: true,
1060        rspackIgnore: true,
1061        rspackMode: "eager",
1062        rspackFetchPriority: "high",
1063        rspackInclude: /\.js$/,
1064        rspackExclude: /\.test\.js$/,
1065        rspackExports: ["a", "b"]
1066      "#,
1067    );
1068
1069    assert!(warnings.is_empty());
1070    assert_eq!(comments.get_chunk_name(), Some(&"chunk".to_string()));
1071    assert_eq!(comments.get_prefetch().as_deref(), Some("1"));
1072    assert_eq!(comments.get_preload().as_deref(), Some("true"));
1073    assert_eq!(comments.get_ignore(), Some(true));
1074    assert_eq!(comments.get_mode(), Some(&"eager".to_string()));
1075    assert_eq!(comments.get_fetch_priority(), Some(&"high".to_string()));
1076    assert!(comments.get_include().is_some());
1077    assert!(comments.get_exclude().is_some());
1078    assert_eq!(comments.get_exports(), Some(vec!["a".into(), "b".into()]));
1079  }
1080
1081  #[test]
1082  fn test_extract_preserved_magic_comments() {
1083    let (comments, warnings) = extract("@preserve webpackIgnore: true");
1084
1085    assert!(warnings.is_empty());
1086    assert_eq!(comments.get_ignore(), Some(true));
1087
1088    let (comments, warnings) = extract("@license MIT webpackIgnore: true");
1089
1090    assert!(warnings.is_empty());
1091    assert_eq!(comments.get_ignore(), Some(true));
1092  }
1093
1094  #[test]
1095  fn test_rspack_prefixed_magic_comments_override_webpack_prefixed_comments() {
1096    let (comments, warnings) = extract(
1097      r#"
1098        webpackChunkName: "webpack-chunk",
1099        rspackChunkName: "rspack-chunk",
1100        rspackMode: "eager",
1101        webpackMode: "lazy",
1102        webpackPrefetch: 1,
1103        rspackPrefetch: true,
1104        rspackPreload: 2,
1105        webpackPreload: true,
1106        webpackIgnore: false,
1107        rspackIgnore: true,
1108        rspackFetchPriority: "high",
1109        webpackFetchPriority: "low",
1110        webpackInclude: /\.jsx$/,
1111        rspackInclude: /\.js$/,
1112        rspackExclude: /\.test\.js$/,
1113        webpackExclude: /\.spec\.js$/,
1114        webpackExports: ["webpack"],
1115        rspackExports: ["rspack"]
1116      "#,
1117    );
1118
1119    assert_eq!(comments.get_chunk_name(), Some(&"rspack-chunk".to_string()));
1120    assert_eq!(comments.get_mode(), Some(&"eager".to_string()));
1121    assert_eq!(comments.get_prefetch().as_deref(), Some("true"));
1122    assert_eq!(comments.get_preload().as_deref(), Some("2"));
1123    assert_eq!(comments.get_ignore(), Some(true));
1124    assert_eq!(comments.get_fetch_priority(), Some(&"high".to_string()));
1125    assert_eq!(comments.get_include().unwrap().source(), r#"\.js$"#);
1126    assert_eq!(comments.get_exclude().unwrap().source(), r#"\.test\.js$"#);
1127    assert_eq!(comments.get_exports(), Some(vec!["rspack".into()]));
1128    assert_eq!(warnings.len(), 9);
1129    for webpack_name in [
1130      "webpackChunkName",
1131      "webpackMode",
1132      "webpackPrefetch",
1133      "webpackPreload",
1134      "webpackIgnore",
1135      "webpackFetchPriority",
1136      "webpackInclude",
1137      "webpackExclude",
1138      "webpackExports",
1139    ] {
1140      assert!(
1141        warnings.iter().any(|warning| warning
1142          .message
1143          .contains(&format!("`{webpack_name}` is ignored"))),
1144        "missing conflict warning for {webpack_name}"
1145      );
1146    }
1147  }
1148
1149  #[test]
1150  fn test_rspack_prefixed_magic_comments_override_webpack_prefixed_comments_in_any_order() {
1151    let (comments, warnings) = extract(
1152      r#"
1153        rspackChunkName: "rspack-chunk",
1154        webpackChunkName: "webpack-chunk"
1155      "#,
1156    );
1157
1158    assert_eq!(comments.get_chunk_name(), Some(&"rspack-chunk".to_string()));
1159    assert_eq!(warnings.len(), 1);
1160    assert!(
1161      warnings[0]
1162        .message
1163        .contains("`webpackChunkName` is ignored")
1164    );
1165  }
1166
1167  #[test]
1168  fn test_repeated_magic_comments_with_same_prefix_keep_first_value() {
1169    let (comments, warnings) = extract(
1170      r#"
1171        webpackChunkName: "first-webpack-chunk",
1172        webpackChunkName: "second-webpack-chunk",
1173        rspackMode: "eager",
1174        rspackMode: "lazy"
1175      "#,
1176    );
1177
1178    assert_eq!(
1179      comments.get_chunk_name(),
1180      Some(&"first-webpack-chunk".to_string())
1181    );
1182    assert_eq!(comments.get_mode(), Some(&"eager".to_string()));
1183    assert_eq!(warnings.len(), 2);
1184    assert!(
1185      warnings[0]
1186        .message
1187        .contains("`webpackChunkName` is ignored")
1188    );
1189    assert!(warnings[1].message.contains("`rspackMode` is ignored"));
1190  }
1191
1192  #[test]
1193  fn test_extract_magic_comment_object() {
1194    test_extract_string();
1195    test_extract_number();
1196    test_extract_boolean();
1197    test_extract_array();
1198    test_extract_regexp();
1199  }
1200}