pub mod convert_ast;
pub mod convert_ast_reverse;
pub mod convert_scope;
pub mod apply_renames;
pub mod diagnostics;
pub mod prefilter;
pub(crate) mod ts_namespace_export_fixup;
use apply_renames::apply_renames;
use convert_ast::convert_module_with_source_type;
use convert_ast_reverse::convert_program_to_swc_with_source;
use convert_scope::build_scope_info;
use diagnostics::{compile_result_to_diagnostics, DiagnosticMessage};
use prefilter::has_react_like_functions;
use react_compiler::entrypoint::compile_result::LoggerEvent;
use react_compiler::entrypoint::plugin_options::PluginOptions;
use std::cell::RefCell;
use swc_common::comments::Comments;
#[derive(Clone, Debug)]
pub enum BlankLinePosition {
BeforeItem { first_code_line: String },
BeforeCode { first_code_line: String },
}
thread_local! {
static LAST_COMMENTS: RefCell<Option<swc_common::comments::SingleThreadedComments>> = RefCell::new(None);
static BLANK_LINE_POSITIONS: RefCell<Vec<BlankLinePosition>> = RefCell::new(Vec::new());
}
pub struct TransformResult {
pub module: Option<swc_ecma_ast::Module>,
pub comments: Option<swc_common::comments::SingleThreadedComments>,
pub diagnostics: Vec<DiagnosticMessage>,
pub events: Vec<LoggerEvent>,
}
pub struct LintResult {
pub diagnostics: Vec<DiagnosticMessage>,
}
pub fn transform(
module: &swc_ecma_ast::Module,
source_text: &str,
options: PluginOptions,
) -> TransformResult {
if options.compilation_mode != "all" && !has_react_like_functions(module) {
return TransformResult {
module: None,
comments: None,
diagnostics: vec![],
events: vec![],
};
}
let source_type = if source_text
.lines()
.next()
.map_or(false, |line| line.contains("@script"))
{
react_compiler_ast::SourceType::Script
} else {
react_compiler_ast::SourceType::Module
};
let file = convert_module_with_source_type(module, source_text, source_type);
let scope_info = build_scope_info(module);
let result =
react_compiler::entrypoint::program::compile_program(file, scope_info, options);
let diagnostics = compile_result_to_diagnostics(&result);
let (program_json, events, renames) = match result {
react_compiler::entrypoint::compile_result::CompileResult::Success {
ast,
events,
renames,
..
} => (ast, events, renames),
react_compiler::entrypoint::compile_result::CompileResult::Error {
events, ..
} => (None, events, Vec::new()),
};
let conversion_result = program_json.and_then(|raw_json| {
let value: serde_json::Value = serde_json::from_str(raw_json.get()).ok()?;
let file: react_compiler_ast::File = serde_json::from_value(value).ok()?;
let result = convert_program_to_swc_with_source(&file, Some(source_text));
Some(result)
});
let (mut swc_module, mut comments) = match conversion_result {
Some(result) => (Some(result.module), Some(result.comments)),
None if !renames.is_empty() => (Some(module.clone()), None),
None => (None, None),
};
if let Some(ref mut swc_mod) = swc_module {
use swc_common::Spanned;
let blank_line_positions =
compute_blank_line_positions(&swc_mod.body, source_text);
let first_source_lo = module.body.first().map(|item| item.span().lo);
let mut top_level_comment_target = None;
if first_source_lo.is_some() {
let mut next_synthetic_pos = swc_common::BytePos(1);
for item in &mut swc_mod.body {
if item.span().lo.is_dummy() {
let synthetic_span =
swc_common::Span::new(next_synthetic_pos, next_synthetic_pos);
next_synthetic_pos = next_synthetic_pos + swc_common::BytePos(1);
match item {
swc_ecma_ast::ModuleItem::ModuleDecl(
swc_ecma_ast::ModuleDecl::Import(import),
) => {
import.span = synthetic_span;
top_level_comment_target = Some(import.span.hi);
}
swc_ecma_ast::ModuleItem::Stmt(
swc_ecma_ast::Stmt::Decl(swc_ecma_ast::Decl::Var(var)),
) => {
var.span = synthetic_span;
}
_ => {}
}
}
}
}
apply_renames(swc_mod, &renames);
let (source_leading_comments, source_trailing_comments) =
extract_source_comments(source_text);
if !source_leading_comments.is_empty() || !source_trailing_comments.is_empty() {
let merged = comments.unwrap_or_default();
let source_bytes = source_text.as_bytes();
for (orig_pos, comment_list) in source_leading_comments {
let is_pragma = Some(orig_pos) == first_source_lo
&& comment_list
.iter()
.all(|c| c.text.trim_start().starts_with('@'));
if is_pragma {
if let Some(pos) = top_level_comment_target {
merged.add_trailing_comments(pos, comment_list);
continue;
}
}
merged.add_leading_comments(orig_pos, comment_list);
}
for (orig_pos, comment_list) in source_trailing_comments {
let idx = orig_pos.0 as usize;
let pos = if idx >= 2 && source_bytes.get(idx - 2) == Some(&b',') {
swc_common::BytePos(orig_pos.0 - 1)
} else {
orig_pos
};
merged.add_trailing_comments(pos, comment_list);
}
comments = Some(merged);
}
BLANK_LINE_POSITIONS.with(|cell| {
*cell.borrow_mut() = blank_line_positions;
});
}
LAST_COMMENTS.with(|cell| {
*cell.borrow_mut() = comments.clone();
});
TransformResult {
module: swc_module,
comments,
diagnostics,
events,
}
}
pub fn transform_source(source_text: &str, options: PluginOptions) -> TransformResult {
let cm = swc_common::sync::Lrc::new(swc_common::SourceMap::default());
let fm = cm.new_source_file(
swc_common::sync::Lrc::new(swc_common::FileName::Anon),
source_text.to_string(),
);
let mut errors = vec![];
let module = swc_ecma_parser::parse_file_as_module(
&fm,
swc_ecma_parser::Syntax::Es(swc_ecma_parser::EsSyntax {
jsx: true,
..Default::default()
}),
swc_ecma_ast::EsVersion::latest(),
None,
&mut errors,
);
match module {
Ok(module) => transform(&module, source_text, options),
Err(_) => TransformResult {
module: None,
comments: None,
diagnostics: vec![],
events: vec![],
},
}
}
pub fn lint(
module: &swc_ecma_ast::Module,
source_text: &str,
options: PluginOptions,
) -> LintResult {
let mut opts = options;
opts.no_emit = true;
let result = transform(module, source_text, opts);
LintResult {
diagnostics: result.diagnostics,
}
}
pub fn emit(module: &swc_ecma_ast::Module) -> String {
LAST_COMMENTS.with(|cell| {
let borrowed = cell.borrow();
let positions = BLANK_LINE_POSITIONS.with(|bl| bl.borrow().clone());
emit_with_comments(module, borrowed.as_ref(), &positions)
})
}
pub fn emit_with_comments(
module: &swc_ecma_ast::Module,
comments: Option<&swc_common::comments::SingleThreadedComments>,
blank_line_positions: &[BlankLinePosition],
) -> String {
let code = emit_module_to_string(module, comments);
let code = fix_block_comment_newlines(&code);
let code = add_blank_lines_after_directives(&code);
let code = reposition_comment_blank_lines(&code);
let code = expand_fixture_entrypoint_objects(&code);
if blank_line_positions.is_empty() || module.body.is_empty() {
return code;
}
insert_blank_lines_in_output(&code, blank_line_positions)
}
fn emit_module_to_string(
module: &swc_ecma_ast::Module,
comments: Option<&swc_common::comments::SingleThreadedComments>,
) -> String {
let cm = swc_common::sync::Lrc::new(swc_common::SourceMap::default());
let mut buf = vec![];
let mut srcmap: Vec<(swc_common::BytePos, swc_common::LineCol)> = Vec::new();
{
let wr = swc_ecma_codegen::text_writer::JsWriter::new(
cm.clone(),
"\n",
&mut buf,
Some(&mut srcmap),
);
let mut emitter = swc_ecma_codegen::Emitter {
cfg: swc_ecma_codegen::Config::default().with_minify(false),
cm,
comments: comments.map(|c| c as &dyn swc_common::comments::Comments),
wr: Box::new(wr),
};
swc_ecma_codegen::Node::emit_with(module, &mut emitter).unwrap();
}
let code = String::from_utf8(buf).unwrap();
ts_namespace_export_fixup::fix_ts_namespace_export_decls(&module.body, &code, &srcmap)
}
fn insert_blank_lines_in_output(
code: &str,
positions: &[BlankLinePosition],
) -> String {
if positions.is_empty() {
return code.to_string();
}
let lines: Vec<&str> = code.lines().collect();
let mut insert_before: Vec<usize> = Vec::new();
let mut used_lines: Vec<bool> = vec![false; lines.len()];
for pos in positions {
let (first_code_line, before_comments) = match pos {
BlankLinePosition::BeforeItem { first_code_line } => {
(first_code_line.as_str(), true)
}
BlankLinePosition::BeforeCode { first_code_line } => {
(first_code_line.as_str(), false)
}
};
let mut found_idx = None;
for (i, &line) in lines.iter().enumerate() {
if line == first_code_line && (!used_lines[i] || !before_comments) {
found_idx = Some(i);
if !used_lines[i] {
used_lines[i] = true;
}
break;
}
}
let code_line_idx = match found_idx {
Some(idx) => idx,
None => continue,
};
let insert_line = if before_comments {
find_comment_block_start(&lines, code_line_idx)
} else {
code_line_idx
};
if insert_line > 0 && !lines[insert_line - 1].trim().is_empty() {
insert_before.push(insert_line);
}
}
if insert_before.is_empty() {
return code.to_string();
}
insert_before.sort_unstable();
insert_before.dedup();
let mut result = String::with_capacity(code.len() + insert_before.len() * 2);
let mut insert_idx = 0;
for (line_idx, &line) in lines.iter().enumerate() {
if insert_idx < insert_before.len() && insert_before[insert_idx] == line_idx {
result.push('\n');
insert_idx += 1;
}
result.push_str(line);
if line_idx < lines.len() - 1 || code.ends_with('\n') {
result.push('\n');
}
}
result
}
fn find_comment_block_start(lines: &[&str], code_line_idx: usize) -> usize {
let mut start = code_line_idx;
let mut i = code_line_idx;
while i > 0 {
i -= 1;
let trimmed = lines[i].trim();
if trimmed.is_empty() {
break;
}
if trimmed.starts_with("//")
|| trimmed.starts_with("/*")
|| trimmed.starts_with("* ")
|| trimmed.starts_with("*/")
|| trimmed == "*"
{
start = i;
} else {
break;
}
}
start
}
fn add_blank_lines_after_directives(code: &str) -> String {
let lines: Vec<&str> = code.lines().collect();
if lines.is_empty() {
return code.to_string();
}
let mut result: Vec<&str> = Vec::with_capacity(lines.len() + 8);
let mut i = 0;
while i < lines.len() {
result.push(lines[i]);
if is_directive_line(lines[i]) {
if i + 1 < lines.len()
&& !is_directive_line(lines[i + 1])
&& !lines[i + 1].trim().is_empty()
{
result.push("");
}
}
i += 1;
}
let mut output = result.join("\n");
if code.ends_with('\n') && !output.ends_with('\n') {
output.push('\n');
}
output
}
fn is_directive_line(line: &str) -> bool {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix('"') {
rest.ends_with("\";")
} else if let Some(rest) = trimmed.strip_prefix('\'') {
rest.ends_with("';")
} else {
false
}
}
fn fix_block_comment_newlines(code: &str) -> String {
let mut result = String::with_capacity(code.len());
let mut chars = code.char_indices().peekable();
let bytes = code.as_bytes();
let mut in_block_comment = false;
let mut block_comment_multiline = false;
while let Some((i, c)) = chars.next() {
if !in_block_comment && c == '/' && bytes.get(i + 1) == Some(&b'*') {
in_block_comment = true;
block_comment_multiline = false;
result.push(c);
continue;
}
if in_block_comment {
if c == '\n' {
block_comment_multiline = true;
}
result.push(c);
if c == '*' && bytes.get(i + 1) == Some(&b'/') {
chars.next();
result.push('/');
in_block_comment = false;
if block_comment_multiline {
let mut spaces = String::new();
while let Some(&(_, next_c)) = chars.peek() {
if next_c == ' ' || next_c == '\t' {
spaces.push(next_c);
chars.next();
} else {
break;
}
}
if let Some(&(_, next_c)) = chars.peek() {
if next_c != '\n' && next_c != '\r' {
result.push('\n');
} else {
result.push_str(&spaces);
}
} else {
result.push_str(&spaces);
}
}
}
continue;
}
result.push(c);
}
result
}
fn reposition_comment_blank_lines(code: &str) -> String {
let lines: Vec<&str> = code.lines().collect();
if lines.len() < 3 {
return code.to_string();
}
let mut result: Vec<&str> = Vec::with_capacity(lines.len());
let mut i = 0;
while i < lines.len() {
if lines[i].trim().is_empty() && i + 1 < lines.len() {
let comment_start = i + 1;
let first_comment = lines[comment_start].trim();
let is_top_level_comment = (first_comment.starts_with("//")
|| first_comment.starts_with("/*")
|| first_comment.starts_with("/**"))
&& !lines[comment_start].starts_with(' ')
&& !lines[comment_start].starts_with('\t');
if is_top_level_comment {
let mut comment_end = comment_start;
while comment_end < lines.len() {
let trimmed = lines[comment_end].trim();
if trimmed.starts_with("//")
|| trimmed.starts_with("/*")
|| trimmed.starts_with("* ")
|| trimmed.starts_with("*/")
|| trimmed == "*"
|| trimmed.starts_with("/**")
{
comment_end += 1;
} else {
break;
}
}
if comment_end < lines.len() && comment_end > comment_start {
let after_comment = lines[comment_end].trim();
let is_declaration = after_comment.starts_with("function ")
|| after_comment.starts_with("export ")
|| after_comment.starts_with("class ")
|| after_comment.starts_with("const ")
|| after_comment.starts_with("let ")
|| after_comment.starts_with("var ")
|| after_comment.starts_with("import ")
|| after_comment.starts_with("async function ")
|| after_comment.starts_with("async function*");
if is_declaration {
let prev_non_empty = i > 0 && !lines[i - 1].trim().is_empty();
if prev_non_empty {
for j in comment_start..comment_end {
result.push(lines[j]);
}
result.push("");
i = comment_end;
continue;
}
}
}
}
}
result.push(lines[i]);
i += 1;
}
let mut output = result.join("\n");
if code.ends_with('\n') && !output.ends_with('\n') {
output.push('\n');
}
output
}
fn compute_blank_line_positions(
body: &[swc_ecma_ast::ModuleItem],
source_text: &str,
) -> Vec<BlankLinePosition> {
use swc_common::Spanned;
let mut result = Vec::new();
for item in body {
let lo = item.span().lo;
if lo.is_dummy() {
continue;
}
let lo_u = (lo.0 as usize).saturating_sub(1);
if lo_u > source_text.len() || lo_u == 0 {
break;
}
let before = &source_text[..lo_u];
if has_blank_line(before) && (before.contains("//") || before.contains("/*")) {
if !is_blank_line_before_comments(before) {
let first_code_line = get_first_code_line(item);
result.push(BlankLinePosition::BeforeCode { first_code_line });
}
}
break;
}
for i in 1..body.len() {
let prev = &body[i - 1];
let curr = &body[i];
let prev_hi = prev.span().hi;
let curr_lo = curr.span().lo;
if prev_hi.is_dummy() || curr_lo.is_dummy() {
continue;
}
let prev_hi_u = (prev_hi.0 as usize).saturating_sub(1);
let curr_lo_u = (curr_lo.0 as usize).saturating_sub(1);
if prev_hi_u >= curr_lo_u || prev_hi_u > source_text.len() || curr_lo_u > source_text.len() {
continue;
}
let between = &source_text[prev_hi_u..curr_lo_u];
if !has_blank_line(between) {
continue;
}
if !between.contains("//") && !between.contains("/*") {
continue;
}
let first_code_line = get_first_code_line(curr);
let (blank_before, blank_after) = blank_line_positions_around_comments(between);
if blank_before && blank_after {
result.push(BlankLinePosition::BeforeItem { first_code_line: first_code_line.clone() });
result.push(BlankLinePosition::BeforeCode { first_code_line });
} else if blank_after {
result.push(BlankLinePosition::BeforeCode { first_code_line });
} else {
result.push(BlankLinePosition::BeforeItem { first_code_line });
}
}
result
}
fn has_blank_line(s: &str) -> bool {
let mut prev_newline = false;
for c in s.chars() {
if c == '\n' {
if prev_newline {
return true;
}
prev_newline = true;
} else if c == ' ' || c == '\t' || c == '\r' {
} else {
prev_newline = false;
}
}
false
}
fn blank_line_positions_around_comments(between: &str) -> (bool, bool) {
let mut found_comment = false;
let mut prev_newline = false;
let mut blank_before = false;
let mut blank_after = false;
for (i, c) in between.char_indices() {
if c == '\n' {
if prev_newline {
if found_comment {
blank_after = true;
} else {
blank_before = true;
}
}
prev_newline = true;
} else if c == ' ' || c == '\t' || c == '\r' {
} else {
prev_newline = false;
if c == '/' {
let next = between.as_bytes().get(i + 1);
if next == Some(&b'*') || next == Some(&b'/') {
found_comment = true;
}
}
}
}
(blank_before, blank_after)
}
fn is_blank_line_before_comments(between: &str) -> bool {
let (blank_before, blank_after) = blank_line_positions_around_comments(between);
if blank_after {
return false;
}
blank_before
}
fn get_first_code_line(item: &swc_ecma_ast::ModuleItem) -> String {
let single_module = swc_ecma_ast::Module {
span: swc_common::DUMMY_SP,
body: vec![item.clone()],
shebang: None,
};
let code = emit_module_to_string(&single_module, None);
code.lines()
.find(|l| !l.trim().is_empty())
.unwrap_or("")
.to_string()
}
fn extract_source_comments(
source_text: &str,
) -> (
Vec<(swc_common::BytePos, Vec<swc_common::comments::Comment>)>,
Vec<(swc_common::BytePos, Vec<swc_common::comments::Comment>)>,
) {
let cm = swc_common::sync::Lrc::new(swc_common::SourceMap::default());
let fm = cm.new_source_file(
swc_common::sync::Lrc::new(swc_common::FileName::Anon),
source_text.to_string(),
);
let comments = swc_common::comments::SingleThreadedComments::default();
let mut errors = vec![];
let _ = swc_ecma_parser::parse_file_as_module(
&fm,
swc_ecma_parser::Syntax::Typescript(swc_ecma_parser::TsSyntax {
tsx: true,
..Default::default()
}),
swc_ecma_ast::EsVersion::latest(),
Some(&comments),
&mut errors,
);
let mut leading_result = Vec::new();
let mut trailing_result = Vec::new();
let (leading, trailing) = comments.borrow_all();
for (pos, cmts) in leading.iter() {
if !cmts.is_empty() {
leading_result.push((*pos, cmts.clone()));
}
}
for (pos, cmts) in trailing.iter() {
if !cmts.is_empty() {
trailing_result.push((*pos, cmts.clone()));
}
}
(leading_result, trailing_result)
}
pub fn normalize_source(source: &str) -> String {
let code = add_blank_lines_after_directives(source);
let code = remove_blank_lines_after_last_import(&code);
let code = remove_blank_lines_before_fixture_entrypoint(&code);
expand_fixture_entrypoint_objects(&code)
}
fn remove_blank_lines_before_fixture_entrypoint(code: &str) -> String {
let lines: Vec<&str> = code.lines().collect();
if lines.is_empty() {
return code.to_string();
}
let mut entrypoint_idx: Option<usize> = None;
for (i, &line) in lines.iter().enumerate() {
if line.trim().starts_with("export const FIXTURE_ENTRYPOINT")
|| line.trim().starts_with("export const FIXTURE_ENTRYPOINT")
{
entrypoint_idx = Some(i);
break;
}
}
let entrypoint_idx = match entrypoint_idx {
Some(idx) if idx > 0 => idx,
_ => return code.to_string(),
};
if !lines[entrypoint_idx - 1].trim().is_empty() {
return code.to_string();
}
let mut result: Vec<&str> = Vec::with_capacity(lines.len());
for (i, &line) in lines.iter().enumerate() {
if i == entrypoint_idx - 1 {
continue;
}
result.push(line);
}
let mut output = result.join("\n");
if code.ends_with('\n') && !output.ends_with('\n') {
output.push('\n');
}
output
}
fn remove_blank_lines_after_last_import(code: &str) -> String {
let lines: Vec<&str> = code.lines().collect();
if lines.is_empty() {
return code.to_string();
}
let mut last_import_idx: Option<usize> = None;
for (i, &line) in lines.iter().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("import ") || trimmed.starts_with("import{") {
last_import_idx = Some(i);
}
}
let last_import_idx = match last_import_idx {
Some(idx) => idx,
None => return code.to_string(),
};
let blank_idx = last_import_idx + 1;
if blank_idx >= lines.len() || !lines[blank_idx].trim().is_empty() {
return code.to_string();
}
let mut result: Vec<&str> = Vec::with_capacity(lines.len());
for (i, &line) in lines.iter().enumerate() {
if i == blank_idx {
continue;
}
result.push(line);
}
let mut output = result.join("\n");
if code.ends_with('\n') && !output.ends_with('\n') {
output.push('\n');
}
output
}
fn expand_fixture_entrypoint_objects(code: &str) -> String {
let entrypoint_marker = "FIXTURE_ENTRYPOINT";
if !code.contains(entrypoint_marker) {
return code.to_string();
}
let entrypoint_pos = match code.find(entrypoint_marker) {
Some(pos) => pos,
None => return code.to_string(),
};
let (before, after) = code.split_at(entrypoint_pos);
let expanded = expand_single_line_objects_in_block(after);
format!("{}{}", before, expanded)
}
fn expand_single_line_objects_in_block(code: &str) -> String {
let mut result = String::with_capacity(code.len() + 256);
let lines: Vec<&str> = code.lines().collect();
for (idx, &line) in lines.iter().enumerate() {
if let Some(expanded) = try_expand_object_line(line) {
result.push_str(&expanded);
} else {
result.push_str(line);
}
if idx < lines.len() - 1 || code.ends_with('\n') {
result.push('\n');
}
}
result
}
fn try_expand_object_line(line: &str) -> Option<String> {
let trimmed = line.trim();
let indent = &line[..line.len() - line.trim_start().len()];
if !trimmed.contains("[{") && !trimmed.contains("{ ") {
return None;
}
let bracket_start = trimmed.find('[')?;
let bracket_end = trimmed.rfind(']')?;
if bracket_start >= bracket_end {
return None;
}
let array_content = &trimmed[bracket_start + 1..bracket_end];
let inner_trimmed = array_content.trim();
if !inner_trimmed.starts_with('{') || !inner_trimmed.contains(':') {
return None;
}
if !inner_trimmed.contains(':') {
return None;
}
let prefix = &trimmed[..bracket_start + 1];
let suffix = &trimmed[bracket_end..];
let elements = split_array_elements(inner_trimmed);
let inner_indent = format!("{} ", indent);
let prop_indent = format!("{} ", indent);
let mut result = String::new();
result.push_str(indent);
result.push_str(prefix);
result.push('\n');
for (i, elem) in elements.iter().enumerate() {
let elem = elem.trim();
if elem.starts_with('{') && elem.ends_with('}') {
let obj_content = &elem[1..elem.len() - 1].trim();
let props = split_object_properties(obj_content);
result.push_str(&inner_indent);
result.push_str("{\n");
for (_j, prop) in props.iter().enumerate() {
result.push_str(&prop_indent);
result.push_str(prop.trim());
result.push_str(",\n");
}
result.push_str(&inner_indent);
result.push('}');
} else {
result.push_str(&inner_indent);
result.push_str(elem);
}
if i < elements.len() - 1 {
result.push(',');
}
result.push('\n');
}
result.push_str(indent);
result.push_str(suffix);
Some(result)
}
fn split_array_elements(s: &str) -> Vec<String> {
let mut elements = Vec::new();
let mut current = String::new();
let mut depth = 0;
for ch in s.chars() {
match ch {
'{' | '[' | '(' => {
depth += 1;
current.push(ch);
}
'}' | ']' | ')' => {
depth -= 1;
current.push(ch);
}
',' if depth == 0 => {
let trimmed = current.trim().to_string();
if !trimmed.is_empty() {
elements.push(trimmed);
}
current.clear();
}
_ => {
current.push(ch);
}
}
}
let trimmed = current.trim().to_string();
if !trimmed.is_empty() {
elements.push(trimmed);
}
elements
}
fn split_object_properties(s: &str) -> Vec<String> {
let mut props = Vec::new();
let mut current = String::new();
let mut depth = 0;
for ch in s.chars() {
match ch {
'{' | '[' | '(' => {
depth += 1;
current.push(ch);
}
'}' | ']' | ')' => {
depth -= 1;
current.push(ch);
}
',' if depth == 0 => {
let trimmed = current.trim().to_string();
if !trimmed.is_empty() {
props.push(trimmed);
}
current.clear();
}
_ => {
current.push(ch);
}
}
}
let trimmed = current.trim().to_string();
if !trimmed.is_empty() {
props.push(trimmed);
}
props
}
pub fn lint_source(source_text: &str, options: PluginOptions) -> LintResult {
let cm = swc_common::sync::Lrc::new(swc_common::SourceMap::default());
let fm = cm.new_source_file(
swc_common::sync::Lrc::new(swc_common::FileName::Anon),
source_text.to_string(),
);
let mut errors = vec![];
let module = swc_ecma_parser::parse_file_as_module(
&fm,
swc_ecma_parser::Syntax::Es(swc_ecma_parser::EsSyntax {
jsx: true,
..Default::default()
}),
swc_ecma_ast::EsVersion::latest(),
None,
&mut errors,
);
match module {
Ok(module) => lint(&module, source_text, options),
Err(_) => LintResult {
diagnostics: vec![],
},
}
}