Skip to main content

dprint_core/plugins/
plugin_handler.rs

1use serde::Deserialize;
2use serde::Serialize;
3
4#[cfg(feature = "async_runtime")]
5use crate::async_runtime::FutureExt;
6#[cfg(feature = "async_runtime")]
7use crate::async_runtime::LocalBoxFuture;
8
9use crate::configuration::ConfigKeyMap;
10use crate::configuration::ConfigKeyValue;
11use crate::configuration::ConfigurationDiagnostic;
12use crate::configuration::GlobalConfiguration;
13use crate::plugins::PluginInfo;
14
15use super::FileMatchingInfo;
16
17pub trait CancellationToken: Send + Sync + std::fmt::Debug {
18  fn is_cancelled(&self) -> bool;
19  #[cfg(feature = "async_runtime")]
20  fn wait_cancellation(&self) -> LocalBoxFuture<'static, ()>;
21}
22
23#[cfg(feature = "async_runtime")]
24impl CancellationToken for tokio_util::sync::CancellationToken {
25  fn is_cancelled(&self) -> bool {
26    self.is_cancelled()
27  }
28
29  fn wait_cancellation(&self) -> LocalBoxFuture<'static, ()> {
30    let token = self.clone();
31    async move { token.cancelled().await }.boxed_local()
32  }
33}
34
35/// A cancellation token that always says it's not cancelled.
36#[derive(Debug)]
37pub struct NullCancellationToken;
38
39impl CancellationToken for NullCancellationToken {
40  fn is_cancelled(&self) -> bool {
41    false
42  }
43
44  #[cfg(feature = "async_runtime")]
45  fn wait_cancellation(&self) -> LocalBoxFuture<'static, ()> {
46    // never resolves
47    Box::pin(std::future::pending())
48  }
49}
50
51pub type FormatRange = Option<std::ops::Range<usize>>;
52
53/// An error returned by formatting operations.
54///
55/// This can hold any error, allowing plugins to return their own error types,
56/// while still implementing [`std::error::Error`] so that consumers can convert
57/// it into their own error type.
58#[derive(Debug, thiserror::Error)]
59#[error(transparent)]
60pub struct FormatError(Box<dyn std::error::Error + Send + Sync + 'static>);
61
62impl FormatError {
63  /// Creates a new error from anything that can be turned into a boxed error
64  /// (for example a `String`, `&str`, or any [`std::error::Error`]).
65  pub fn new(error: impl Into<Box<dyn std::error::Error + Send + Sync + 'static>>) -> Self {
66    FormatError(error.into())
67  }
68
69  /// Attempts to downcast the underlying error to a concrete type
70  /// (ex. to check for a [`CriticalFormatError`]).
71  pub fn downcast_ref<E: std::error::Error + 'static>(&self) -> Option<&E> {
72    self.0.downcast_ref::<E>()
73  }
74}
75
76/// Formats an error and its source chain into a single string,
77/// joining each level with `: ` (equivalent to formatting an
78/// `anyhow` error with the alternate `{:#}` specifier).
79pub fn error_to_string(err: &(dyn std::error::Error + 'static)) -> String {
80  // cap the depth so a pathological error with a cyclic `source()` chain
81  // can't make this loop forever
82  const MAX_DEPTH: usize = 100;
83  let mut result = err.to_string();
84  let mut source = err.source();
85  for _ in 0..MAX_DEPTH {
86    let Some(err) = source else { break };
87    result.push_str(": ");
88    result.push_str(&err.to_string());
89    source = err.source();
90  }
91  result
92}
93
94macro_rules! impl_format_error_from {
95  ($($t:ty),* $(,)?) => {
96    $(
97      impl From<$t> for FormatError {
98        fn from(error: $t) -> Self {
99          FormatError(error.into())
100        }
101      }
102    )*
103  };
104}
105
106impl_format_error_from!(
107  String,
108  &str,
109  Box<dyn std::error::Error + Send + Sync + 'static>,
110  std::io::Error,
111  std::str::Utf8Error,
112  std::string::FromUtf8Error,
113  CriticalFormatError,
114);
115
116#[cfg(test)]
117mod tests {
118  use super::FormatError;
119
120  #[test]
121  fn should_convert_utf8_error_to_format_error() {
122    let bytes = [u8::MAX];
123    let utf8_error = std::str::from_utf8(&bytes).unwrap_err();
124    let format_error: FormatError = utf8_error.into();
125
126    assert!(format_error.downcast_ref::<std::str::Utf8Error>().is_some());
127  }
128}
129
130#[cfg(feature = "serde_json")]
131impl_format_error_from!(serde_json::Error);
132
133#[cfg(feature = "async_runtime")]
134impl_format_error_from!(tokio::task::JoinError, tokio::sync::oneshot::error::RecvError);
135
136/// A formatting error where the plugin cannot recover.
137///
138/// Return one of these to signal to the dprint CLI that
139/// it should recreate the plugin.
140#[derive(Debug, thiserror::Error)]
141#[error(transparent)]
142pub struct CriticalFormatError(pub FormatError);
143
144#[derive(Debug, Serialize, Deserialize)]
145#[serde(rename_all = "camelCase")]
146pub struct CheckConfigUpdatesMessage {
147  /// dprint versions < 0.47 won't have this set
148  #[serde(default)]
149  pub old_version: Option<String>,
150  pub config: ConfigKeyMap,
151}
152
153#[cfg(feature = "process")]
154#[derive(Debug)]
155pub struct HostFormatRequest {
156  pub file_path: std::path::PathBuf,
157  pub file_bytes: Vec<u8>,
158  /// Range to format.
159  pub range: FormatRange,
160  pub override_config: ConfigKeyMap,
161  pub token: std::sync::Arc<dyn CancellationToken>,
162}
163
164#[cfg(feature = "wasm")]
165#[derive(Debug)]
166pub struct SyncHostFormatRequest<'a> {
167  pub file_path: &'a std::path::Path,
168  pub file_bytes: &'a [u8],
169  /// Range to format.
170  pub range: FormatRange,
171  pub override_config: &'a ConfigKeyMap,
172}
173
174/// `Ok(Some(text))` - Changes due to the format.
175/// `Ok(None)` - No changes.
176/// `Err(err)` - Error formatting. Use a `CriticalError` to signal that the plugin can't recover.
177pub type FormatResult = Result<Option<Vec<u8>>, FormatError>;
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct RawFormatConfig {
181  pub plugin: ConfigKeyMap,
182  pub global: GlobalConfiguration,
183}
184
185/// A unique configuration id used for formatting.
186#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
187pub struct FormatConfigId(u32);
188
189impl std::fmt::Display for FormatConfigId {
190  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191    write!(f, "${}", self.0)
192  }
193}
194
195impl FormatConfigId {
196  pub fn from_raw(raw: u32) -> FormatConfigId {
197    FormatConfigId(raw)
198  }
199
200  pub fn uninitialized() -> FormatConfigId {
201    FormatConfigId(0)
202  }
203
204  pub fn as_raw(&self) -> u32 {
205    self.0
206  }
207}
208
209#[cfg(feature = "process")]
210pub struct FormatRequest<TConfiguration> {
211  pub file_path: std::path::PathBuf,
212  pub file_bytes: Vec<u8>,
213  pub config_id: FormatConfigId,
214  pub config: std::sync::Arc<TConfiguration>,
215  /// Range to format.
216  pub range: FormatRange,
217  pub token: std::sync::Arc<dyn CancellationToken>,
218}
219
220#[cfg(feature = "wasm")]
221pub struct SyncFormatRequest<'a, TConfiguration> {
222  pub file_path: &'a std::path::Path,
223  pub file_bytes: Vec<u8>,
224  pub config_id: FormatConfigId,
225  pub config: &'a TConfiguration,
226  /// Range to format.
227  pub range: FormatRange,
228  pub token: &'a dyn CancellationToken,
229}
230
231#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
232#[serde(untagged)]
233pub enum ConfigChangePathItem {
234  /// String property name.
235  String(String),
236  /// Number if an index in an array.
237  Number(usize),
238}
239
240impl From<String> for ConfigChangePathItem {
241  fn from(value: String) -> Self {
242    Self::String(value)
243  }
244}
245
246impl From<usize> for ConfigChangePathItem {
247  fn from(value: usize) -> Self {
248    Self::Number(value)
249  }
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize)]
253#[serde(rename_all = "camelCase")]
254pub struct ConfigChange {
255  /// The path to make modifications at.
256  pub path: Vec<ConfigChangePathItem>,
257  #[serde(flatten)]
258  pub kind: ConfigChangeKind,
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
262#[serde(tag = "kind", content = "value")]
263pub enum ConfigChangeKind {
264  /// Adds an object property or array element.
265  Add(ConfigKeyValue),
266  /// Overwrites an existing value at the provided path.
267  Set(ConfigKeyValue),
268  /// Removes the value at the path.
269  Remove,
270}
271
272#[derive(Clone, Serialize)]
273#[serde(rename_all = "camelCase")]
274pub struct PluginResolveConfigurationResult<T>
275where
276  T: Clone + Serialize,
277{
278  /// Information about what files are matched for the provided configuration.
279  pub file_matching: FileMatchingInfo,
280
281  /// The configuration diagnostics.
282  pub diagnostics: Vec<ConfigurationDiagnostic>,
283
284  /// The configuration derived from the unresolved configuration
285  /// that can be used to format a file.
286  pub config: T,
287}
288
289/// Trait for implementing a process plugin.
290#[cfg(feature = "process")]
291#[crate::async_runtime::async_trait(?Send)]
292pub trait AsyncPluginHandler: 'static {
293  type Configuration: Serialize + Clone + Send + Sync;
294
295  /// Gets the plugin's plugin info.
296  fn plugin_info(&self) -> PluginInfo;
297  /// Gets the plugin's license text.
298  fn license_text(&self) -> String;
299  /// Resolves configuration based on the provided config map and global configuration.
300  async fn resolve_config(&self, config: ConfigKeyMap, global_config: GlobalConfiguration) -> PluginResolveConfigurationResult<Self::Configuration>;
301  /// Updates the config key map. This will be called after the CLI has upgraded the
302  /// plugin in `dprint config update`.
303  async fn check_config_updates(&self, _message: CheckConfigUpdatesMessage) -> Result<Vec<ConfigChange>, FormatError> {
304    Ok(Vec::new())
305  }
306  /// Formats the provided file text based on the provided file path and configuration.
307  async fn format(
308    &self,
309    request: FormatRequest<Self::Configuration>,
310    format_with_host: impl FnMut(HostFormatRequest) -> LocalBoxFuture<'static, FormatResult> + 'static,
311  ) -> FormatResult;
312}
313
314/// Trait for implementing a Wasm plugin.
315#[cfg(feature = "wasm")]
316pub trait SyncPluginHandler<TConfiguration: Clone + serde::Serialize> {
317  /// Resolves configuration based on the provided config map and global configuration.
318  fn resolve_config(&mut self, config: ConfigKeyMap, global_config: &GlobalConfiguration) -> PluginResolveConfigurationResult<TConfiguration>;
319  /// Gets the plugin's plugin info.
320  fn plugin_info(&mut self) -> PluginInfo;
321  /// Gets the plugin's license text.
322  fn license_text(&mut self) -> String;
323  /// Updates the config key map. This will be called after the CLI has upgraded the
324  /// plugin in `dprint config update`.
325  fn check_config_updates(&self, message: CheckConfigUpdatesMessage) -> Result<Vec<ConfigChange>, FormatError>;
326  /// Formats the provided file text based on the provided file path and configuration.
327  fn format(&mut self, request: SyncFormatRequest<TConfiguration>, format_with_host: impl FnMut(SyncHostFormatRequest) -> FormatResult) -> FormatResult;
328}