/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
cli/tools/fmt.rs
2 052 строки
61 KB
minato32
fix(fmt): honor .editorconfig when formatting from stdin (#36267)
10 авг 2026, 17:43
Не верифицирован
10 авг 2026, 17:43
3bbcbe7
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. //! This module provides file formatting utilities using //! [`dprint-plugin-typescript`](https://github.com/dprint/dprint-plugin-typescript). //! //! At the moment it is only consumed using CLI but in //! the future it can be easily extended to provide //! the same functions as ops available in JS runtime. use std::borrow::Cow; use std::fs; use std::io::Read; use std::io::Write; use std::io::stdin; use std::io::stdout; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; use async_trait::async_trait; use deno_ast::ParsedSource; use deno_config::deno_json::NewLineKind; use deno_config::glob::FileCollector; use deno_config::glob::FilePatterns; use deno_core::anyhow::Context; use deno_core::anyhow::anyhow; use deno_core::anyhow::bail; use deno_core::error::AnyError; use deno_core::futures; use deno_core::parking_lot::Mutex; use deno_core::unsync::spawn_blocking; use log::debug; use log::info; use log::warn; use crate::args::CliOptions; use crate::args::Flags; use crate::args::FmtFlags; use crate::args::FmtOptions; use crate::args::FmtOptionsConfig; use crate::args::ProseWrap; use crate::args::UnstableFmtOptions; use crate::cache::CacheDBHash; use crate::cache::Caches; use crate::cache::IncrementalCache; use crate::colors; use crate::factory::CliFactory; use crate::sys::CliSys; use crate::tools::fmt_editorconfig::EditorConfigCache; use crate::util; use crate::util::file_watcher; use crate::util::fs::canonicalize_path; use crate::util::path::get_extension; /// Format JavaScript/TypeScript files. pub async fn format( flags: Arc<Flags>, fmt_flags: FmtFlags, ) -> Result<(), AnyError> { if fmt_flags.is_stdin() { let factory = CliFactory::from_flags(flags); let cli_options = factory.cli_options()?; let start_dir = &cli_options.start_dir; let fmt_config = start_dir .to_fmt_config(FilePatterns::new_with_base(start_dir.dir_path()))?; let mut fmt_options = FmtOptions::resolve( fmt_config, cli_options.resolve_config_unstable_fmt_options(), &fmt_flags, ); let ext = cli_options .ext_flag() .as_ref() .map(|s| s.as_str()) .unwrap_or("ts"); // Honor `.editorconfig` for stdin the same way file-based formatting does, // resolving it against a synthetic `_stdin.<ext>` path. The resolution base // is the current working directory, not the (possibly relocated via // `--config`) config start dir: stdin has no real location, so the cwd is // the only meaningful place to look for an `.editorconfig` // (https://github.com/denoland/deno/issues/36172). Build the synthetic path // once and pass it down so editorconfig matching and the dprint call can // never diverge on the name. let editorconfig_cache = EditorConfigCache::new(); let stdin_path = cli_options.initial_cwd().join(format!("_stdin.{ext}")); fmt_options.options = resolve_per_file_options( &fmt_options.options, &editorconfig_cache, &stdin_path, ); return format_stdin(&fmt_flags, fmt_options, &stdin_path); } if let Some(watch_flags) = &flags.watch { let no_clear_screen = watch_flags.no_clear_screen; file_watcher::watch_func( flags, file_watcher::PrintConfig::new("Fmt", !no_clear_screen), move |flags, watcher_communicator, changed_paths| { let fmt_flags = fmt_flags.clone(); watcher_communicator.show_path_changed(changed_paths.clone()); Ok(async move { let factory = CliFactory::from_flags(flags); let cli_options = factory.cli_options()?; let caches = factory.caches()?; let _ = watcher_communicator.watch_paths(cli_options.watch_paths()); let mut paths_with_options_batches = resolve_paths_with_options_batches(cli_options, &fmt_flags)?; for paths_with_options in &mut paths_with_options_batches { let _ = watcher_communicator.watch_paths( file_watcher::watch_paths_for_file_patterns( &paths_with_options.options.files, ), ); let files = std::mem::take(&mut paths_with_options.paths); paths_with_options.paths = if let Some(paths) = &changed_paths { if fmt_flags.check { // check all files on any changed (https://github.com/denoland/deno/issues/12446) if files.iter().any(|path| { canonicalize_path(path) .map(|path| paths.contains(&path)) .unwrap_or(false) }) { files } else { [].to_vec() } } else { files .into_iter() .filter(|path| { canonicalize_path(path) .map(|path| paths.contains(&path)) .unwrap_or(false) }) .collect::<Vec<_>>() } } else { files }; } format_files( caches, cli_options, &fmt_flags, paths_with_options_batches, ) .await?; Ok(()) }) }, ) .await?; } else { let factory = CliFactory::from_flags(flags); let cli_options = factory.cli_options()?; let caches = factory.caches()?; let paths_with_options_batches = resolve_paths_with_options_batches(cli_options, &fmt_flags)?; return format_files( caches, cli_options, &fmt_flags, paths_with_options_batches, ) .await; } Ok(()) } struct PathsWithOptions { base: PathBuf, paths: Vec<PathBuf>, options: FmtOptions, } fn resolve_paths_with_options_batches( cli_options: &CliOptions, fmt_flags: &FmtFlags, ) -> Result<Vec<PathsWithOptions>, AnyError> { maybe_show_format_confirmation(cli_options, fmt_flags)?; let members_fmt_options = cli_options.resolve_fmt_options_for_members(fmt_flags)?; let mut paths_with_options_batches = Vec::with_capacity(members_fmt_options.len()); for (_ctx, member_fmt_options) in members_fmt_options { let files = collect_fmt_files(cli_options, member_fmt_options.files.clone()); if !files.is_empty() { paths_with_options_batches.push(PathsWithOptions { base: member_fmt_options.files.base.clone(), paths: files, options: member_fmt_options, }); } } if paths_with_options_batches.is_empty() && !fmt_flags.permit_no_files { return Err(anyhow!("No target files found.")); } Ok(paths_with_options_batches) } fn maybe_show_format_confirmation( cli_options: &CliOptions, fmt_flags: &FmtFlags, ) -> Result<(), AnyError> { if fmt_flags.check || !fmt_flags.files.include.is_empty() || cli_options.workspace().deno_jsons().next().is_some() || cli_options.workspace().package_jsons().next().is_some() { return Ok(()); } let confirm_result = util::console::confirm(util::console::ConfirmOptions { default: true, message: format!( "{} It looks like you're not in a workspace. Are you sure you want to format the entire '{}' directory?", colors::yellow("Warning"), cli_options.initial_cwd().display() ), }) .unwrap_or(false); if confirm_result { Ok(()) } else { bail!( "Did not format non-workspace directory. Run again specifying the current directory (ex. `deno fmt .`)" ) } } async fn format_files( caches: &Arc<Caches>, cli_options: &Arc<CliOptions>, fmt_flags: &FmtFlags, paths_with_options_batches: Vec<PathsWithOptions>, ) -> Result<(), AnyError> { let formatter: Box<dyn Formatter> = if fmt_flags.check { let fail_fast = fmt_flags.fail_fast || cli_options.is_quiet(); Box::new(CheckFormatter::new(fail_fast)) } else { Box::new(RealFormatter::default()) }; let editorconfig_cache = Arc::new(EditorConfigCache::new()); for paths_with_options in paths_with_options_batches { log::debug!( "Formatting {} file(s) in {}", paths_with_options.paths.len(), paths_with_options.base.display() ); let fmt_options = paths_with_options.options; let paths = paths_with_options.paths; // The DB key captures only the batch-level fmt options. Per-file // `.editorconfig` options are not part of this key — instead they are // folded into the hashed *text* of each file by `incremental_cache_text` // below, so a file governed by an `.editorconfig` is re-evaluated when // that file changes even though the batch key stays the same. let incremental_cache = Arc::new(IncrementalCache::new( caches.fmt_incremental_cache_db(), CacheDBHash::from_hashable((&fmt_options.options, &fmt_options.unstable)), &paths, )); formatter .handle_files( paths, fmt_options.options, fmt_options.unstable, incremental_cache.clone(), editorconfig_cache.clone(), cli_options.ext_flag().clone(), ) .await?; incremental_cache.wait_completion().await; } formatter.finish() } fn collect_fmt_files( cli_options: &CliOptions, files: FilePatterns, ) -> Vec<PathBuf> { FileCollector::new(|e| { is_supported_ext_fmt(e.path) || (e.path.extension().is_none() && cli_options.ext_flag().is_some()) }) .ignore_git_folder() .ignore_node_modules() .use_gitignore() .set_vendor_folder(cli_options.vendor_dir_path().map(ToOwned::to_owned)) .collect_file_patterns(&CliSys::default(), &files) } /// Formats markdown (using <https://github.com/dprint/dprint-plugin-markdown>) and its code blocks /// (ts/tsx, js/jsx). fn format_markdown( file_text: &str, fmt_options: &FmtOptionsConfig, unstable_options: &UnstableFmtOptions, ) -> Result<Option<String>, AnyError> { let markdown_config = get_resolved_markdown_config(fmt_options); dprint_plugin_markdown::format_text( file_text, &markdown_config, move |tag, text, line_width| { let tag = tag.to_lowercase(); if matches!( tag.as_str(), "ts" | "tsx" | "js" | "jsx" | "cjs" | "cts" | "mjs" | "mts" | "javascript" | "typescript" | "json" | "jsonc" | "css" | "scss" | "less" | "html" | "svelte" | "vue" | "astro" | "vto" | "njk" | "yml" | "yaml" | "sql" ) { // It's important to tell dprint proper file extension, otherwise // it might parse the file twice. let extension = match tag.as_str() { "javascript" => "js", "typescript" => "ts", rest => rest, }; let fake_filename = PathBuf::from(format!("deno_fmt_stdin.{extension}")); match extension { "json" | "jsonc" => { let mut json_config = get_resolved_json_config(fmt_options); json_config.line_width = line_width; dprint_plugin_json::format_text(&fake_filename, text, &json_config) } "css" | "scss" | "less" => { format_css(&fake_filename, text, fmt_options) } "html" | "svg" | "xml" => { format_html(&fake_filename, text, fmt_options, unstable_options) } "svelte" | "vue" | "astro" | "vto" | "njk" => { if unstable_options.component { format_html(&fake_filename, text, fmt_options, unstable_options) } else { Ok(None) } } "yml" | "yaml" => format_yaml(text, fmt_options), "sql" => { if unstable_options.sql { format_sql(text, fmt_options) } else { Ok(None) } } _ => { let mut codeblock_config = get_resolved_typescript_config(fmt_options); codeblock_config.line_width = line_width; dprint_plugin_typescript::format_text( dprint_plugin_typescript::FormatTextOptions { path: &fake_filename, extension: None, text: text.to_string(), config: &codeblock_config, external_formatter: Some( &create_external_formatter_for_typescript(unstable_options), ), }, ) } } } else { Ok(None) } }, ) } /// Formats JSON and JSONC using the rules provided by .deno() /// of configuration builder of <https://github.com/dprint/dprint-plugin-json>. /// See <https://github.com/dprint/dprint-plugin-json/blob/cfa1052dbfa0b54eb3d814318034cdc514c813d7/src/configuration/builder.rs#L87> for configuration. pub fn format_json( file_path: &Path, file_text: &str, fmt_options: &FmtOptionsConfig, ) -> Result<Option<String>, AnyError> { let config = get_resolved_json_config(fmt_options); dprint_plugin_json::format_text(file_path, file_text, &config) } pub fn format_css( file_path: &Path, file_text: &str, fmt_options: &FmtOptionsConfig, ) -> Result<Option<String>, AnyError> { lax_css::format_text( file_path, file_text, &get_resolved_lax_css_config(fmt_options), ) } fn get_resolved_lax_css_config( options: &FmtOptionsConfig, ) -> lax_css::configuration::Configuration { lax_css::configuration::Configuration { line_width: options.line_width.unwrap_or(80), use_tabs: options.use_tabs.unwrap_or_default(), indent_width: options.indent_width.unwrap_or(2), new_line_kind: dprint_core::configuration::NewLineKind::LineFeed, ignore_node_comment_text: "deno-fmt-ignore".to_string(), ignore_file_comment_text: "deno-fmt-ignore-file".to_string(), single_line: false, } } fn format_yaml( file_text: &str, fmt_options: &FmtOptionsConfig, ) -> Result<Option<String>, AnyError> { let ignore_file = file_text .lines() .take_while(|line| line.starts_with('#')) .any(|line| { line .strip_prefix('#') .unwrap() .trim() .starts_with("deno-fmt-ignore-file") }); if ignore_file { return Ok(None); } let formatted_str = pretty_yaml::format_text(file_text, &get_resolved_yaml_config(fmt_options)) .map_err(AnyError::from)?; Ok(if formatted_str == file_text { None } else { Some(formatted_str) }) } pub fn format_html( file_path: &Path, file_text: &str, fmt_options: &FmtOptionsConfig, unstable_options: &UnstableFmtOptions, ) -> Result<Option<String>, AnyError> { let config = get_resolved_lax_markup_config(fmt_options); let external = move |lang: &str, text: &str, print_width: u32| { format_markup_embedded( lang, text, print_width, file_path, fmt_options, unstable_options, ) }; lax_markup::format_text_with_external( file_path, file_text, &config, &external, ) } /// Formats the contents of style and script elements in markup. The language /// comes from the element's `lang` or `type` attribute when present, or is /// `css`/`js` by element kind. Unknown languages are kept verbatim. fn format_markup_embedded( lang: &str, text: &str, print_width: u32, file_path: &Path, fmt_options: &FmtOptionsConfig, unstable_options: &UnstableFmtOptions, ) -> Result<Option<String>, AnyError> { let lang = lang.to_ascii_lowercase(); match lang.as_str() { "css" | "scss" | "less" | "text/css" => { let ext = match lang.as_str() { "scss" => "scss", "less" => "less", _ => "css", }; let mut lax_css_config = get_resolved_lax_css_config(fmt_options); lax_css_config.line_width = print_width; lax_css::format_text( &file_path.with_extension(ext), text, &lax_css_config, ) } "json" | "jsonc" | "application/json" | "application/ld+json" | "importmap" => { let mut json_config = get_resolved_json_config(fmt_options); json_config.line_width = print_width; let path = file_path.with_extension("json"); dprint_plugin_json::format_text(&path, text, &json_config) } "js" | "jsx" | "ts" | "tsx" | "mjs" | "mts" | "javascript" | "typescript" | "module" | "text/javascript" | "application/javascript" => { let ext = match lang.as_str() { "ts" | "typescript" | "mts" => "ts", "tsx" => "tsx", "jsx" => "jsx", // Astro treats inline `<script>` contents as TypeScript by default, // so a bare script tag (which `lax_markup` reports as `js`) must be // formatted as TypeScript rather than JavaScript. _ if file_path.extension().and_then(|e| e.to_str()) == Some("astro") => { "ts" } _ => "js", }; let path = file_path.with_extension(ext); let mut typescript_config = get_typescript_config_builder(fmt_options).build(); typescript_config.line_width = print_width; dprint_plugin_typescript::format_text( dprint_plugin_typescript::FormatTextOptions { path: &path, extension: Some(ext), text: text.to_string(), config: &typescript_config, external_formatter: Some(&create_external_formatter_for_typescript( unstable_options, )), }, ) } _ => Ok(None), } } /// A function for formatting embedded code blocks in JavaScript and TypeScript. fn create_external_formatter_for_typescript( unstable_options: &UnstableFmtOptions, ) -> impl Fn( &str, String, &dprint_plugin_typescript::configuration::Configuration, ) -> deno_core::anyhow::Result<Option<String>> + use<> { let unstable_sql = unstable_options.sql; move |lang, text, config| match lang { "css" => format_embedded_css(&text, config), "html" | "xml" | "svg" => format_embedded_html(lang, &text, config), "sql" => { if unstable_sql { format_embedded_sql(&text, config) } else { Ok(None) } } _ => Ok(None), } } /// Formats embedded CSS code blocks in JavaScript and TypeScript. /// /// Template literal expressions arrive as `@dpr1nt_<n>` placeholder tokens, /// which lax-css passes through untouched in property, value, selector, and /// statement positions, so the text can be formatted directly. Properties /// only CSS expressions, like: /// ```css /// margin: 10px; /// padding: 10px; /// ``` /// parse as top level declarations without any wrapping. fn format_embedded_css( text: &str, config: &dprint_plugin_typescript::configuration::Configuration, ) -> deno_core::anyhow::Result<Option<String>> { let lax_css_config = lax_css::configuration::Configuration { line_width: config.line_width, use_tabs: config.use_tabs, indent_width: config.indent_width, new_line_kind: dprint_core::configuration::NewLineKind::LineFeed, ignore_node_comment_text: "deno-fmt-ignore".to_string(), ignore_file_comment_text: "deno-fmt-ignore-file".to_string(), single_line: false, }; // lax-css prints custom property values verbatim, keeping the leading // whitespace on their wrapped continuation lines. the typescript formatter // reindents the embedded result when it puts it back in the template, so // without dedenting first that leading whitespace grows by the template's // indentation on every pass and the format never reaches a fixed point. let dedented = dedent_embedded(text); let Some(formatted) = lax_css::format_text( Path::new("embedded.css"), &dedented, &lax_css_config, )? else { // lax-css declined to reformat (an ignore directive, or content it // considers already canonical). leave the block exactly as the user wrote // it: the typescript formatter then emits the template verbatim, which is a // fixed point and preserves any intentional formatting inside it. return Ok(None); }; let formatted = formatted.trim_matches('\n'); Ok(if formatted == text { None } else { Some(formatted.to_string()) }) } /// Formats the embedded HTML code blocks in JavaScript and TypeScript. fn format_embedded_html( _lang: &str, text: &str, config: &dprint_plugin_typescript::configuration::Configuration, ) -> deno_core::anyhow::Result<Option<String>> { let markup_config = lax_markup::configuration::Configuration { line_width: config.line_width, use_tabs: config.use_tabs, indent_width: config.indent_width, new_line_kind: dprint_core::configuration::NewLineKind::LineFeed, ignore_node_comment_text: "deno-fmt-ignore".to_string(), ignore_file_comment_text: "deno-fmt-ignore-file".to_string(), }; // code inside markup that is itself inside a tagged template is kept as // written let external = |_: &str, _: &str, _: u32| Ok(None); // the typescript formatter reindents the template contents when it embeds // the result, so the text must be dedented first to make the round trip a // fixed point let dedented = dedent_embedded(text); let formatted = match lax_markup::format_text_with_external( Path::new("embedded.html"), &dedented, &markup_config, &external, )? { Some(formatted) => formatted, None => dedented, }; // the typescript formatter frames the contents on their own lines between the // opening and closing backticks, so any leading or trailing blank line would // be re-added on every pass and the format would never reach a fixed point. // unlike lax-css, lax-markup keeps leading blank lines verbatim, so trim both // ends here. let formatted = formatted.trim_matches('\n'); Ok(if formatted == text { None } else { Some(formatted.to_string()) }) } /// Strips the longest common leading whitespace prefix from every non empty /// line. fn dedent_embedded(text: &str) -> String { let mut common: Option<&str> = None; for line in text.split('\n') { if line.trim().is_empty() { continue; } let leading = &line[..line.len() - line.trim_start().len()]; common = Some(match common { None => leading, Some(prev) => { let len = prev .as_bytes() .iter() .zip(leading.as_bytes()) .take_while(|(a, b)| a == b) .count(); &prev[..len] } }); } let common = common.unwrap_or(""); if common.is_empty() { return text.to_string(); } text .split('\n') .map(|line| line.strip_prefix(common).unwrap_or(line)) .collect::<Vec<_>>() .join("\n") } /// Formats the embedded SQL code blocks in JavaScript and TypeScript. fn format_embedded_sql( text: &str, config: &dprint_plugin_typescript::configuration::Configuration, ) -> deno_core::anyhow::Result<Option<String>> { let sql_config = get_resolved_lax_sql_config( config.line_width, config.use_tabs, config.indent_width, ); let Some(formatted) = lax_sql::format_text(Path::new("embedded.sql"), text, &sql_config)? else { return Ok(None); }; let formatted = formatted.trim_end_matches('\n'); Ok(if formatted == text { None } else { Some(formatted.to_string()) }) } fn get_resolved_lax_sql_config( line_width: u32, use_tabs: bool, indent_width: u8, ) -> lax_sql::configuration::Configuration { lax_sql::configuration::Configuration { line_width, use_tabs, indent_width, new_line_kind: dprint_core::configuration::NewLineKind::LineFeed, // matches the previous sqlformat behavior of uppercasing keywords keyword_case: lax_sql::configuration::KeywordCase::Upper, clause_style: lax_sql::configuration::ClauseStyle::Fill, ignore_node_comment_text: "deno-fmt-ignore".to_string(), ignore_file_comment_text: "deno-fmt-ignore-file".to_string(), } } pub fn format_sql( file_text: &str, fmt_options: &FmtOptionsConfig, ) -> Result<Option<String>, AnyError> { lax_sql::format_text( Path::new("file.sql"), file_text, &get_resolved_lax_sql_config( fmt_options.line_width.unwrap_or(80), fmt_options.use_tabs.unwrap_or_default(), fmt_options.indent_width.unwrap_or(2), ), ) } /// Formats a single TS, TSX, JS, JSX, JSONC, JSON, MD, IPYNB or SQL file. pub fn format_file( file_path: &Path, file: &FileContents, fmt_options: &FmtOptionsConfig, unstable_options: &UnstableFmtOptions, ext: Option<String>, ) -> Result<Option<String>, AnyError> { let ext = ext .or_else(|| get_extension(file_path)) .unwrap_or("ts".to_string()); let maybe_result = match ext.as_str() { "md" | "mkd" | "mkdn" | "mdwn" | "mdown" | "markdown" => { format_markdown(&file.text, fmt_options, unstable_options)? } "json" | "jsonc" => format_json(file_path, &file.text, fmt_options)?, "css" | "scss" | "less" => format_css(file_path, &file.text, fmt_options)?, "html" | "xml" | "svg" => { format_html(file_path, &file.text, fmt_options, unstable_options)? } "svelte" | "vue" | "astro" | "vto" | "njk" | "mustache" => { if unstable_options.component { format_html(file_path, &file.text, fmt_options, unstable_options)? } else { None } } "yml" | "yaml" => format_yaml(&file.text, fmt_options)?, "ipynb" => dprint_plugin_jupyter::format_text( &file.text, |file_path: &Path, file_text: String| { let file = FileContents { had_bom: false, text: file_text.into(), }; format_file(file_path, &file, fmt_options, unstable_options, None) }, )?, "sql" => { if unstable_options.sql { format_sql(&file.text, fmt_options)? } else { None } } _ => { let config = get_resolved_typescript_config(fmt_options); dprint_plugin_typescript::format_text( dprint_plugin_typescript::FormatTextOptions { path: file_path, extension: Some(&ext), text: file.text.to_string(), config: &config, external_formatter: Some(&create_external_formatter_for_typescript( unstable_options, )), }, )? } }; Ok(match maybe_result { Some(result) => Some(result), None if file.had_bom => { // return back the text without the BOM Some(file.text.to_string()) } None => None, }) } pub fn format_parsed_source( parsed_source: &ParsedSource, fmt_options: &FmtOptionsConfig, unstable_options: &UnstableFmtOptions, ) -> Result<Option<String>, AnyError> { dprint_plugin_typescript::format_parsed_source( parsed_source, &get_resolved_typescript_config(fmt_options), Some(&create_external_formatter_for_typescript(unstable_options)), ) } #[async_trait] trait Formatter { async fn handle_files( &self, paths: Vec<PathBuf>, fmt_options: FmtOptionsConfig, unstable_options: UnstableFmtOptions, incremental_cache: Arc<IncrementalCache>, editorconfig_cache: Arc<EditorConfigCache>, ext: Option<String>, ) -> Result<(), AnyError>; fn finish(&self) -> Result<(), AnyError>; } /// Returns a per-file [`FmtOptionsConfig`], merging values from /// `.editorconfig` (lowest priority) under the resolved `base` config /// (which already incorporates `deno.json` plus CLI flags). fn resolve_per_file_options( base: &FmtOptionsConfig, editorconfig_cache: &EditorConfigCache, file_path: &Path, ) -> FmtOptionsConfig { // `.editorconfig` reading is on by default; `useEditorConfig: false` in // deno.json or `--no-editorconfig` on the CLI opts out (the latter takes // precedence, resolved upstream in `resolve_fmt_options`). if base.use_editor_config == Some(false) { return base.clone(); } let props = editorconfig_cache.resolve(file_path); if props.is_empty() { return base.clone(); } let mut cfg = base.clone(); props.apply_to(&mut cfg); cfg } /// Returns the value hashed by the incremental cache for a file. When /// `.editorconfig` contributes options that differ from the batch-level /// `base` config, those options are folded into the hashed value so that /// editing `.editorconfig` invalidates the cached "already formatted" /// result even when the file body itself is unchanged. When nothing was /// contributed the file text is hashed as-is, preserving existing cache /// entries and avoiding an allocation. fn incremental_cache_text<'a>( per_file_options: &FmtOptionsConfig, base: &FmtOptionsConfig, text: &'a str, ) -> Cow<'a, str> { if per_file_options == base { Cow::Borrowed(text) } else { Cow::Owned(format!("{per_file_options:?}\n{text}")) } } struct CheckFormatter { not_formatted_files_count: Arc<AtomicUsize>, checked_files_count: Arc<AtomicUsize>, fail_fast_found_error: Option<Arc<std::sync::atomic::AtomicBool>>, } impl CheckFormatter { fn new(fail_fast: bool) -> Self { Self { not_formatted_files_count: Arc::new(AtomicUsize::new(0)), checked_files_count: Arc::new(AtomicUsize::new(0)), fail_fast_found_error: if fail_fast { Some(Arc::new(std::sync::atomic::AtomicBool::new(false))) } else { None }, } } } #[async_trait] impl Formatter for CheckFormatter { async fn handle_files( &self, paths: Vec<PathBuf>, fmt_options: FmtOptionsConfig, unstable_options: UnstableFmtOptions, incremental_cache: Arc<IncrementalCache>, editorconfig_cache: Arc<EditorConfigCache>, ext: Option<String>, ) -> Result<(), AnyError> { // prevent threads outputting at the same time let output_lock = Arc::new(Mutex::new(0)); run_parallelized(paths, { let not_formatted_files_count = self.not_formatted_files_count.clone(); let checked_files_count = self.checked_files_count.clone(); let fail_fast_found_error = self.fail_fast_found_error.clone(); move |file_path| { // Early exit if fail-fast is enabled and we've already found an error if let Some(fast) = &fail_fast_found_error && fast.load(Ordering::Relaxed) { return Ok(()); } checked_files_count.fetch_add(1, Ordering::Relaxed); let file = read_file_contents(&file_path)?; let per_file_options = resolve_per_file_options( &fmt_options, &editorconfig_cache, &file_path, ); let cache_text = incremental_cache_text(&per_file_options, &fmt_options, &file.text); // skip checking the file if we know it's formatted if !file.had_bom && incremental_cache.is_file_same(&file_path, &cache_text) { return Ok(()); } match format_file( &file_path, &file, &per_file_options, &unstable_options, ext.clone(), ) { Ok(Some(formatted_text)) => { not_formatted_files_count.fetch_add(1, Ordering::Relaxed); if let Some(fast) = &fail_fast_found_error { fast.store(true, Ordering::Relaxed); } let _g = output_lock.lock(); let diff = deno_resolver::display::diff(&file.text, &formatted_text); info!(""); info!("{} {}:", colors::bold("from"), file_path.display()); if file.had_bom { info!(" {}", colors::gray("File has strippable UTF-8 BOM.")); } info!("{}", diff); } Ok(None) => { // When checking formatting, only update the incremental cache when // the file is the same since we don't bother checking for stable // formatting here. Additionally, ensure this is done during check // so that CIs that cache the DENO_DIR will get the benefit of // incremental formatting incremental_cache.update_file(&file_path, &cache_text); } Err(e) => { not_formatted_files_count.fetch_add(1, Ordering::Relaxed); if let Some(fast) = &fail_fast_found_error { fast.store(true, Ordering::Relaxed); } let _g = output_lock.lock(); warn!("Error checking: {}", file_path.to_string_lossy()); warn!( "{}", format!("{e}") .split('\n') .map(|l| { if l.trim().is_empty() { String::new() } else { format!(" {l}") } }) .collect::<Vec<_>>() .join("\n") ); } } Ok(()) } }) .await?; Ok(()) } fn finish(&self) -> Result<(), AnyError> { let not_formatted_files_count = self.not_formatted_files_count.load(Ordering::Relaxed); let checked_files_count = self.checked_files_count.load(Ordering::Relaxed); let checked_files_str = format!("{} {}", checked_files_count, files_str(checked_files_count)); if not_formatted_files_count == 0 { info!("Checked {}", checked_files_str); Ok(()) } else { let not_formatted_files_str = files_str(not_formatted_files_count); Err(anyhow!( "Found {not_formatted_files_count} not formatted {not_formatted_files_str} in {checked_files_str}", )) } } } #[derive(Default)] struct RealFormatter { formatted_files_count: Arc<AtomicUsize>, failed_files_count: Arc<AtomicUsize>, checked_files_count: Arc<AtomicUsize>, } #[async_trait] impl Formatter for RealFormatter { async fn handle_files( &self, paths: Vec<PathBuf>, fmt_options: FmtOptionsConfig, unstable_options: UnstableFmtOptions, incremental_cache: Arc<IncrementalCache>, editorconfig_cache: Arc<EditorConfigCache>, ext: Option<String>, ) -> Result<(), AnyError> { let output_lock = Arc::new(Mutex::new(0)); // prevent threads outputting at the same time run_parallelized(paths, { let formatted_files_count = self.formatted_files_count.clone(); let failed_files_count = self.failed_files_count.clone(); let checked_files_count = self.checked_files_count.clone(); move |file_path| { checked_files_count.fetch_add(1, Ordering::Relaxed); let file = read_file_contents(&file_path)?; let per_file_options = resolve_per_file_options( &fmt_options, &editorconfig_cache, &file_path, ); let cache_text = incremental_cache_text(&per_file_options, &fmt_options, &file.text); // skip formatting the file if we know it's formatted if !file.had_bom && incremental_cache.is_file_same(&file_path, &cache_text) { return Ok(()); } match format_ensure_stable(&file_path, &file, |file_path, file| { format_file( file_path, file, &per_file_options, &unstable_options, ext.clone(), ) }) { Ok(Some(formatted_text)) => { incremental_cache.update_file( &file_path, &incremental_cache_text( &per_file_options, &fmt_options, &formatted_text, ), ); write_file_contents(&file_path, &formatted_text)?; formatted_files_count.fetch_add(1, Ordering::Relaxed); let _g = output_lock.lock(); info!("{}", file_path.to_string_lossy()); } Ok(None) => { incremental_cache.update_file(&file_path, &cache_text); } Err(e) => { failed_files_count.fetch_add(1, Ordering::Relaxed); let _g = output_lock.lock(); log::error!("Error formatting: {}", file_path.to_string_lossy()); log::error!(" {e}"); } } Ok(()) } }) .await?; Ok(()) } fn finish(&self) -> Result<(), AnyError> { let formatted_files_count = self.formatted_files_count.load(Ordering::Relaxed); debug!( "Formatted {} {}", formatted_files_count, files_str(formatted_files_count), ); let failed_files_count = self.failed_files_count.load(Ordering::Relaxed); let checked_files_count = self.checked_files_count.load(Ordering::Relaxed); if failed_files_count == 0 { info!( "Checked {} {}", checked_files_count, files_str(checked_files_count) ); Ok(()) } else { let checked_files_str = format!( "{} checked {}", checked_files_count, files_str(checked_files_count) ); Err(anyhow!( "Failed to format {failed_files_count} of {checked_files_str}", )) } } } /// When storing any formatted text in the incremental cache, we want /// to ensure that anything stored when formatted will have itself as /// the output as well. This is to prevent "double format" issues where /// a user formats their code locally and it fails on the CI afterwards. fn format_ensure_stable( file_path: &Path, file: &FileContents, fmt_func: impl Fn(&Path, &FileContents) -> Result<Option<String>, AnyError>, ) -> Result<Option<String>, AnyError> { let formatted_text = fmt_func(file_path, file)?; match formatted_text { Some(mut current_text) => { let mut count = 0; loop { match fmt_func( file_path, &FileContents { had_bom: false, text: (¤t_text).into(), }, ) { Ok(Some(next_pass_text)) => { // just in case if next_pass_text == current_text { return Ok(Some(next_pass_text)); } current_text = next_pass_text; } Ok(None) => { return Ok(Some(current_text)); } Err(err) => { bail!( concat!( "Formatting succeeded initially, but failed when ensuring a ", "stable format. This indicates a bug in the formatter where ", "the text it produces is not syntactically correct. As a temporary ", "workaround you can ignore this file.\n\n{:#}" ), err, ) } } count += 1; if count == 5 { bail!( concat!( "Formatting not stable. Bailed after {} tries. This indicates a bug ", "in the formatter where it formats the file differently each time. As a ", "temporary workaround you can ignore this file." ), count, ) } } } None => Ok(None), } } /// Format stdin and write result to stdout. /// Treats input as set by `--ext` flag. /// Compatible with `--check` flag. fn format_stdin( fmt_flags: &FmtFlags, fmt_options: FmtOptions, file_path: &Path, ) -> Result<(), AnyError> { let mut source = String::new(); if stdin().read_to_string(&mut source).is_err() { bail!("Failed to read from stdin"); } let file = FileContents { had_bom: false, text: source.into(), }; let formatted_text = format_file( file_path, &file, &fmt_options.options, &fmt_options.unstable, None, )?; if fmt_flags.check { #[allow(clippy::print_stdout, reason = "actually want to output")] if formatted_text.is_some() { println!("Not formatted stdin"); } } else { stdout().write_all( formatted_text .as_ref() .map(|t| t.as_bytes()) .unwrap_or(file.text.as_bytes()), )?; } Ok(()) } fn files_str(len: usize) -> &'static str { if len == 1 { "file" } else { "files" } } fn get_typescript_config_builder( options: &FmtOptionsConfig, ) -> dprint_plugin_typescript::configuration::ConfigurationBuilder { use deno_config::deno_json::*; use dprint_plugin_typescript::configuration as dprint_config; let mut builder = dprint_plugin_typescript::configuration::ConfigurationBuilder::new(); builder.deno(); if let Some(use_tabs) = options.use_tabs { builder.use_tabs(use_tabs); } if let Some(line_width) = options.line_width { builder.line_width(line_width); } if let Some(indent_width) = options.indent_width { builder.indent_width(indent_width); } if let Some(single_quote) = options.single_quote && single_quote { builder.quote_style( dprint_plugin_typescript::configuration::QuoteStyle::PreferSingle, ); } if let Some(semi_colons) = options.semi_colons { builder.semi_colons(match semi_colons { true => dprint_plugin_typescript::configuration::SemiColons::Prefer, false => dprint_plugin_typescript::configuration::SemiColons::Asi, }); } if let Some(quote_props) = options.quote_props { builder.quote_props(match quote_props { QuoteProps::AsNeeded => dprint_config::QuoteProps::AsNeeded, QuoteProps::Consistent => dprint_config::QuoteProps::Consistent, QuoteProps::Preserve => dprint_config::QuoteProps::Preserve, }); } if let Some(new_line_kind) = options.new_line_kind { builder.new_line_kind(match new_line_kind { NewLineKind::Auto => dprint_core::configuration::NewLineKind::Auto, NewLineKind::CarriageReturnLineFeed => { dprint_core::configuration::NewLineKind::CarriageReturnLineFeed } NewLineKind::LineFeed => { dprint_core::configuration::NewLineKind::LineFeed } NewLineKind::System => { if cfg!(windows) { dprint_core::configuration::NewLineKind::CarriageReturnLineFeed } else { dprint_core::configuration::NewLineKind::LineFeed } } }); } if let Some(use_braces) = options.use_braces { builder.use_braces(match use_braces { UseBraces::Always => dprint_config::UseBraces::Always, UseBraces::Maintain => dprint_config::UseBraces::Maintain, UseBraces::PreferNone => dprint_config::UseBraces::PreferNone, UseBraces::WhenNotSingleLine => { dprint_config::UseBraces::WhenNotSingleLine } }); } if let Some(brace_position) = options.brace_position { builder.brace_position(match brace_position { BracePosition::Maintain => dprint_config::BracePosition::Maintain, BracePosition::NextLine => dprint_config::BracePosition::NextLine, BracePosition::SameLine => dprint_config::BracePosition::SameLine, BracePosition::SameLineUnlessHanging => { dprint_config::BracePosition::SameLineUnlessHanging } }); } if let Some(single_body_position) = options.single_body_position { builder.single_body_position(match single_body_position { SingleBodyPosition::Maintain => { dprint_config::SameOrNextLinePosition::Maintain } SingleBodyPosition::NextLine => { dprint_config::SameOrNextLinePosition::NextLine } SingleBodyPosition::SameLine => { dprint_config::SameOrNextLinePosition::SameLine } }); } if let Some(next_control_flow_position) = options.next_control_flow_position { builder.next_control_flow_position(match next_control_flow_position { NextControlFlowPosition::Maintain => { dprint_config::NextControlFlowPosition::Maintain } NextControlFlowPosition::NextLine => { dprint_config::NextControlFlowPosition::NextLine } NextControlFlowPosition::SameLine => { dprint_config::NextControlFlowPosition::SameLine } }); } if let Some(trailing_commas) = options.trailing_commas { builder.trailing_commas(match trailing_commas { TrailingCommas::Always => dprint_config::TrailingCommas::Always, TrailingCommas::Never => dprint_config::TrailingCommas::Never, TrailingCommas::OnlyMultiLine => { dprint_config::TrailingCommas::OnlyMultiLine } }); } if let Some(operator_position) = options.operator_position { let option = match operator_position { OperatorPosition::Maintain => dprint_config::OperatorPosition::Maintain, OperatorPosition::NextLine => dprint_config::OperatorPosition::NextLine, OperatorPosition::SameLine => dprint_config::OperatorPosition::SameLine, }; // Because Deno's defaults are set at AST specific options, we need to // set them to AST specific options to override them. builder.binary_expression_operator_position(option); builder.conditional_expression_operator_position(option); builder.conditional_type_operator_position(option); } if let Some(jsx_bracket_position) = options.jsx_bracket_position { builder.jsx_bracket_position(match jsx_bracket_position { BracketPosition::Maintain => { dprint_config::SameOrNextLinePosition::Maintain } BracketPosition::NextLine => { dprint_config::SameOrNextLinePosition::NextLine } BracketPosition::SameLine => { dprint_config::SameOrNextLinePosition::SameLine } }); } if let Some(jsx_force_new_lines_surrounding_content) = options.jsx_force_new_lines_surrounding_content { builder.jsx_force_new_lines_surrounding_content( jsx_force_new_lines_surrounding_content, ); } if let Some(jsx_multi_line_parens) = options.jsx_multi_line_parens { builder.jsx_multi_line_parens(match jsx_multi_line_parens { MultiLineParens::Always => dprint_config::JsxMultiLineParens::Always, MultiLineParens::Never => dprint_config::JsxMultiLineParens::Never, MultiLineParens::Prefer => dprint_config::JsxMultiLineParens::Prefer, }); } if let Some(type_literal_separator_kind) = options.type_literal_separator_kind { builder.type_literal_separator_kind(match type_literal_separator_kind { SeparatorKind::Comma => dprint_config::SemiColonOrComma::Comma, SeparatorKind::SemiColon => dprint_config::SemiColonOrComma::SemiColon, }); } if let Some(space_around) = options.space_around { builder.space_around(space_around); } if let Some(space_surrounding_properties) = options.space_surrounding_properties { builder.space_surrounding_properties(space_surrounding_properties); builder.import_declaration_space_surrounding_named_imports( space_surrounding_properties, ); builder.export_declaration_space_surrounding_named_exports( space_surrounding_properties, ); } if let Some(sort_named_imports) = options.sort_named_imports { builder.import_declaration_sort_named_imports(match sort_named_imports { SortOrder::Maintain => dprint_config::SortOrder::Maintain, SortOrder::CaseSensitive => dprint_config::SortOrder::CaseSensitive, SortOrder::CaseInsensitive => dprint_config::SortOrder::CaseInsensitive, }); } if let Some(sort_named_exports) = options.sort_named_exports { builder.export_declaration_sort_named_exports(match sort_named_exports { SortOrder::Maintain => dprint_config::SortOrder::Maintain, SortOrder::CaseSensitive => dprint_config::SortOrder::CaseSensitive, SortOrder::CaseInsensitive => dprint_config::SortOrder::CaseInsensitive, }); } builder } fn get_resolved_typescript_config( options: &FmtOptionsConfig, ) -> dprint_plugin_typescript::configuration::Configuration { get_typescript_config_builder(options).build() } fn get_resolved_markdown_config( options: &FmtOptionsConfig, ) -> dprint_plugin_markdown::configuration::Configuration { let mut builder = dprint_plugin_markdown::configuration::ConfigurationBuilder::new(); builder.deno(); if let Some(line_width) = options.line_width { builder.line_width(line_width); } if let Some(prose_wrap) = options.prose_wrap { builder.text_wrap(match prose_wrap { ProseWrap::Always => { dprint_plugin_markdown::configuration::TextWrap::Always } ProseWrap::Never => { dprint_plugin_markdown::configuration::TextWrap::Never } ProseWrap::Preserve => { dprint_plugin_markdown::configuration::TextWrap::Maintain } }); } if let Some(new_line_kind) = options.new_line_kind { builder.new_line_kind(match new_line_kind { NewLineKind::Auto => dprint_core::configuration::NewLineKind::Auto, NewLineKind::CarriageReturnLineFeed => { dprint_core::configuration::NewLineKind::CarriageReturnLineFeed } NewLineKind::LineFeed => { dprint_core::configuration::NewLineKind::LineFeed } NewLineKind::System => { if cfg!(windows) { dprint_core::configuration::NewLineKind::CarriageReturnLineFeed } else { dprint_core::configuration::NewLineKind::LineFeed } } }); } builder.build() } fn get_resolved_json_config( options: &FmtOptionsConfig, ) -> dprint_plugin_json::configuration::Configuration { use deno_config::deno_json::JsonTrailingCommaKind; use dprint_plugin_json::configuration::TrailingCommaKind; let mut builder = dprint_plugin_json::configuration::ConfigurationBuilder::new(); builder.deno(); if let Some(json_trailing_commas) = options.json_trailing_commas { builder.trailing_commas(match json_trailing_commas { JsonTrailingCommaKind::Always => TrailingCommaKind::Always, JsonTrailingCommaKind::Jsonc => TrailingCommaKind::Jsonc, JsonTrailingCommaKind::Maintain => TrailingCommaKind::Maintain, JsonTrailingCommaKind::Never => TrailingCommaKind::Never, }); } if let Some(use_tabs) = options.use_tabs { builder.use_tabs(use_tabs); } if let Some(line_width) = options.line_width { builder.line_width(line_width); } if let Some(indent_width) = options.indent_width { builder.indent_width(indent_width); } if let Some(new_line_kind) = options.new_line_kind { builder.new_line_kind(match new_line_kind { NewLineKind::Auto => dprint_core::configuration::NewLineKind::Auto, NewLineKind::CarriageReturnLineFeed => { dprint_core::configuration::NewLineKind::CarriageReturnLineFeed } NewLineKind::LineFeed => { dprint_core::configuration::NewLineKind::LineFeed } NewLineKind::System => { if cfg!(windows) { dprint_core::configuration::NewLineKind::CarriageReturnLineFeed } else { dprint_core::configuration::NewLineKind::LineFeed } } }); } builder.build() } fn get_resolved_lax_markup_config( options: &FmtOptionsConfig, ) -> lax_markup::configuration::Configuration { lax_markup::configuration::Configuration { line_width: options.line_width.unwrap_or(80), use_tabs: options.use_tabs.unwrap_or_default(), indent_width: options.indent_width.unwrap_or(2), new_line_kind: dprint_core::configuration::NewLineKind::LineFeed, ignore_node_comment_text: "deno-fmt-ignore".to_string(), ignore_file_comment_text: "deno-fmt-ignore-file".to_string(), } } fn get_resolved_yaml_config( options: &FmtOptionsConfig, ) -> pretty_yaml::config::FormatOptions { use pretty_yaml::config::*; let layout_options = LayoutOptions { print_width: options.line_width.unwrap_or(80) as usize, indent_width: options.indent_width.unwrap_or(2) as usize, line_break: LineBreak::Lf, }; let language_options = LanguageOptions { quotes: if let Some(true) = options.single_quote { Quotes::PreferSingle } else { Quotes::PreferDouble }, trailing_comma: true, format_comments: false, indent_block_sequence_in_map: true, brace_spacing: true, bracket_spacing: false, dash_spacing: DashSpacing::OneSpace, prefer_single_line: false, flow_sequence_prefer_single_line: None, flow_map_prefer_single_line: None, trim_trailing_whitespaces: true, trim_trailing_zero: false, ignore_comment_directive: "deno-fmt-ignore".into(), }; FormatOptions { layout: layout_options, language: language_options, } } pub struct FileContents<'a> { pub text: Cow<'a, str>, pub had_bom: bool, } fn read_file_contents(file_path: &Path) -> Result<FileContents<'_>, AnyError> { let file_bytes = fs::read(file_path) .with_context(|| format!("Error reading {}", file_path.display()))?; let had_bom = file_bytes.starts_with(&[0xEF, 0xBB, 0xBF]); let charset = deno_media_type::encoding::detect_charset_local_file(&file_bytes); let text = deno_media_type::encoding::decode_owned_source( charset, file_bytes.to_vec(), ) .with_context(|| { anyhow!("{} is not a valid UTF-8 file", file_path.display()) })?; Ok(FileContents { text: Cow::Owned(text), had_bom, }) } fn write_file_contents( file_path: &Path, file_contents: &str, ) -> Result<(), AnyError> { Ok(fs::write(file_path, file_contents)?) } pub async fn run_parallelized<F>( file_paths: Vec<PathBuf>, f: F, ) -> Result<(), AnyError> where F: FnOnce(PathBuf) -> Result<(), AnyError> + Send + 'static + Clone, { let handles = file_paths.iter().map(|file_path| { let f = f.clone(); let file_path = file_path.clone(); spawn_blocking(move || f(file_path)) }); let join_results = futures::future::join_all(handles).await; // find the tasks that panicked and let the user know which files let panic_file_paths = join_results .iter() .enumerate() .filter_map(|(i, join_result)| { join_result .as_ref() .err() .map(|_| file_paths[i].to_string_lossy()) }) .collect::<Vec<_>>(); if !panic_file_paths.is_empty() { panic!("Panic formatting: {}", panic_file_paths.join(", ")) } // check for any errors and if so return the first one let mut errors = join_results.into_iter().filter_map(|join_result| { join_result .ok() .and_then(|handle_result| handle_result.err()) }); match errors.next() { Some(e) => Err(e), _ => Ok(()), } } /// This function is similar to is_supported_ext but adds additional extensions /// supported by `deno fmt`. fn is_supported_ext_fmt(path: &Path) -> bool { get_extension(path).is_some_and(|ext| { matches!( ext.as_str(), "ts" | "tsx" | "js" | "jsx" | "cjs" | "cts" | "mjs" | "mts" | "json" | "jsonc" | "css" | "scss" | "less" | "html" | "svelte" | "vue" | "astro" | "vto" | "njk" | "md" | "mkd" | "mkdn" | "mdwn" | "mdown" | "markdown" | "yml" | "yaml" | "ipynb" | "sql" | "xml" | "svg" | "mustache" ) }) } #[cfg(test)] mod test { use deno_config::deno_json::JsonTrailingCommaKind; use test_util::assert_starts_with; use super::*; #[test] fn test_is_supported_ext_fmt() { assert!(!is_supported_ext_fmt(Path::new("tests/subdir/redirects"))); assert!(is_supported_ext_fmt(Path::new("README.md"))); assert!(is_supported_ext_fmt(Path::new("readme.MD"))); assert!(is_supported_ext_fmt(Path::new("readme.mkd"))); assert!(is_supported_ext_fmt(Path::new("readme.mkdn"))); assert!(is_supported_ext_fmt(Path::new("readme.mdwn"))); assert!(is_supported_ext_fmt(Path::new("readme.mdown"))); assert!(is_supported_ext_fmt(Path::new("readme.markdown"))); assert!(is_supported_ext_fmt(Path::new("lib/typescript.d.ts"))); assert!(is_supported_ext_fmt(Path::new("testdata/run/001_hello.js"))); assert!(is_supported_ext_fmt(Path::new("testdata/run/002_hello.ts"))); assert!(is_supported_ext_fmt(Path::new("foo.jsx"))); assert!(is_supported_ext_fmt(Path::new("foo.tsx"))); assert!(is_supported_ext_fmt(Path::new("foo.TS"))); assert!(is_supported_ext_fmt(Path::new("foo.TSX"))); assert!(is_supported_ext_fmt(Path::new("foo.JS"))); assert!(is_supported_ext_fmt(Path::new("foo.JSX"))); assert!(is_supported_ext_fmt(Path::new("foo.mjs"))); assert!(!is_supported_ext_fmt(Path::new("foo.mjsx"))); assert!(is_supported_ext_fmt(Path::new("foo.jsonc"))); assert!(is_supported_ext_fmt(Path::new("foo.JSONC"))); assert!(is_supported_ext_fmt(Path::new("foo.json"))); assert!(is_supported_ext_fmt(Path::new("foo.JsON"))); assert!(is_supported_ext_fmt(Path::new("foo.css"))); assert!(is_supported_ext_fmt(Path::new("foo.Css"))); assert!(is_supported_ext_fmt(Path::new("foo.scss"))); assert!(is_supported_ext_fmt(Path::new("foo.SCSS"))); assert!(!is_supported_ext_fmt(Path::new("foo.sass"))); assert!(!is_supported_ext_fmt(Path::new("foo.Sass"))); assert!(is_supported_ext_fmt(Path::new("foo.less"))); assert!(is_supported_ext_fmt(Path::new("foo.LeSS"))); assert!(is_supported_ext_fmt(Path::new("foo.html"))); assert!(is_supported_ext_fmt(Path::new("foo.HTML"))); assert!(is_supported_ext_fmt(Path::new("foo.svelte"))); assert!(is_supported_ext_fmt(Path::new("foo.Svelte"))); assert!(is_supported_ext_fmt(Path::new("foo.vue"))); assert!(is_supported_ext_fmt(Path::new("foo.VUE"))); assert!(is_supported_ext_fmt(Path::new("foo.astro"))); assert!(is_supported_ext_fmt(Path::new("foo.AsTrO"))); assert!(is_supported_ext_fmt(Path::new("foo.vto"))); assert!(is_supported_ext_fmt(Path::new("foo.Vto"))); assert!(is_supported_ext_fmt(Path::new("foo.njk"))); assert!(is_supported_ext_fmt(Path::new("foo.NJk"))); assert!(is_supported_ext_fmt(Path::new("foo.yml"))); assert!(is_supported_ext_fmt(Path::new("foo.Yml"))); assert!(is_supported_ext_fmt(Path::new("foo.yaml"))); assert!(is_supported_ext_fmt(Path::new("foo.YaML"))); assert!(is_supported_ext_fmt(Path::new("foo.ipynb"))); assert!(is_supported_ext_fmt(Path::new("foo.sql"))); assert!(is_supported_ext_fmt(Path::new("foo.Sql"))); assert!(is_supported_ext_fmt(Path::new("foo.sQl"))); assert!(is_supported_ext_fmt(Path::new("foo.sqL"))); assert!(is_supported_ext_fmt(Path::new("foo.SQL"))); } #[test] fn test_format_ensure_stable_unstable_format() { let err = format_ensure_stable( Path::new("mod.ts"), &FileContents { had_bom: false, text: "1".into(), }, |_, file| Ok(Some(format!("1{}", file.text))), ) .unwrap_err(); assert_starts_with!( err.to_string(), "Formatting not stable. Bailed after 5 tries." ); } #[test] fn test_format_ensure_stable_error_first() { let err = format_ensure_stable( Path::new("mod.ts"), &FileContents { had_bom: false, text: "1".into(), }, |_, _| bail!("Error formatting."), ) .unwrap_err(); assert_eq!(err.to_string(), "Error formatting."); } #[test] fn test_format_ensure_stable_error_second() { let err = format_ensure_stable( Path::new("mod.ts"), &FileContents { had_bom: false, text: "1".into(), }, |_, file| { if file.text == "1" { Ok(Some("11".to_string())) } else { bail!("Error formatting.") } }, ) .unwrap_err(); assert_starts_with!( err.to_string(), "Formatting succeeded initially, but failed when" ); } #[test] fn test_format_stable_after_two() { let result = format_ensure_stable( Path::new("mod.ts"), &FileContents { had_bom: false, text: "1".into(), }, |_, file| { if file.text == "1" { Ok(Some("11".to_string())) } else if file.text == "11" { Ok(None) } else { unreachable!(); } }, ) .unwrap(); assert_eq!(result, Some("11".to_string())); } #[test] fn test_single_quote_true_prefers_single_quote() { let file_text = format_file( Path::new("test.ts"), &FileContents { had_bom: false, text: "console.log(\"there's\");\nconsole.log('hi');\nconsole.log(\"bye\")\n".into(), }, &FmtOptionsConfig { single_quote: Some(true), ..Default::default() }, &UnstableFmtOptions::default(), None, ) .unwrap() .unwrap(); assert_eq!( file_text, // should use double quotes for the string with a single quote "console.log(\"there's\");\nconsole.log('hi');\nconsole.log('bye');\n", ); } #[test] fn test_formated_removes_utf8_bom() { let file_text = format_file( Path::new("test.ts"), &FileContents { had_bom: true, text: "let a = 1;".into(), }, &FmtOptionsConfig { single_quote: Some(true), ..Default::default() }, &UnstableFmtOptions::default(), None, ) .unwrap() .unwrap(); assert_eq!(file_text, "let a = 1;\n",); } #[test] fn test_jsonc_does_not_add_trailing_commas_by_default() { let file_text = format_file( Path::new("test.jsonc"), &FileContents { had_bom: false, text: r#"{ "a": 1, "b": 2 } "# .into(), }, &FmtOptionsConfig::default(), &UnstableFmtOptions::default(), None, ) .unwrap(); assert_eq!(file_text, None); } #[test] fn test_json_does_not_add_trailing_commas() { let file_text = format_file( Path::new("test.json"), &FileContents { had_bom: false, text: r#"{ "a": 1, "b": 2 } "# .into(), }, &FmtOptionsConfig::default(), &UnstableFmtOptions::default(), None, ) .unwrap(); assert_eq!(file_text, None); } #[test] fn test_jsonc_trailing_commas_can_be_disabled() { let file_text = format_file( Path::new("test.jsonc"), &FileContents { had_bom: false, text: r#"{ "a": 1, "b": 2 } "# .into(), }, &FmtOptionsConfig { json_trailing_commas: Some(JsonTrailingCommaKind::Never), ..Default::default() }, &UnstableFmtOptions::default(), None, ) .unwrap(); assert_eq!(file_text, None); } #[test] fn test_json_trailing_commas_can_be_enabled() { let file_text = format_file( Path::new("test.json"), &FileContents { had_bom: false, text: r#"{ "a": 1, "b": 2 } "# .into(), }, &FmtOptionsConfig { json_trailing_commas: Some(JsonTrailingCommaKind::Always), ..Default::default() }, &UnstableFmtOptions::default(), None, ) .unwrap() .unwrap(); assert_eq!( file_text, r#"{ "a": 1, "b": 2, } "# ); } }