1pub mod convert_ast;
7pub mod convert_ast_reverse;
8pub mod convert_scope;
9pub mod apply_renames;
10pub mod diagnostics;
11pub mod prefilter;
12pub(crate) mod ts_namespace_export_fixup;
13
14use apply_renames::apply_renames;
15use convert_ast::convert_module_with_source_type;
16use convert_ast_reverse::convert_program_to_swc_with_source;
17use convert_scope::build_scope_info;
18use diagnostics::{compile_result_to_diagnostics, DiagnosticMessage};
19use prefilter::has_react_like_functions;
20use react_compiler::entrypoint::compile_result::LoggerEvent;
21use react_compiler::entrypoint::plugin_options::PluginOptions;
22use std::cell::RefCell;
23use swc_common::comments::Comments;
24
25#[derive(Clone, Debug)]
27pub enum BlankLinePosition {
28 BeforeItem { first_code_line: String },
32 BeforeCode { first_code_line: String },
35}
36
37thread_local! {
38 static LAST_COMMENTS: RefCell<Option<swc_common::comments::SingleThreadedComments>> = RefCell::new(None);
41
42 static BLANK_LINE_POSITIONS: RefCell<Vec<BlankLinePosition>> = RefCell::new(Vec::new());
45}
46
47pub struct TransformResult {
49 pub module: Option<swc_ecma_ast::Module>,
51 pub comments: Option<swc_common::comments::SingleThreadedComments>,
53 pub diagnostics: Vec<DiagnosticMessage>,
54 pub events: Vec<LoggerEvent>,
55}
56
57pub struct LintResult {
59 pub diagnostics: Vec<DiagnosticMessage>,
60}
61
62pub fn transform(
64 module: &swc_ecma_ast::Module,
65 source_text: &str,
66 options: PluginOptions,
67) -> TransformResult {
68 if options.compilation_mode != "all" && !has_react_like_functions(module) {
69 return TransformResult {
70 module: None,
71 comments: None,
72 diagnostics: vec![],
73 events: vec![],
74 };
75 }
76
77 let source_type = if source_text
80 .lines()
81 .next()
82 .map_or(false, |line| line.contains("@script"))
83 {
84 react_compiler_ast::SourceType::Script
85 } else {
86 react_compiler_ast::SourceType::Module
87 };
88 let file = convert_module_with_source_type(module, source_text, source_type);
89 let scope_info = build_scope_info(module);
90 let result =
91 react_compiler::entrypoint::program::compile_program(file, scope_info, options);
92
93 let diagnostics = compile_result_to_diagnostics(&result);
94 let (program_ast, events, renames) = match result {
95 react_compiler::entrypoint::compile_result::CompileResult::Success {
96 ast,
97 events,
98 renames,
99 ..
100 } => (ast, events, renames),
101 react_compiler::entrypoint::compile_result::CompileResult::Error {
102 events, ..
103 } => (None, events, Vec::new()),
104 };
105
106 let conversion_result = program_ast.map(|file| {
107 convert_program_to_swc_with_source(&file, Some(source_text))
108 });
109
110 let (mut swc_module, mut comments) = match conversion_result {
111 Some(result) => (Some(result.module), Some(result.comments)),
112 None if !renames.is_empty() => (Some(module.clone()), None),
113 None => (None, None),
114 };
115
116 if let Some(ref mut swc_mod) = swc_module {
120 use swc_common::Spanned;
121
122 let blank_line_positions =
127 compute_blank_line_positions(&swc_mod.body, source_text);
128
129 let first_source_lo = module.body.first().map(|item| item.span().lo);
133 let mut top_level_comment_target = None;
134 if first_source_lo.is_some() {
135 let mut next_synthetic_pos = swc_common::BytePos(1);
136 for item in &mut swc_mod.body {
137 if item.span().lo.is_dummy() {
138 let synthetic_span =
139 swc_common::Span::new(next_synthetic_pos, next_synthetic_pos);
140 next_synthetic_pos = next_synthetic_pos + swc_common::BytePos(1);
141 match item {
142 swc_ecma_ast::ModuleItem::ModuleDecl(
143 swc_ecma_ast::ModuleDecl::Import(import),
144 ) => {
145 import.span = synthetic_span;
146 top_level_comment_target = Some(import.span.hi);
147 }
148 swc_ecma_ast::ModuleItem::Stmt(
149 swc_ecma_ast::Stmt::Decl(swc_ecma_ast::Decl::Var(var)),
150 ) => {
151 var.span = synthetic_span;
152 }
153 _ => {}
154 }
155 }
156 }
157 }
158
159 apply_renames(swc_mod, &renames);
160
161 let (source_leading_comments, source_trailing_comments) =
162 extract_source_comments(source_text);
163 if !source_leading_comments.is_empty() || !source_trailing_comments.is_empty() {
164 let merged = comments.unwrap_or_default();
165
166 let source_bytes = source_text.as_bytes();
167 for (orig_pos, comment_list) in source_leading_comments {
168 let is_pragma = Some(orig_pos) == first_source_lo
174 && comment_list
175 .iter()
176 .all(|c| c.text.trim_start().starts_with('@'));
177 if is_pragma {
178 if let Some(pos) = top_level_comment_target {
179 merged.add_trailing_comments(pos, comment_list);
180 continue;
181 }
182 }
183 merged.add_leading_comments(orig_pos, comment_list);
184 }
185 for (orig_pos, comment_list) in source_trailing_comments {
192 let idx = orig_pos.0 as usize;
193 let pos = if idx >= 2 && source_bytes.get(idx - 2) == Some(&b',') {
194 swc_common::BytePos(orig_pos.0 - 1)
195 } else {
196 orig_pos
197 };
198 merged.add_trailing_comments(pos, comment_list);
199 }
200 comments = Some(merged);
201 }
202
203 BLANK_LINE_POSITIONS.with(|cell| {
205 *cell.borrow_mut() = blank_line_positions;
206 });
207 }
208
209 LAST_COMMENTS.with(|cell| {
211 *cell.borrow_mut() = comments.clone();
212 });
213
214 TransformResult {
215 module: swc_module,
216 comments,
217 diagnostics,
218 events,
219 }
220}
221
222pub fn transform_source(source_text: &str, options: PluginOptions) -> TransformResult {
224 let cm = swc_common::sync::Lrc::new(swc_common::SourceMap::default());
225 let fm = cm.new_source_file(
226 swc_common::sync::Lrc::new(swc_common::FileName::Anon),
227 source_text.to_string(),
228 );
229
230 let mut errors = vec![];
231 let module = swc_ecma_parser::parse_file_as_module(
232 &fm,
233 swc_ecma_parser::Syntax::Es(swc_ecma_parser::EsSyntax {
234 jsx: true,
235 ..Default::default()
236 }),
237 swc_ecma_ast::EsVersion::latest(),
238 None,
239 &mut errors,
240 );
241
242 match module {
243 Ok(module) => transform(&module, source_text, options),
244 Err(_) => TransformResult {
245 module: None,
246 comments: None,
247 diagnostics: vec![],
248 events: vec![],
249 },
250 }
251}
252
253pub fn lint(
255 module: &swc_ecma_ast::Module,
256 source_text: &str,
257 options: PluginOptions,
258) -> LintResult {
259 let mut opts = options;
260 opts.no_emit = true;
261
262 let result = transform(module, source_text, opts);
263 LintResult {
264 diagnostics: result.diagnostics,
265 }
266}
267
268pub fn emit(module: &swc_ecma_ast::Module) -> String {
272 LAST_COMMENTS.with(|cell| {
273 let borrowed = cell.borrow();
274 let positions = BLANK_LINE_POSITIONS.with(|bl| bl.borrow().clone());
275 emit_with_comments(module, borrowed.as_ref(), &positions)
276 })
277}
278
279pub fn emit_with_comments(
283 module: &swc_ecma_ast::Module,
284 comments: Option<&swc_common::comments::SingleThreadedComments>,
285 blank_line_positions: &[BlankLinePosition],
286) -> String {
287 let code = emit_module_to_string(module, comments);
289 let code = fix_block_comment_newlines(&code);
290
291 let code = add_blank_lines_after_directives(&code);
295
296 let code = reposition_comment_blank_lines(&code);
302
303 let code = expand_fixture_entrypoint_objects(&code);
308
309 if blank_line_positions.is_empty() || module.body.is_empty() {
310 return code;
311 }
312
313 insert_blank_lines_in_output(&code, blank_line_positions)
317}
318
319fn emit_module_to_string(
325 module: &swc_ecma_ast::Module,
326 comments: Option<&swc_common::comments::SingleThreadedComments>,
327) -> String {
328 let cm = swc_common::sync::Lrc::new(swc_common::SourceMap::default());
329 let mut buf = vec![];
330 let mut srcmap: Vec<(swc_common::BytePos, swc_common::LineCol)> = Vec::new();
331 {
332 let wr = swc_ecma_codegen::text_writer::JsWriter::new(
333 cm.clone(),
334 "\n",
335 &mut buf,
336 Some(&mut srcmap),
337 );
338 let mut emitter = swc_ecma_codegen::Emitter {
339 cfg: swc_ecma_codegen::Config::default().with_minify(false),
340 cm,
341 comments: comments.map(|c| c as &dyn swc_common::comments::Comments),
342 wr: Box::new(wr),
343 };
344 swc_ecma_codegen::Node::emit_with(module, &mut emitter).unwrap();
345 }
346 let code = String::from_utf8(buf).unwrap();
347 ts_namespace_export_fixup::fix_ts_namespace_export_decls(&module.body, &code, &srcmap)
348}
349
350fn insert_blank_lines_in_output(
355 code: &str,
356 positions: &[BlankLinePosition],
357) -> String {
358 if positions.is_empty() {
359 return code.to_string();
360 }
361
362 let lines: Vec<&str> = code.lines().collect();
363
364 let mut insert_before: Vec<usize> = Vec::new();
368 let mut used_lines: Vec<bool> = vec![false; lines.len()];
369
370 for pos in positions {
371 let (first_code_line, before_comments) = match pos {
372 BlankLinePosition::BeforeItem { first_code_line } => {
373 (first_code_line.as_str(), true)
374 }
375 BlankLinePosition::BeforeCode { first_code_line } => {
376 (first_code_line.as_str(), false)
377 }
378 };
379
380 let mut found_idx = None;
384 for (i, &line) in lines.iter().enumerate() {
385 if line == first_code_line && (!used_lines[i] || !before_comments) {
386 found_idx = Some(i);
387 if !used_lines[i] {
388 used_lines[i] = true;
389 }
390 break;
391 }
392 }
393
394 let code_line_idx = match found_idx {
395 Some(idx) => idx,
396 None => continue,
397 };
398
399 let insert_line = if before_comments {
400 find_comment_block_start(&lines, code_line_idx)
403 } else {
404 code_line_idx
406 };
407
408 if insert_line > 0 && !lines[insert_line - 1].trim().is_empty() {
410 insert_before.push(insert_line);
411 }
412 }
413
414 if insert_before.is_empty() {
415 return code.to_string();
416 }
417
418 insert_before.sort_unstable();
419 insert_before.dedup();
420
421 let mut result = String::with_capacity(code.len() + insert_before.len() * 2);
423 let mut insert_idx = 0;
424
425 for (line_idx, &line) in lines.iter().enumerate() {
426 if insert_idx < insert_before.len() && insert_before[insert_idx] == line_idx {
428 result.push('\n');
429 insert_idx += 1;
430 }
431
432 result.push_str(line);
433 if line_idx < lines.len() - 1 || code.ends_with('\n') {
434 result.push('\n');
435 }
436 }
437
438 result
439}
440
441fn find_comment_block_start(lines: &[&str], code_line_idx: usize) -> usize {
445 let mut start = code_line_idx;
446 let mut i = code_line_idx;
447 while i > 0 {
448 i -= 1;
449 let trimmed = lines[i].trim();
450 if trimmed.is_empty() {
451 break; }
453 if trimmed.starts_with("//")
454 || trimmed.starts_with("/*")
455 || trimmed.starts_with("* ")
456 || trimmed.starts_with("*/")
457 || trimmed == "*"
458 {
459 start = i;
460 } else {
461 break;
462 }
463 }
464 start
465}
466
467fn add_blank_lines_after_directives(code: &str) -> String {
473 let lines: Vec<&str> = code.lines().collect();
474 if lines.is_empty() {
475 return code.to_string();
476 }
477
478 let mut result: Vec<&str> = Vec::with_capacity(lines.len() + 8);
479 let mut i = 0;
480
481 while i < lines.len() {
482 result.push(lines[i]);
483
484 if is_directive_line(lines[i]) {
486 if i + 1 < lines.len()
488 && !is_directive_line(lines[i + 1])
489 && !lines[i + 1].trim().is_empty()
490 {
491 result.push("");
492 }
493 }
494
495 i += 1;
496 }
497
498 let mut output = result.join("\n");
500 if code.ends_with('\n') && !output.ends_with('\n') {
501 output.push('\n');
502 }
503 output
504}
505
506fn is_directive_line(line: &str) -> bool {
510 let trimmed = line.trim();
511 if let Some(rest) = trimmed.strip_prefix('"') {
513 rest.ends_with("\";")
514 } else if let Some(rest) = trimmed.strip_prefix('\'') {
515 rest.ends_with("';")
516 } else {
517 false
518 }
519}
520
521fn fix_block_comment_newlines(code: &str) -> String {
524 let mut result = String::with_capacity(code.len());
525 let mut chars = code.char_indices().peekable();
526 let bytes = code.as_bytes();
527 let mut in_block_comment = false;
528 let mut block_comment_multiline = false;
529
530 while let Some((i, c)) = chars.next() {
531 if !in_block_comment && c == '/' && bytes.get(i + 1) == Some(&b'*') {
533 in_block_comment = true;
534 block_comment_multiline = false;
535 result.push(c);
536 continue;
537 }
538
539 if in_block_comment {
540 if c == '\n' {
541 block_comment_multiline = true;
542 }
543 result.push(c);
544
545 if c == '*' && bytes.get(i + 1) == Some(&b'/') {
547 chars.next();
548 result.push('/');
549 in_block_comment = false;
550
551 if block_comment_multiline {
552 let mut spaces = String::new();
554 while let Some(&(_, next_c)) = chars.peek() {
555 if next_c == ' ' || next_c == '\t' {
556 spaces.push(next_c);
557 chars.next();
558 } else {
559 break;
560 }
561 }
562
563 if let Some(&(_, next_c)) = chars.peek() {
565 if next_c != '\n' && next_c != '\r' {
566 result.push('\n');
567 } else {
568 result.push_str(&spaces);
569 }
570 } else {
571 result.push_str(&spaces);
572 }
573 }
574 }
575 continue;
576 }
577
578 result.push(c);
579 }
580 result
581}
582
583fn reposition_comment_blank_lines(code: &str) -> String {
603 let lines: Vec<&str> = code.lines().collect();
604 if lines.len() < 3 {
605 return code.to_string();
606 }
607
608 let mut result: Vec<&str> = Vec::with_capacity(lines.len());
609 let mut i = 0;
610
611 while i < lines.len() {
612 if lines[i].trim().is_empty() && i + 1 < lines.len() {
614 let comment_start = i + 1;
615 let first_comment = lines[comment_start].trim();
616
617 let is_top_level_comment = (first_comment.starts_with("//")
619 || first_comment.starts_with("/*")
620 || first_comment.starts_with("/**"))
621 && !lines[comment_start].starts_with(' ')
622 && !lines[comment_start].starts_with('\t');
623
624 if is_top_level_comment {
625 let mut comment_end = comment_start;
627 while comment_end < lines.len() {
628 let trimmed = lines[comment_end].trim();
629 if trimmed.starts_with("//")
630 || trimmed.starts_with("/*")
631 || trimmed.starts_with("* ")
632 || trimmed.starts_with("*/")
633 || trimmed == "*"
634 || trimmed.starts_with("/**")
635 {
636 comment_end += 1;
637 } else {
638 break;
639 }
640 }
641
642 if comment_end < lines.len() && comment_end > comment_start {
647 let after_comment = lines[comment_end].trim();
648 let is_declaration = after_comment.starts_with("function ")
649 || after_comment.starts_with("export ")
650 || after_comment.starts_with("class ")
651 || after_comment.starts_with("const ")
652 || after_comment.starts_with("let ")
653 || after_comment.starts_with("var ")
654 || after_comment.starts_with("import ")
655 || after_comment.starts_with("async function ")
656 || after_comment.starts_with("async function*");
657
658 if is_declaration {
659 let prev_non_empty = i > 0 && !lines[i - 1].trim().is_empty();
662
663 if prev_non_empty {
664 for j in comment_start..comment_end {
667 result.push(lines[j]);
668 }
669 result.push(""); i = comment_end;
671 continue;
672 }
673 }
674 }
675 }
676 }
677
678 result.push(lines[i]);
679 i += 1;
680 }
681
682 let mut output = result.join("\n");
684 if code.ends_with('\n') && !output.ends_with('\n') {
685 output.push('\n');
686 }
687 output
688}
689
690fn compute_blank_line_positions(
701 body: &[swc_ecma_ast::ModuleItem],
702 source_text: &str,
703) -> Vec<BlankLinePosition> {
704 use swc_common::Spanned;
705
706 let mut result = Vec::new();
707
708 for item in body {
713 let lo = item.span().lo;
714 if lo.is_dummy() {
715 continue;
716 }
717 let lo_u = (lo.0 as usize).saturating_sub(1);
718 if lo_u > source_text.len() || lo_u == 0 {
719 break;
720 }
721 let before = &source_text[..lo_u];
723 if has_blank_line(before) && (before.contains("//") || before.contains("/*")) {
724 if !is_blank_line_before_comments(before) {
728 let first_code_line = get_first_code_line(item);
729 result.push(BlankLinePosition::BeforeCode { first_code_line });
730 }
731 }
732 break; }
734
735 for i in 1..body.len() {
736 let prev = &body[i - 1];
737 let curr = &body[i];
738
739 let prev_hi = prev.span().hi;
740 let curr_lo = curr.span().lo;
741
742 if prev_hi.is_dummy() || curr_lo.is_dummy() {
744 continue;
745 }
746
747 let prev_hi_u = (prev_hi.0 as usize).saturating_sub(1);
750 let curr_lo_u = (curr_lo.0 as usize).saturating_sub(1);
751
752 if prev_hi_u >= curr_lo_u || prev_hi_u > source_text.len() || curr_lo_u > source_text.len() {
753 continue;
754 }
755
756 let between = &source_text[prev_hi_u..curr_lo_u];
760 if !has_blank_line(between) {
761 continue;
762 }
763
764 if !between.contains("//") && !between.contains("/*") {
772 continue;
773 }
774
775 let first_code_line = get_first_code_line(curr);
778
779 let (blank_before, blank_after) = blank_line_positions_around_comments(between);
781
782 if blank_before && blank_after {
783 result.push(BlankLinePosition::BeforeItem { first_code_line: first_code_line.clone() });
785 result.push(BlankLinePosition::BeforeCode { first_code_line });
786 } else if blank_after {
787 result.push(BlankLinePosition::BeforeCode { first_code_line });
788 } else {
789 result.push(BlankLinePosition::BeforeItem { first_code_line });
791 }
792 }
793
794 result
795}
796
797fn has_blank_line(s: &str) -> bool {
800 let mut prev_newline = false;
801 for c in s.chars() {
802 if c == '\n' {
803 if prev_newline {
804 return true;
805 }
806 prev_newline = true;
807 } else if c == ' ' || c == '\t' || c == '\r' {
808 } else {
810 prev_newline = false;
811 }
812 }
813 false
814}
815
816fn blank_line_positions_around_comments(between: &str) -> (bool, bool) {
822 let mut found_comment = false;
823 let mut prev_newline = false;
824 let mut blank_before = false;
825 let mut blank_after = false;
826
827 for (i, c) in between.char_indices() {
828 if c == '\n' {
829 if prev_newline {
830 if found_comment {
831 blank_after = true;
832 } else {
833 blank_before = true;
834 }
835 }
836 prev_newline = true;
837 } else if c == ' ' || c == '\t' || c == '\r' {
838 } else {
840 prev_newline = false;
841 if c == '/' {
842 let next = between.as_bytes().get(i + 1);
843 if next == Some(&b'*') || next == Some(&b'/') {
844 found_comment = true;
845 }
846 }
847 }
848 }
849
850 (blank_before, blank_after)
851}
852
853fn is_blank_line_before_comments(between: &str) -> bool {
856 let (blank_before, blank_after) = blank_line_positions_around_comments(between);
857 if blank_after {
859 return false;
860 }
861 blank_before
862}
863
864fn get_first_code_line(item: &swc_ecma_ast::ModuleItem) -> String {
868 let single_module = swc_ecma_ast::Module {
869 span: swc_common::DUMMY_SP,
870 body: vec![item.clone()],
871 shebang: None,
872 };
873
874 let code = emit_module_to_string(&single_module, None);
875 code.lines()
876 .find(|l| !l.trim().is_empty())
877 .unwrap_or("")
878 .to_string()
879}
880
881fn extract_source_comments(
885 source_text: &str,
886) -> (
887 Vec<(swc_common::BytePos, Vec<swc_common::comments::Comment>)>,
888 Vec<(swc_common::BytePos, Vec<swc_common::comments::Comment>)>,
889) {
890 let cm = swc_common::sync::Lrc::new(swc_common::SourceMap::default());
891 let fm = cm.new_source_file(
892 swc_common::sync::Lrc::new(swc_common::FileName::Anon),
893 source_text.to_string(),
894 );
895
896 let comments = swc_common::comments::SingleThreadedComments::default();
897 let mut errors = vec![];
898 let _ = swc_ecma_parser::parse_file_as_module(
900 &fm,
901 swc_ecma_parser::Syntax::Typescript(swc_ecma_parser::TsSyntax {
902 tsx: true,
903 ..Default::default()
904 }),
905 swc_ecma_ast::EsVersion::latest(),
906 Some(&comments),
907 &mut errors,
908 );
909
910 let mut leading_result = Vec::new();
911 let mut trailing_result = Vec::new();
912 let (leading, trailing) = comments.borrow_all();
913 for (pos, cmts) in leading.iter() {
914 if !cmts.is_empty() {
915 leading_result.push((*pos, cmts.clone()));
916 }
917 }
918 for (pos, cmts) in trailing.iter() {
919 if !cmts.is_empty() {
920 trailing_result.push((*pos, cmts.clone()));
921 }
922 }
923
924 (leading_result, trailing_result)
925}
926
927pub fn normalize_source(source: &str) -> String {
933 let code = add_blank_lines_after_directives(source);
934 let code = remove_blank_lines_after_last_import(&code);
935 let code = remove_blank_lines_before_fixture_entrypoint(&code);
936 expand_fixture_entrypoint_objects(&code)
937}
938
939fn remove_blank_lines_before_fixture_entrypoint(code: &str) -> String {
943 let lines: Vec<&str> = code.lines().collect();
944 if lines.is_empty() {
945 return code.to_string();
946 }
947
948 let mut entrypoint_idx: Option<usize> = None;
950 for (i, &line) in lines.iter().enumerate() {
951 if line.trim().starts_with("export const FIXTURE_ENTRYPOINT")
952 || line.trim().starts_with("export const FIXTURE_ENTRYPOINT")
953 {
954 entrypoint_idx = Some(i);
955 break;
956 }
957 }
958
959 let entrypoint_idx = match entrypoint_idx {
960 Some(idx) if idx > 0 => idx,
961 _ => return code.to_string(),
962 };
963
964 if !lines[entrypoint_idx - 1].trim().is_empty() {
966 return code.to_string();
967 }
968
969 let mut result: Vec<&str> = Vec::with_capacity(lines.len());
971 for (i, &line) in lines.iter().enumerate() {
972 if i == entrypoint_idx - 1 {
973 continue;
974 }
975 result.push(line);
976 }
977
978 let mut output = result.join("\n");
979 if code.ends_with('\n') && !output.ends_with('\n') {
980 output.push('\n');
981 }
982 output
983}
984
985fn remove_blank_lines_after_last_import(code: &str) -> String {
991 let lines: Vec<&str> = code.lines().collect();
992 if lines.is_empty() {
993 return code.to_string();
994 }
995
996 let mut last_import_idx: Option<usize> = None;
998 for (i, &line) in lines.iter().enumerate() {
999 let trimmed = line.trim();
1000 if trimmed.starts_with("import ") || trimmed.starts_with("import{") {
1001 last_import_idx = Some(i);
1002 }
1003 }
1004
1005 let last_import_idx = match last_import_idx {
1006 Some(idx) => idx,
1007 None => return code.to_string(),
1008 };
1009
1010 let blank_idx = last_import_idx + 1;
1012 if blank_idx >= lines.len() || !lines[blank_idx].trim().is_empty() {
1013 return code.to_string();
1014 }
1015
1016 let mut result: Vec<&str> = Vec::with_capacity(lines.len());
1018 for (i, &line) in lines.iter().enumerate() {
1019 if i == blank_idx {
1020 continue; }
1022 result.push(line);
1023 }
1024
1025 let mut output = result.join("\n");
1026 if code.ends_with('\n') && !output.ends_with('\n') {
1027 output.push('\n');
1028 }
1029 output
1030}
1031
1032fn expand_fixture_entrypoint_objects(code: &str) -> String {
1043 let entrypoint_marker = "FIXTURE_ENTRYPOINT";
1045 if !code.contains(entrypoint_marker) {
1046 return code.to_string();
1047 }
1048
1049 let entrypoint_pos = match code.find(entrypoint_marker) {
1051 Some(pos) => pos,
1052 None => return code.to_string(),
1053 };
1054
1055 let (before, after) = code.split_at(entrypoint_pos);
1057 let expanded = expand_single_line_objects_in_block(after);
1058 format!("{}{}", before, expanded)
1059}
1060
1061fn expand_single_line_objects_in_block(code: &str) -> String {
1062 let mut result = String::with_capacity(code.len() + 256);
1063 let lines: Vec<&str> = code.lines().collect();
1064
1065 for (idx, &line) in lines.iter().enumerate() {
1066 if let Some(expanded) = try_expand_object_line(line) {
1067 result.push_str(&expanded);
1068 } else {
1069 result.push_str(line);
1070 }
1071 if idx < lines.len() - 1 || code.ends_with('\n') {
1072 result.push('\n');
1073 }
1074 }
1075
1076 result
1077}
1078
1079fn try_expand_object_line(line: &str) -> Option<String> {
1082 let trimmed = line.trim();
1083
1084 let indent = &line[..line.len() - line.trim_start().len()];
1086
1087 if !trimmed.contains("[{") && !trimmed.contains("{ ") {
1093 return None;
1094 }
1095
1096 let bracket_start = trimmed.find('[')?;
1098 let bracket_end = trimmed.rfind(']')?;
1099 if bracket_start >= bracket_end {
1100 return None;
1101 }
1102
1103 let array_content = &trimmed[bracket_start + 1..bracket_end];
1104 let inner_trimmed = array_content.trim();
1105
1106 if !inner_trimmed.starts_with('{') || !inner_trimmed.contains(':') {
1108 return None;
1109 }
1110
1111 if !inner_trimmed.contains(':') {
1113 return None;
1114 }
1115
1116 let prefix = &trimmed[..bracket_start + 1];
1118 let suffix = &trimmed[bracket_end..];
1119
1120 let elements = split_array_elements(inner_trimmed);
1122
1123 let inner_indent = format!("{} ", indent);
1124 let prop_indent = format!("{} ", indent);
1125
1126 let mut result = String::new();
1127 result.push_str(indent);
1128 result.push_str(prefix);
1129 result.push('\n');
1130
1131 for (i, elem) in elements.iter().enumerate() {
1132 let elem = elem.trim();
1133 if elem.starts_with('{') && elem.ends_with('}') {
1134 let obj_content = &elem[1..elem.len() - 1].trim();
1136 let props = split_object_properties(obj_content);
1137
1138 result.push_str(&inner_indent);
1139 result.push_str("{\n");
1140 for (_j, prop) in props.iter().enumerate() {
1141 result.push_str(&prop_indent);
1142 result.push_str(prop.trim());
1143 result.push_str(",\n");
1144 }
1145 result.push_str(&inner_indent);
1146 result.push('}');
1147 } else {
1148 result.push_str(&inner_indent);
1149 result.push_str(elem);
1150 }
1151 if i < elements.len() - 1 {
1152 result.push(',');
1153 }
1154 result.push('\n');
1155 }
1156
1157 result.push_str(indent);
1158 result.push_str(suffix);
1159
1160 Some(result)
1161}
1162
1163fn split_array_elements(s: &str) -> Vec<String> {
1165 let mut elements = Vec::new();
1166 let mut current = String::new();
1167 let mut depth = 0;
1168
1169 for ch in s.chars() {
1170 match ch {
1171 '{' | '[' | '(' => {
1172 depth += 1;
1173 current.push(ch);
1174 }
1175 '}' | ']' | ')' => {
1176 depth -= 1;
1177 current.push(ch);
1178 }
1179 ',' if depth == 0 => {
1180 let trimmed = current.trim().to_string();
1181 if !trimmed.is_empty() {
1182 elements.push(trimmed);
1183 }
1184 current.clear();
1185 }
1186 _ => {
1187 current.push(ch);
1188 }
1189 }
1190 }
1191 let trimmed = current.trim().to_string();
1192 if !trimmed.is_empty() {
1193 elements.push(trimmed);
1194 }
1195 elements
1196}
1197
1198fn split_object_properties(s: &str) -> Vec<String> {
1200 let mut props = Vec::new();
1201 let mut current = String::new();
1202 let mut depth = 0;
1203
1204 for ch in s.chars() {
1205 match ch {
1206 '{' | '[' | '(' => {
1207 depth += 1;
1208 current.push(ch);
1209 }
1210 '}' | ']' | ')' => {
1211 depth -= 1;
1212 current.push(ch);
1213 }
1214 ',' if depth == 0 => {
1215 let trimmed = current.trim().to_string();
1216 if !trimmed.is_empty() {
1217 props.push(trimmed);
1218 }
1219 current.clear();
1220 }
1221 _ => {
1222 current.push(ch);
1223 }
1224 }
1225 }
1226 let trimmed = current.trim().to_string();
1227 if !trimmed.is_empty() {
1228 props.push(trimmed);
1229 }
1230 props
1231}
1232
1233pub fn lint_source(source_text: &str, options: PluginOptions) -> LintResult {
1235 let cm = swc_common::sync::Lrc::new(swc_common::SourceMap::default());
1236 let fm = cm.new_source_file(
1237 swc_common::sync::Lrc::new(swc_common::FileName::Anon),
1238 source_text.to_string(),
1239 );
1240
1241 let mut errors = vec![];
1242 let module = swc_ecma_parser::parse_file_as_module(
1243 &fm,
1244 swc_ecma_parser::Syntax::Es(swc_ecma_parser::EsSyntax {
1245 jsx: true,
1246 ..Default::default()
1247 }),
1248 swc_ecma_ast::EsVersion::latest(),
1249 None,
1250 &mut errors,
1251 );
1252
1253 match module {
1254 Ok(module) => lint(&module, source_text, options),
1255 Err(_) => LintResult {
1256 diagnostics: vec![],
1257 },
1258 }
1259}