diff --git a/CHANGELOG.md b/CHANGELOG.md index 58dc66caaad3..c440d429765c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -279,6 +279,7 @@ Biome now scores 97% compatibility with Prettier and features more than 180 lint #### Bug fixes - Fix [#933](https://github.com/biomejs/biome/issues/933). Some files are properly ignored in the LSP too. E.g. `package.json`, `tsconfig.json`, etc. +- Fix [#1394](https://github.com/biomejs/biome/issues/1394), by inferring the language extension from the internal saved files. Now newly created files JavaScript correctly show diagnostics. ### Formatter diff --git a/crates/biome_lsp/tests/server.rs b/crates/biome_lsp/tests/server.rs index 301b2f19e6e5..97f1f2a0cb55 100644 --- a/crates/biome_lsp/tests/server.rs +++ b/crates/biome_lsp/tests/server.rs @@ -204,6 +204,21 @@ impl Server { .await } + async fn open_untitled_document(&mut self, text: impl Display) -> Result<()> { + self.notify( + "textDocument/didOpen", + DidOpenTextDocumentParams { + text_document: TextDocumentItem { + uri: url!("untitled-1"), + language_id: String::from("javascript"), + version: 0, + text: text.to_string(), + }, + }, + ) + .await + } + /// Opens a document with given contents and given name. The name must contain the extension too async fn open_named_document( &mut self, @@ -624,6 +639,88 @@ async fn pull_diagnostics() -> Result<()> { Ok(()) } +#[tokio::test] +async fn pull_diagnostics_from_new_file() -> Result<()> { + let factory = ServerFactory::default(); + let (service, client) = factory.create(None).into_inner(); + let (stream, sink) = client.split(); + let mut server = Server::new(service); + + let (sender, mut receiver) = channel(CHANNEL_BUFFER_SIZE); + let reader = tokio::spawn(client_handler(stream, sink, sender)); + + server.initialize().await?; + server.initialized().await?; + + server.open_untitled_document("if(a == b) {}").await?; + + let notification = tokio::select! { + msg = receiver.next() => msg, + _ = sleep(Duration::from_secs(1)) => { + panic!("timed out waiting for the server to send diagnostics") + } + }; + + assert_eq!( + notification, + Some(ServerNotification::PublishDiagnostics( + PublishDiagnosticsParams { + uri: url!("untitled-1"), + version: Some(0), + diagnostics: vec![lsp::Diagnostic { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 5, + }, + end: lsp::Position { + line: 0, + character: 7, + }, + }, + severity: Some(lsp::DiagnosticSeverity::ERROR), + code: Some(lsp::NumberOrString::String(String::from( + "lint/suspicious/noDoubleEquals", + ))), + code_description: Some(CodeDescription { + href: Url::parse("https://biomejs.dev/linter/rules/no-double-equals") + .unwrap() + }), + source: Some(String::from("biome")), + message: String::from( + "Use === instead of ==.\n== is only allowed when comparing against `null`", + ), + related_information: Some(vec![lsp::DiagnosticRelatedInformation { + location: lsp::Location { + uri: url!("untitled-1"), + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 5, + }, + end: lsp::Position { + line: 0, + character: 7, + }, + }, + }, + message: String::new(), + }]), + tags: None, + data: None, + }], + } + )) + ); + + server.close_document().await?; + + server.shutdown().await?; + reader.abort(); + + Ok(()) +} + fn fixable_diagnostic(line: u32) -> Result { Ok(lsp::Diagnostic { range: lsp::Range { diff --git a/crates/biome_service/src/file_handlers/javascript.rs b/crates/biome_service/src/file_handlers/javascript.rs index 1db19fc886fc..9dc42093f06f 100644 --- a/crates/biome_service/src/file_handlers/javascript.rs +++ b/crates/biome_service/src/file_handlers/javascript.rs @@ -43,7 +43,7 @@ use biome_rowan::{AstNode, BatchMutationExt, Direction, FileSource, NodeCache}; use std::borrow::Cow; use std::fmt::Debug; use std::path::PathBuf; -use tracing::{debug, error, info, trace}; +use tracing::{debug, debug_span, error, info, trace}; #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] @@ -267,89 +267,101 @@ fn debug_formatter_ir( } fn lint(params: LintParams) -> LintResults { - let Ok(file_source) = params.parse.file_source(params.path) else { - return LintResults { - errors: 0, - diagnostics: vec![], - skipped_diagnostics: 0, - }; - }; - let tree = params.parse.tree(); - let mut diagnostics = params.parse.into_diagnostics(); + debug_span!("Linting JavaScript file", path =? params.path, language =? params.language) + .in_scope(move || { + let file_source = match params.parse.file_source(params.path) { + Ok(file_source) => file_source, + Err(_) => { + if let Some(file_source) = params.language.as_js_file_source() { + file_source + } else { + return LintResults { + errors: 0, + diagnostics: vec![], + skipped_diagnostics: 0, + }; + } + } + }; + let tree = params.parse.tree(); + let mut diagnostics = params.parse.into_diagnostics(); - let analyzer_options = - compute_analyzer_options(¶ms.settings, PathBuf::from(params.path.as_path())); + let analyzer_options = + compute_analyzer_options(¶ms.settings, PathBuf::from(params.path.as_path())); - let mut diagnostic_count = diagnostics.len() as u64; - let mut errors = diagnostics - .iter() - .filter(|diag| diag.severity() <= Severity::Error) - .count(); + let mut diagnostic_count = diagnostics.len() as u64; + let mut errors = diagnostics + .iter() + .filter(|diag| diag.severity() <= Severity::Error) + .count(); + + let has_lint = params.filter.categories.contains(RuleCategories::LINT); + + info!("Analyze file {}", params.path.display()); + let (_, analyze_diagnostics) = analyze( + &tree, + params.filter, + &analyzer_options, + file_source, + |signal| { + if let Some(mut diagnostic) = signal.diagnostic() { + // Do not report unused suppression comment diagnostics if this is a syntax-only analyzer pass + if !has_lint + && diagnostic.category() == Some(category!("suppressions/unused")) + { + return ControlFlow::::Continue(()); + } - let has_lint = params.filter.categories.contains(RuleCategories::LINT); + diagnostic_count += 1; + + // We do now check if the severity of the diagnostics should be changed. + // The configuration allows to change the severity of the diagnostics emitted by rules. + let severity = diagnostic + .category() + .filter(|category| category.name().starts_with("lint/")) + .map(|category| { + params + .rules + .and_then(|rules| rules.get_severity_from_code(category)) + .unwrap_or(Severity::Warning) + }) + .unwrap_or_else(|| diagnostic.severity()); + + if severity >= Severity::Error { + errors += 1; + } - info!("Analyze file {}", params.path.display()); - let (_, analyze_diagnostics) = analyze( - &tree, - params.filter, - &analyzer_options, - file_source, - |signal| { - if let Some(mut diagnostic) = signal.diagnostic() { - // Do not report unused suppression comment diagnostics if this is a syntax-only analyzer pass - if !has_lint && diagnostic.category() == Some(category!("suppressions/unused")) { - return ControlFlow::::Continue(()); - } + if diagnostic_count <= params.max_diagnostics { + for action in signal.actions() { + if !action.is_suppression() { + diagnostic = diagnostic.add_code_suggestion(action.into()); + } + } - diagnostic_count += 1; - - // We do now check if the severity of the diagnostics should be changed. - // The configuration allows to change the severity of the diagnostics emitted by rules. - let severity = diagnostic - .category() - .filter(|category| category.name().starts_with("lint/")) - .map(|category| { - params - .rules - .and_then(|rules| rules.get_severity_from_code(category)) - .unwrap_or(Severity::Warning) - }) - .unwrap_or_else(|| diagnostic.severity()); - - if severity >= Severity::Error { - errors += 1; - } + let error = diagnostic.with_severity(severity); - if diagnostic_count <= params.max_diagnostics { - for action in signal.actions() { - if !action.is_suppression() { - diagnostic = diagnostic.add_code_suggestion(action.into()); + diagnostics.push(biome_diagnostics::serde::Diagnostic::new(error)); } } - let error = diagnostic.with_severity(severity); - - diagnostics.push(biome_diagnostics::serde::Diagnostic::new(error)); - } + ControlFlow::::Continue(()) + }, + ); + + diagnostics.extend( + analyze_diagnostics + .into_iter() + .map(biome_diagnostics::serde::Diagnostic::new) + .collect::>(), + ); + let skipped_diagnostics = diagnostic_count.saturating_sub(diagnostics.len() as u64); + + LintResults { + diagnostics, + errors, + skipped_diagnostics, } - - ControlFlow::::Continue(()) - }, - ); - - diagnostics.extend( - analyze_diagnostics - .into_iter() - .map(biome_diagnostics::serde::Diagnostic::new) - .collect::>(), - ); - let skipped_diagnostics = diagnostic_count.saturating_sub(diagnostics.len() as u64); - - LintResults { - diagnostics, - errors, - skipped_diagnostics, - } + }) } struct ActionsVisitor<'a> { @@ -380,7 +392,7 @@ impl RegistryVisitor for ActionsVisitor<'_> { } } -#[tracing::instrument(level = "trace", skip(parse))] +#[tracing::instrument(level = "debug", skip(parse, settings))] fn code_actions( parse: AnyParse, range: TextRange, @@ -555,7 +567,7 @@ fn fix_all(params: FixAllParams) -> Result { } } -#[tracing::instrument(level = "trace", skip(parse))] +#[tracing::instrument(level = "trace", skip(parse, settings))] fn format( rome_path: &RomePath, parse: AnyParse, @@ -576,7 +588,7 @@ fn format( } } } -#[tracing::instrument(level = "trace", skip(parse))] +#[tracing::instrument(level = "trace", skip(parse, settings))] fn format_range( rome_path: &RomePath, parse: AnyParse, @@ -590,7 +602,7 @@ fn format_range( Ok(printed) } -#[tracing::instrument(level = "trace", skip(parse))] +#[tracing::instrument(level = "trace", skip(parse, settings))] fn format_on_type( rome_path: &RomePath, parse: AnyParse, diff --git a/crates/biome_service/src/file_handlers/json.rs b/crates/biome_service/src/file_handlers/json.rs index f9ebf2b138ec..dc03a7277d03 100644 --- a/crates/biome_service/src/file_handlers/json.rs +++ b/crates/biome_service/src/file_handlers/json.rs @@ -193,7 +193,7 @@ fn debug_formatter_ir( Ok(root_element.to_string()) } -#[tracing::instrument(level = "debug", skip(parse))] +#[tracing::instrument(level = "debug", skip(parse, settings))] fn format( rome_path: &RomePath, parse: AnyParse, @@ -261,90 +261,94 @@ fn format_on_type( Ok(printed) } fn lint(params: LintParams) -> LintResults { - tracing::debug_span!("lint").in_scope(move || { - let root: JsonRoot = params.parse.tree(); - let mut diagnostics = params.parse.into_diagnostics(); - - // if we're parsing the `biome.json` file, we deserialize it, so we can emit diagnostics for - // malformed configuration - if params.path.ends_with(ROME_JSON) || params.path.ends_with(BIOME_JSON) { - let deserialized = deserialize_from_json_ast::(&root); + tracing::debug_span!("Linting JSON file", path =? params.path, language =? params.language) + .in_scope(move || { + let root: JsonRoot = params.parse.tree(); + let mut diagnostics = params.parse.into_diagnostics(); + + // if we're parsing the `biome.json` file, we deserialize it, so we can emit diagnostics for + // malformed configuration + if params.path.ends_with(ROME_JSON) || params.path.ends_with(BIOME_JSON) { + let deserialized = deserialize_from_json_ast::(&root); + diagnostics.extend( + deserialized + .into_diagnostics() + .into_iter() + .map(biome_diagnostics::serde::Diagnostic::new) + .collect::>(), + ); + } + + let mut diagnostic_count = diagnostics.len() as u64; + let mut errors = diagnostics + .iter() + .filter(|diag| diag.severity() <= Severity::Error) + .count(); + + let skipped_diagnostics = diagnostic_count - diagnostics.len() as u64; + + let has_lint = params.filter.categories.contains(RuleCategories::LINT); + let analyzer_options = + compute_analyzer_options(¶ms.settings, PathBuf::from(params.path.as_path())); + + let (_, analyze_diagnostics) = + analyze(&root, params.filter, &analyzer_options, |signal| { + if let Some(mut diagnostic) = signal.diagnostic() { + // Do not report unused suppression comment diagnostics if this is a syntax-only analyzer pass + if !has_lint + && diagnostic.category() == Some(category!("suppressions/unused")) + { + return ControlFlow::::Continue(()); + } + + diagnostic_count += 1; + + // We do now check if the severity of the diagnostics should be changed. + // The configuration allows to change the severity of the diagnostics emitted by rules. + let severity = diagnostic + .category() + .filter(|category| category.name().starts_with("lint/")) + .map(|category| { + params + .rules + .and_then(|rules| rules.get_severity_from_code(category)) + .unwrap_or(Severity::Warning) + }) + .unwrap_or_else(|| diagnostic.severity()); + + if severity <= Severity::Error { + errors += 1; + } + + if diagnostic_count <= params.max_diagnostics { + for action in signal.actions() { + if !action.is_suppression() { + diagnostic = diagnostic.add_code_suggestion(action.into()); + } + } + + let error = diagnostic.with_severity(severity); + + diagnostics.push(biome_diagnostics::serde::Diagnostic::new(error)); + } + } + + ControlFlow::::Continue(()) + }); + diagnostics.extend( - deserialized - .into_diagnostics() + analyze_diagnostics .into_iter() .map(biome_diagnostics::serde::Diagnostic::new) .collect::>(), ); - } - - let mut diagnostic_count = diagnostics.len() as u64; - let mut errors = diagnostics - .iter() - .filter(|diag| diag.severity() <= Severity::Error) - .count(); - - let skipped_diagnostics = diagnostic_count - diagnostics.len() as u64; - - let has_lint = params.filter.categories.contains(RuleCategories::LINT); - let analyzer_options = - compute_analyzer_options(¶ms.settings, PathBuf::from(params.path.as_path())); - - let (_, analyze_diagnostics) = analyze(&root, params.filter, &analyzer_options, |signal| { - if let Some(mut diagnostic) = signal.diagnostic() { - // Do not report unused suppression comment diagnostics if this is a syntax-only analyzer pass - if !has_lint && diagnostic.category() == Some(category!("suppressions/unused")) { - return ControlFlow::::Continue(()); - } - - diagnostic_count += 1; - - // We do now check if the severity of the diagnostics should be changed. - // The configuration allows to change the severity of the diagnostics emitted by rules. - let severity = diagnostic - .category() - .filter(|category| category.name().starts_with("lint/")) - .map(|category| { - params - .rules - .and_then(|rules| rules.get_severity_from_code(category)) - .unwrap_or(Severity::Warning) - }) - .unwrap_or_else(|| diagnostic.severity()); - - if severity <= Severity::Error { - errors += 1; - } - - if diagnostic_count <= params.max_diagnostics { - for action in signal.actions() { - if !action.is_suppression() { - diagnostic = diagnostic.add_code_suggestion(action.into()); - } - } - - let error = diagnostic.with_severity(severity); - diagnostics.push(biome_diagnostics::serde::Diagnostic::new(error)); - } + LintResults { + diagnostics, + errors, + skipped_diagnostics, } - - ControlFlow::::Continue(()) - }); - - diagnostics.extend( - analyze_diagnostics - .into_iter() - .map(biome_diagnostics::serde::Diagnostic::new) - .collect::>(), - ); - - LintResults { - diagnostics, - errors, - skipped_diagnostics, - } - }) + }) } fn code_actions( _parse: AnyParse, diff --git a/crates/biome_service/src/file_handlers/mod.rs b/crates/biome_service/src/file_handlers/mod.rs index ecdaef48acd3..fe71222b0d49 100644 --- a/crates/biome_service/src/file_handlers/mod.rs +++ b/crates/biome_service/src/file_handlers/mod.rs @@ -14,7 +14,7 @@ use biome_console::markup; use biome_diagnostics::{Diagnostic, Severity}; use biome_formatter::Printed; use biome_fs::RomePath; -use biome_js_syntax::{TextRange, TextSize}; +use biome_js_syntax::{JsFileSource, TextRange, TextSize}; use biome_parser::AnyParse; use biome_rowan::NodeCache; pub use javascript::JsFormatterSettings; @@ -162,6 +162,16 @@ impl Language { pub const fn is_css_like(&self) -> bool { matches!(self, Language::Css) } + + pub fn as_js_file_source(&self) -> Option { + match self { + Language::JavaScript => Some(JsFileSource::js_module()), + Language::JavaScriptReact => Some(JsFileSource::jsx()), + Language::TypeScript => Some(JsFileSource::tsx()), + Language::TypeScriptReact => Some(JsFileSource::tsx()), + Language::Json | Language::Jsonc | Language::Css | Language::Unknown => None, + } + } } impl biome_console::fmt::Display for Language { @@ -252,6 +262,7 @@ pub(crate) struct LintParams<'a> { pub(crate) filter: AnalysisFilter<'a>, pub(crate) rules: Option<&'a Rules>, pub(crate) settings: SettingsHandle<'a>, + pub(crate) language: Language, pub(crate) max_diagnostics: u64, pub(crate) path: &'a RomePath, } diff --git a/crates/biome_service/src/workspace/server.rs b/crates/biome_service/src/workspace/server.rs index 0c8628e5d27b..123125b21ae6 100644 --- a/crates/biome_service/src/workspace/server.rs +++ b/crates/biome_service/src/workspace/server.rs @@ -29,7 +29,7 @@ use dashmap::{mapref::entry::Entry, DashMap}; use std::ffi::OsStr; use std::path::Path; use std::{panic::RefUnwindSafe, sync::RwLock}; -use tracing::{info_span, trace}; +use tracing::{debug, info, info_span, trace}; pub(super) struct WorkspaceServer { /// features available throughout the application @@ -87,6 +87,7 @@ impl WorkspaceServer { fn get_file_capabilities(&self, path: &RomePath) -> Capabilities { let language = self.get_language(path); + debug!("File capabilities: {:?} {:?}", &language, &path); self.features.get_capabilities(path, language) } @@ -455,6 +456,7 @@ impl Workspace for WorkspaceServer { } /// Retrieves the list of diagnostics associated with a file + #[tracing::instrument(level = "debug", skip(self))] fn pull_diagnostics( &self, params: PullDiagnosticsParams, @@ -491,6 +493,7 @@ impl Workspace for WorkspaceServer { settings: self.settings(), max_diagnostics: params.max_diagnostics, path: ¶ms.path, + language: self.get_language(¶ms.path), }); ( @@ -509,6 +512,7 @@ impl Workspace for WorkspaceServer { (parse_diagnostics, errors, 0) }; + info!("Pulled {:?} diagnostic(s)", diagnostics.len()); Ok(PullDiagnosticsResult { diagnostics: diagnostics .into_iter() @@ -524,6 +528,7 @@ impl Workspace for WorkspaceServer { /// Retrieves the list of code actions available for a given cursor /// position within a file + #[tracing::instrument(level = "debug", skip(self))] fn pull_actions(&self, params: PullActionsParams) -> Result { let capabilities = self.get_file_capabilities(¶ms.path); let code_actions = capabilities diff --git a/justfile b/justfile index 9cfeb502fb51..e436473e1de0 100644 --- a/justfile +++ b/justfile @@ -19,16 +19,16 @@ upgrade-tools: cargo binstall cargo-insta cargo-nextest taplo-cli wasm-pack wasm-tools cargo-workspaces --force # Generate all files across crates and tools. You rarely want to use it locally. -codegen: +gen: cargo codegen all cargo codegen-configuration cargo lintdoc - just codegen-bindings - cargo codegen-website + just gen-bindings + jest gen-web cargo format # Generates TypeScript types and JSON schema of the configuration -codegen-bindings: +gen-bindings: cargo codegen-schema cargo codegen-bindings @@ -36,7 +36,7 @@ codegen-bindings: gen-lint: cargo codegen analyzer cargo codegen-configuration - just codegen-bindings + just gen-bindings cargo lintdoc # Generates code generated files for the website diff --git a/website/src/content/docs/internals/changelog.mdx b/website/src/content/docs/internals/changelog.mdx index c9d90f9ba139..6e3c156337ed 100644 --- a/website/src/content/docs/internals/changelog.mdx +++ b/website/src/content/docs/internals/changelog.mdx @@ -285,6 +285,7 @@ Biome now scores 97% compatibility with Prettier and features more than 180 lint #### Bug fixes - Fix [#933](https://github.com/biomejs/biome/issues/933). Some files are properly ignored in the LSP too. E.g. `package.json`, `tsconfig.json`, etc. +- Fix [#1394](https://github.com/biomejs/biome/issues/1394), by inferring the language extension from the internal saved files. Now newly created files JavaScript correctly show diagnostics. ### Formatter