Fix non‑BMP code point handling in quick‑fixes and markers (#20526)

Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
Dan Parizher 2025-09-24 04:08:00 -04:00 committed by GitHub
parent 09f570af92
commit 3e1e02e9b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 70 additions and 33 deletions

View file

@ -19,7 +19,7 @@ There are multiple versions for the different wasm-pack targets. See [here](http
This example uses the wasm-pack web target and is known to work with Vite. This example uses the wasm-pack web target and is known to work with Vite.
```ts ```ts
import init, { Workspace, type Diagnostic } from '@astral-sh/ruff-wasm-web'; import init, { Workspace, type Diagnostic, PositionEncoding } from '@astral-sh/ruff-wasm-web';
const exampleDocument = `print('hello'); print("world")` const exampleDocument = `print('hello'); print("world")`
@ -42,7 +42,7 @@ const workspace = new Workspace({
'F' 'F'
], ],
}, },
}); }, PositionEncoding.UTF16);
// Will contain 1 diagnostic code for E702: Multiple statements on one line // Will contain 1 diagnostic code for E702: Multiple statements on one line
const diagnostics: Diagnostic[] = workspace.check(exampleDocument); const diagnostics: Diagnostic[] = workspace.check(exampleDocument);

View file

@ -19,7 +19,7 @@ use ruff_python_formatter::{PyFormatContext, QuoteStyle, format_module_ast, pret
use ruff_python_index::Indexer; use ruff_python_index::Indexer;
use ruff_python_parser::{Mode, ParseOptions, Parsed, parse, parse_unchecked}; use ruff_python_parser::{Mode, ParseOptions, Parsed, parse, parse_unchecked};
use ruff_python_trivia::CommentRanges; use ruff_python_trivia::CommentRanges;
use ruff_source_file::{LineColumn, OneIndexed}; use ruff_source_file::{OneIndexed, PositionEncoding as SourcePositionEncoding, SourceLocation};
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use ruff_workspace::Settings; use ruff_workspace::Settings;
use ruff_workspace::configuration::Configuration; use ruff_workspace::configuration::Configuration;
@ -117,6 +117,7 @@ pub fn run() {
#[wasm_bindgen] #[wasm_bindgen]
pub struct Workspace { pub struct Workspace {
settings: Settings, settings: Settings,
position_encoding: SourcePositionEncoding,
} }
#[wasm_bindgen] #[wasm_bindgen]
@ -126,7 +127,7 @@ impl Workspace {
} }
#[wasm_bindgen(constructor)] #[wasm_bindgen(constructor)]
pub fn new(options: JsValue) -> Result<Workspace, Error> { pub fn new(options: JsValue, position_encoding: PositionEncoding) -> Result<Workspace, Error> {
let options: Options = serde_wasm_bindgen::from_value(options).map_err(into_error)?; let options: Options = serde_wasm_bindgen::from_value(options).map_err(into_error)?;
let configuration = let configuration =
Configuration::from_options(options, Some(Path::new(".")), Path::new(".")) Configuration::from_options(options, Some(Path::new(".")), Path::new("."))
@ -135,7 +136,10 @@ impl Workspace {
.into_settings(Path::new(".")) .into_settings(Path::new("."))
.map_err(into_error)?; .map_err(into_error)?;
Ok(Workspace { settings }) Ok(Workspace {
settings,
position_encoding: position_encoding.into(),
})
} }
#[wasm_bindgen(js_name = defaultSettings)] #[wasm_bindgen(js_name = defaultSettings)]
@ -228,14 +232,16 @@ impl Workspace {
let messages: Vec<ExpandedMessage> = diagnostics let messages: Vec<ExpandedMessage> = diagnostics
.into_iter() .into_iter()
.map(|msg| ExpandedMessage { .map(|msg| {
let range = msg.range().unwrap_or_default();
ExpandedMessage {
code: msg.secondary_code_or_id().to_string(), code: msg.secondary_code_or_id().to_string(),
message: msg.body().to_string(), message: msg.body().to_string(),
start_location: source_code start_location: source_code
.line_column(msg.range().unwrap_or_default().start()) .source_location(range.start(), self.position_encoding)
.into(), .into(),
end_location: source_code end_location: source_code
.line_column(msg.range().unwrap_or_default().end()) .source_location(range.end(), self.position_encoding)
.into(), .into(),
fix: msg.fix().map(|fix| ExpandedFix { fix: msg.fix().map(|fix| ExpandedFix {
message: msg.first_help_text().map(ToString::to_string), message: msg.first_help_text().map(ToString::to_string),
@ -243,12 +249,17 @@ impl Workspace {
.edits() .edits()
.iter() .iter()
.map(|edit| ExpandedEdit { .map(|edit| ExpandedEdit {
location: source_code.line_column(edit.start()).into(), location: source_code
end_location: source_code.line_column(edit.end()).into(), .source_location(edit.start(), self.position_encoding)
.into(),
end_location: source_code
.source_location(edit.end(), self.position_encoding)
.into(),
content: edit.content().map(ToString::to_string), content: edit.content().map(ToString::to_string),
}) })
.collect(), .collect(),
}), }),
}
}) })
.collect(); .collect();
@ -331,14 +342,37 @@ impl<'a> ParsedModule<'a> {
#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)]
pub struct Location { pub struct Location {
pub row: OneIndexed, pub row: OneIndexed,
/// The character offset from the start of the line.
///
/// The semantic of the offset depends on the [`PositionEncoding`] used when creating
/// the [`Workspace`].
pub column: OneIndexed, pub column: OneIndexed,
} }
impl From<LineColumn> for Location { impl From<SourceLocation> for Location {
fn from(value: LineColumn) -> Self { fn from(value: SourceLocation) -> Self {
Self { Self {
row: value.line, row: value.line,
column: value.column, column: value.character_offset,
}
}
}
#[derive(Default, Copy, Clone)]
#[wasm_bindgen]
pub enum PositionEncoding {
#[default]
Utf8,
Utf16,
Utf32,
}
impl From<PositionEncoding> for SourcePositionEncoding {
fn from(value: PositionEncoding) -> Self {
match value {
PositionEncoding::Utf8 => Self::Utf8,
PositionEncoding::Utf16 => Self::Utf16,
PositionEncoding::Utf32 => Self::Utf32,
} }
} }
} }

View file

@ -4,12 +4,15 @@ use wasm_bindgen_test::wasm_bindgen_test;
use ruff_linter::registry::Rule; use ruff_linter::registry::Rule;
use ruff_source_file::OneIndexed; use ruff_source_file::OneIndexed;
use ruff_wasm::{ExpandedMessage, Location, Workspace}; use ruff_wasm::{ExpandedMessage, Location, PositionEncoding, Workspace};
macro_rules! check { macro_rules! check {
($source:expr, $config:expr, $expected:expr) => {{ ($source:expr, $config:expr, $expected:expr) => {{
let config = js_sys::JSON::parse($config).unwrap(); let config = js_sys::JSON::parse($config).unwrap();
match Workspace::new(config).unwrap().check($source) { match Workspace::new(config, PositionEncoding::Utf8)
.unwrap()
.check($source)
{
Ok(output) => { Ok(output) => {
let result: Vec<ExpandedMessage> = serde_wasm_bindgen::from_value(output).unwrap(); let result: Vec<ExpandedMessage> = serde_wasm_bindgen::from_value(output).unwrap();
assert_eq!(result, $expected); assert_eq!(result, $expected);

View file

@ -6,7 +6,7 @@ import {
useState, useState,
} from "react"; } from "react";
import { Panel, PanelGroup } from "react-resizable-panels"; import { Panel, PanelGroup } from "react-resizable-panels";
import { Diagnostic, Workspace } from "ruff_wasm"; import { Diagnostic, Workspace, PositionEncoding } from "ruff_wasm";
import { import {
ErrorMessage, ErrorMessage,
Theme, Theme,
@ -173,7 +173,7 @@ export default function Editor({
try { try {
const config = JSON.parse(settingsSource); const config = JSON.parse(settingsSource);
const workspace = new Workspace(config); const workspace = new Workspace(config, PositionEncoding.Utf16);
const diagnostics = workspace.check(pythonSource); const diagnostics = workspace.check(pythonSource);
let secondary: SecondaryPanelResult = null; let secondary: SecondaryPanelResult = null;