diff --git a/crates/pyrefly_config/src/error_kind.rs b/crates/pyrefly_config/src/error_kind.rs index 6530212c8..dbbe5a15a 100644 --- a/crates/pyrefly_config/src/error_kind.rs +++ b/crates/pyrefly_config/src/error_kind.rs @@ -273,6 +273,15 @@ impl ErrorKind { _ => Severity::Error, } } + + /// Returns the public documentation URL for this error kind. + /// Example: https://pyrefly.org/en/docs/error-kinds/#bad-context-manager + pub fn docs_url(self) -> String { + format!( + "https://pyrefly.org/en/docs/error-kinds/#{}", + self.to_name() + ) + } } #[cfg(test)] diff --git a/pyrefly/lib/error/error.rs b/pyrefly/lib/error/error.rs index 0057a496e..76286c871 100644 --- a/pyrefly/lib/error/error.rs +++ b/pyrefly/lib/error/error.rs @@ -12,7 +12,9 @@ use std::io::Write; use std::path::Path; use itertools::Itertools; +use lsp_types::CodeDescription; use lsp_types::Diagnostic; +use lsp_types::Url; use pyrefly_python::module::Module; use pyrefly_python::module_path::ModulePath; use pyrefly_util::display::number_thousands; @@ -168,6 +170,13 @@ impl Error { /// Create a diagnostic suitable for use in LSP. pub fn to_diagnostic(&self) -> Diagnostic { + let code = self.error_kind().to_name().to_owned(); + let code_description = Url::parse(&format!( + "{}", + self.error_kind().docs_url() + )) + .ok() + .map(|href| CodeDescription { href }); Diagnostic { range: self.lined_buffer().to_lsp_range(self.range()), severity: Some(match self.severity() { @@ -179,9 +188,8 @@ impl Error { }), source: Some("Pyrefly".to_owned()), message: self.msg().to_owned(), - code: Some(lsp_types::NumberOrString::String( - self.error_kind().to_name().to_owned(), - )), + code: Some(lsp_types::NumberOrString::String(code)), + code_description, ..Default::default() } } diff --git a/pyrefly/lib/test/lsp/lsp_interaction/diagnostic.rs b/pyrefly/lib/test/lsp/lsp_interaction/diagnostic.rs index fa6cfb33d..991a44667 100644 --- a/pyrefly/lib/test/lsp/lsp_interaction/diagnostic.rs +++ b/pyrefly/lib/test/lsp/lsp_interaction/diagnostic.rs @@ -52,3 +52,86 @@ fn test_unexpected_keyword_range() { interaction.shutdown(); } + +#[test] +fn test_error_documentation_links() { + let test_files_root = get_test_files_root(); + let mut interaction = LspInteraction::new(); + interaction.set_root(test_files_root.path().to_path_buf()); + interaction.initialize(InitializeSettings { + configuration: Some(None), + ..Default::default() + }); + + interaction.server.did_change_configuration(); + + interaction.client.expect_configuration_request(2, None); + interaction.server.send_configuration_response(2, serde_json::json!([{"pyrefly": {"displayTypeErrors": "force-on"}}, {"pyrefly": {"displayTypeErrors": "force-on"}}])); + + interaction.server.did_open("error_docs_test.py"); + interaction.server.diagnostic("error_docs_test.py"); + + interaction.client.expect_response(Response { + id: RequestId::from(2), + result: Some(serde_json::json!({ + "items": [ + { + "code": "bad-assignment", + "codeDescription": { + "href": "https://pyrefly.org/en/docs/error-kinds/#bad-assignment" + }, + "message": "`Literal['']` is not assignable to `int`", + "range": { + "end": {"character": 11, "line": 9}, + "start": {"character": 9, "line": 9} + }, + "severity": 1, + "source": "Pyrefly" + }, + { + "code": "bad-context-manager", + "codeDescription": { + "href": "https://pyrefly.org/en/docs/error-kinds/#bad-context-manager" + }, + "message": "Cannot use `A` as a context manager\n Object of class `A` has no attribute `__enter__`", + "range": { + "end": {"character": 8, "line": 15}, + "start": {"character": 5, "line": 15} + }, + "severity": 1, + "source": "Pyrefly" + }, + { + "code": "bad-context-manager", + "codeDescription": { + "href": "https://pyrefly.org/en/docs/error-kinds/#bad-context-manager" + }, + "message": "Cannot use `A` as a context manager\n Object of class `A` has no attribute `__exit__`", + "range": { + "end": {"character": 8, "line": 15}, + "start": {"character": 5, "line": 15} + }, + "severity": 1, + "source": "Pyrefly" + }, + { + "code": "missing-attribute", + "codeDescription": { + "href": "https://pyrefly.org/en/docs/error-kinds/#missing-attribute" + }, + "message": "Object of class `object` has no attribute `nonexistent_method`", + "range": { + "end": {"character": 22, "line": 20}, + "start": {"character": 0, "line": 20} + }, + "severity": 1, + "source": "Pyrefly" + } + ], + "kind": "full" + })), + error: None, + }); + + interaction.shutdown(); +} diff --git a/pyrefly/lib/test/lsp/lsp_interaction/test_files/error_docs_test.py b/pyrefly/lib/test/lsp/lsp_interaction/test_files/error_docs_test.py new file mode 100644 index 000000000..c8f80ddab --- /dev/null +++ b/pyrefly/lib/test/lsp/lsp_interaction/test_files/error_docs_test.py @@ -0,0 +1,21 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +# Test file for error documentation links +# This file contains various error types to test that documentation links are properly included + +# Bad assignment error +x: int = "" + +# Bad context manager error +class A: + pass + +with A(): + pass + +# Missing attribute error +obj = object() +obj.nonexistent_method()