use biome_analyze::{AnalysisFilter, AnalyzerAction, ControlFlow, Never, RuleFilter}; use biome_diagnostics::advice::CodeSuggestionAdvice; use biome_diagnostics::{DiagnosticExt, Severity}; use biome_js_parser::{parse, JsParserOptions}; use biome_js_syntax::{JsFileSource, JsLanguage, ModuleKind}; use biome_rowan::AstNode; use biome_test_utils::{ assert_errors_are_absent, code_fix_to_string, create_analyzer_options, diagnostic_to_string, has_bogus_nodes_or_empty_slots, load_manifest, parse_test_path, register_leak_checker, scripts_from_json, write_analyzer_snapshot, CheckActionType, }; use std::{ffi::OsStr, fs::read_to_string, path::Path, slice}; tests_macros::gen_tests! {"tests/specs/**/*.{cjs,js,jsx,tsx,ts,json,jsonc}", crate::run_test, "module"} tests_macros::gen_tests! {"tests/suppression/**/*.{cjs,js,jsx,tsx,ts,json,jsonc}", crate::run_suppression_test, "module"} fn run_test(input: &'static str, _: &str, _: &str, _: &str) { register_leak_checker(); let input_file = Path::new(input); let file_name = input_file.file_name().and_then(OsStr::to_str).unwrap(); let (group, rule) = parse_test_path(input_file); if rule == "specs" || rule == "suppression" { panic!("the test file must be placed in the {rule}/// directory"); } if group == "specs" || group == "suppression" { panic!("the test file must be placed in the {group}/{rule}// directory"); } if biome_js_analyze::metadata() .find_rule(group, rule) .is_none() { panic!("could not find rule {group}/{rule}"); } let rule_filter = RuleFilter::Rule(group, rule); let filter = AnalysisFilter { enabled_rules: Some(slice::from_ref(&rule_filter)), ..AnalysisFilter::default() }; let mut snapshot = String::new(); let extension = input_file.extension().unwrap_or_default(); let input_code = read_to_string(input_file) .unwrap_or_else(|err| panic!("failed to read {:?}: {:?}", input_file, err)); let quantity_diagnostics = if let Some(scripts) = scripts_from_json(extension, &input_code) { for script in scripts { analyze_and_snap( &mut snapshot, &script, JsFileSource::js_script(), filter, file_name, input_file, CheckActionType::Lint, JsParserOptions::default(), ); } 0 } else { let Ok(source_type) = input_file.try_into() else { return; }; analyze_and_snap( &mut snapshot, &input_code, source_type, filter, file_name, input_file, CheckActionType::Lint, JsParserOptions::default(), ) }; insta::with_settings!({ prepend_module_to_snapshot => false, snapshot_path => input_file.parent().unwrap(), }, { insta::assert_snapshot!(file_name, snapshot, file_name); }); if input_code.contains("/* should not generate diagnostics */") && quantity_diagnostics > 0 { panic!("This test should not generate diagnostics"); } } #[allow(clippy::too_many_arguments)] pub(crate) fn analyze_and_snap( snapshot: &mut String, input_code: &str, source_type: JsFileSource, filter: AnalysisFilter, file_name: &str, input_file: &Path, check_action_type: CheckActionType, parser_options: JsParserOptions, ) -> usize { let parsed = parse(input_code, source_type, parser_options.clone()); let root = parsed.tree(); let mut diagnostics = Vec::new(); let mut code_fixes = Vec::new(); let options = create_analyzer_options(input_file, &mut diagnostics); let manifest = load_manifest(input_file, &mut diagnostics); let (_, errors) = biome_js_analyze::analyze(&root, filter, &options, source_type, manifest, |event| { if let Some(mut diag) = event.diagnostic() { for action in event.actions() { if check_action_type.is_suppression() { if action.is_suppression() { check_code_action( input_file, input_code, source_type, &action, parser_options.clone(), ); diag = diag.add_code_suggestion(CodeSuggestionAdvice::from(action)); } } else if !action.is_suppression() { check_code_action( input_file, input_code, source_type, &action, parser_options.clone(), ); diag = diag.add_code_suggestion(CodeSuggestionAdvice::from(action)); } } let error = diag.with_severity(Severity::Warning); diagnostics.push(diagnostic_to_string(file_name, input_code, error)); return ControlFlow::Continue(()); } for action in event.actions() { if check_action_type.is_suppression() { if action.category.matches("quickfix.suppressRule") { check_code_action( input_file, input_code, source_type, &action, parser_options.clone(), ); code_fixes.push(code_fix_to_string(input_code, action)); } } else if !action.category.matches("quickfix.suppressRule") { check_code_action( input_file, input_code, source_type, &action, parser_options.clone(), ); code_fixes.push(code_fix_to_string(input_code, action)); } } ControlFlow::::Continue(()) }); for error in errors { diagnostics.push(diagnostic_to_string(file_name, input_code, error)); } write_analyzer_snapshot( snapshot, input_code, diagnostics.as_slice(), code_fixes.as_slice(), source_type.file_extension(), ); diagnostics.len() } fn check_code_action( path: &Path, source: &str, source_type: JsFileSource, action: &AnalyzerAction, options: JsParserOptions, ) { let (_, text_edit) = action.mutation.as_text_edits().unwrap_or_default(); let output = text_edit.new_string(source); let new_tree = action.mutation.clone().commit(); // Checks that applying the text edits returned by the BatchMutation // returns the same code as printing the modified syntax tree assert_eq!(new_tree.to_string(), output); if has_bogus_nodes_or_empty_slots(&new_tree) { panic!( "modified tree has bogus nodes or empty slots:\n{new_tree:#?} \n\n {}", new_tree ) } // Checks the returned tree contains no missing children node if format!("{new_tree:?}").contains("missing (required)") { panic!("modified tree has missing children:\n{new_tree:#?}") } // Re-parse the modified code and panic if the resulting tree has syntax errors let re_parse = parse(&output, source_type, options); assert_errors_are_absent(re_parse.tree().syntax(), re_parse.diagnostics(), path); } pub(crate) fn run_suppression_test(input: &'static str, _: &str, _: &str, _: &str) { register_leak_checker(); let input_file = Path::new(input); let file_name = input_file.file_name().and_then(OsStr::to_str).unwrap(); let file_ext = match input_file.extension().and_then(OsStr::to_str).unwrap() { "cjs" => JsFileSource::js_module().with_module_kind(ModuleKind::Script), "js" | "mjs" | "jsx" => JsFileSource::jsx(), "ts" => JsFileSource::ts(), "mts" | "cts" => JsFileSource::ts_restricted(), "tsx" => JsFileSource::tsx(), _ => { panic!("Unknown file extension: {:?}", input_file.extension()); } }; let input_code = read_to_string(input_file) .unwrap_or_else(|err| panic!("failed to read {:?}: {:?}", input_file, err)); let (group, rule) = parse_test_path(input_file); let rule_filter = RuleFilter::Rule(group, rule); let filter = AnalysisFilter { enabled_rules: Some(slice::from_ref(&rule_filter)), ..AnalysisFilter::default() }; let mut snapshot = String::new(); analyze_and_snap( &mut snapshot, &input_code, file_ext, filter, file_name, input_file, CheckActionType::Suppression, JsParserOptions::default(), ); insta::with_settings!({ prepend_module_to_snapshot => false, snapshot_path => input_file.parent().unwrap(), }, { insta::assert_snapshot!(file_name, snapshot, file_name); }); }