Skip to content

Commit

Permalink
chore: validate config inside deserialization
Browse files Browse the repository at this point in the history
  • Loading branch information
ematipico committed Feb 28, 2025
1 parent c54b57e commit 3c5ccac
Show file tree
Hide file tree
Showing 11 changed files with 199 additions and 90 deletions.
5 changes: 5 additions & 0 deletions .changeset/great-planes-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": minor
---

Biome now emits a warning diagnostic if the configuration contains an out-of-sync schema URL.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

74 changes: 0 additions & 74 deletions crates/biome_cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use biome_console::{ColorMode, Console};
use biome_service::{App, Workspace, WorkspaceRef};
use commands::search::SearchCommandPayload;
use std::cmp::Ordering;
use std::env;

mod changed;
Expand Down Expand Up @@ -37,50 +36,6 @@ pub use panic::setup_panic_handler;
pub use reporter::{DiagnosticsPayload, Reporter, ReporterVisitor, TraversalSummary};
pub use service::{open_transport, SocketTransport};

#[derive(PartialEq, Eq)]
pub struct Version(String);

impl Version {
pub fn new(version: &str) -> Self {
Version(version.to_string())
}

fn parse_version(&self) -> Vec<u32> {
self.0
.split('.')
.filter_map(|part| part.parse::<u32>().ok())
.collect()
}
}

impl PartialOrd for Version {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}

impl Ord for Version {
fn cmp(&self, other: &Self) -> Ordering {
let self_parts = self.parse_version();
let other_parts = other.parse_version();

for (a, b) in self_parts.iter().zip(other_parts.iter()) {
match a.cmp(b) {
Ordering::Equal => continue,
non_eq => return non_eq,
}
}

self_parts.len().cmp(&other_parts.len())
}
}

impl Display for Version {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::io::Error> {
write!(f, "{}", self.0)
}
}

pub(crate) const VERSION: &str = match option_env!("BIOME_VERSION") {
Some(version) => version,
None => env!("CARGO_PKG_VERSION"),
Expand Down Expand Up @@ -328,32 +283,3 @@ pub(crate) fn run_command(
let command = &mut command;
command.run(session, cli_options)
}

pub fn check_schema_version(
loaded_configuration: &PartialConfiguration,
console: &mut dyn Console,
) {
let schema = &loaded_configuration.schema;
let version_regex =
regex::Regex::new(r"https://biomejs.dev/schemas/([\d.]+)/schema.json").unwrap();
if let Some(schema_string) = schema {
if let Some(captures) = version_regex.captures(schema_string) {
if let Some(config_version_match) = captures.get(1) {
let cli_version = Version::new(VERSION);
let config_version_str = Version::new(config_version_match.as_str());
match config_version_str.cmp(&cli_version) {
Ordering::Less =>
console.log(markup!(<Warn>"The configuration schema version does not match the CLI version.\n"
{KeyValuePair("Expect", markup!({VERSION}))}
{KeyValuePair("Found", markup!({config_version_str}))}</Warn>"\n"
<Info>"If you wish to update the configuration schema, run `biome migrate --write`."</Info>)),
Ordering::Greater => console.log(markup!(<Warn>"The configuration schema version does not match the CLI version.\n"
{KeyValuePair("Expect", markup!({VERSION}))}
{KeyValuePair("Found", markup!({config_version_str}))}</Warn>"\n"
<Info>"If you wish to update the configuration schema, setting the `$schema` option to the expected version."</Info>)),
_ => {}
}
}
}
}
}
8 changes: 2 additions & 6 deletions crates/biome_cli/tests/commands/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3909,19 +3909,15 @@ fn should_report_when_schema_version_mismatch() {
let mut console = BufferConsole::default();
let mut fs = MemoryFileSystem::default();

let biome_json = Path::new("biome.json");
let biome_json = Utf8Path::new("biome.json");
fs.insert(
biome_json.into(),
r#"{
"$schema": "https://biomejs.dev/schemas/0.0.1/schema.json"
}
"#,
);
let result = run_cli(
DynRef::Borrowed(&mut fs),
&mut console,
Args::from([("check")].as_slice()),
);
let (fs, result) = run_cli(fs, &mut console, Args::from([("check")].as_slice()));

assert!(result.is_err(), "run_cli returned {result:?}");
assert_cli_snapshot(SnapshotPayload::new(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,24 @@ check ━━━━━━━━━━━━━━━━━━━━━━━━

# Emitted Messages

```block
biome.json:2:24 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! The configuration schema version does not match the CLI version.
1 │ {
> 2 │ "$schema": "https://biomejs.dev/schemas/1.6.1/schema.json",
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3 │ "assist": {
4 │ "enabled": true
i Expected: 0.0.0
Found: 1.6.1
```

```block
build/file.js format ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,24 @@ value["optimizelyService"] = optimizelyService;

# Emitted Messages

```block
biome.json:2:24 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! The configuration schema version does not match the CLI version.
1 │ {
> 2 │ "$schema": "https://biomejs.dev/schemas/1.6.1/schema.json",
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3 │ "assist": {
4 │ "enabled": true
i Expected: 0.0.0
Found: 1.6.1
```

```block
Formatted 1 file in <TIME>. Fixed 1 file.
```
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
source: crates/biome_cli/tests/snap_test.rs
expression: content
expression: redactor(content)
snapshot_kind: text
---
## `biome.json`
Expand All @@ -25,11 +25,21 @@ internalError/io ━━━━━━━━━━━━━━━━━━━━━
# Emitted Messages

```block
The configuration schema version does not match the CLI version.
Expect: 0.0.0
Found: 0.0.1
biome.json:2:16 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! The configuration schema version does not match the CLI version.
1 │ {
> 2 │ "$schema": "https://biomejs.dev/schemas/0.0.1/schema.json"
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3 │ }
4 │
i Expected: 0.0.0
Found: 0.0.1
If you wish to update the configuration schema, setting the `$schema` option to the expected version.
```

```block
Expand Down
1 change: 1 addition & 0 deletions crates/biome_configuration/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ biome_rowan = { workspace = true, features = ["serde"] }
bpaf = { workspace = true }
camino = { workspace = true }
oxc_resolver = { workspace = true }
regex = { workspace = true }
rustc-hash = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"] }
Expand Down
138 changes: 136 additions & 2 deletions crates/biome_configuration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,13 @@ pub use analyzer::{
RulePlainConfiguration, RuleWithFixOptions, RuleWithOptions, Rules,
};
use biome_console::fmt::{Display, Formatter};
use biome_deserialize::Deserialized;
use biome_console::{markup, KeyValuePair};
use biome_deserialize::{
Deserializable, DeserializableTypes, DeserializableValue, DeserializationContext,
DeserializationDiagnostic, DeserializationVisitor, Deserialized, Text, TextRange,
};
use biome_deserialize_macros::{Deserializable, Merge};
use biome_diagnostics::Severity;
use biome_formatter::{IndentStyle, QuoteStyle};
use bpaf::Bpaf;
use camino::Utf8PathBuf;
Expand All @@ -53,9 +58,13 @@ pub use overrides::{
OverrideLinterConfiguration, OverridePattern, Overrides,
};
use plugins::Plugins;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::fmt::Debug;
use std::num::NonZeroU64;
use std::str::FromStr;
use std::sync::LazyLock;
use vcs::VcsClientKind;

pub const VERSION: &str = match option_env!("BIOME_VERSION") {
Expand All @@ -79,7 +88,7 @@ pub struct Configuration {
#[serde(rename = "$schema")]
#[bpaf(hide, pure(Default::default()))]
#[serde(skip_serializing_if = "Option::is_none")]
pub schema: Option<Box<str>>,
pub schema: Option<Schema>,

/// Indicates whether this configuration file is at the root of a Biome
/// project. By default, this is `true`.
Expand Down Expand Up @@ -324,6 +333,131 @@ impl Configuration {
}
}

#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize, Bpaf, Merge)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields, default, rename_all = "camelCase")]
pub struct Schema(String);

impl FromStr for Schema {
type Err = &'static str;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(s.into())
}
}

impl From<String> for Schema {
fn from(value: String) -> Self {
Self(value)
}
}

impl From<&str> for Schema {
fn from(value: &str) -> Self {
Self(value.into())
}
}

static SCHEMA_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"https://biomejs.dev/schemas/([\d.]+)/schema.json").unwrap());

impl Deserializable for Schema {
fn deserialize(
ctx: &mut impl DeserializationContext,
value: &impl DeserializableValue,
name: &str,
) -> Option<Self> {
struct Visitor;
impl DeserializationVisitor for Visitor {
type Output = Schema;
const EXPECTED_TYPE: DeserializableTypes = DeserializableTypes::STR;

fn visit_str(
self,
ctx: &mut impl DeserializationContext,
value: Text,
range: TextRange,
_name: &str,
) -> Option<Self::Output> {
if let Some(captures) = SCHEMA_REGEX.captures(value.text()) {
if let Some(config_version_match) = captures.get(1) {
let cli_version = Version::new(VERSION);
let config_version_str = Version::new(config_version_match.as_str());
let diagnostic = match config_version_str.cmp(&cli_version) {
Ordering::Less => Some(DeserializationDiagnostic::new(
markup!(<Warn>"The configuration schema version does not match the CLI version"</Warn>),
)),
Ordering::Greater => Some(DeserializationDiagnostic::new(markup!(
<Warn>"The configuration schema version does not match the CLI version."</Warn>
))),
_ => None,
};
if let Some(diagnostic) = diagnostic {
ctx.report(
diagnostic
.with_range(range)
.with_custom_severity(Severity::Warning)
.with_note(markup!(
{KeyValuePair("Expected", markup!({VERSION}))}
{KeyValuePair("Found", markup!({config_version_str}))}
)),
)
}
}
}

Some(Schema(value.text().into()))
}
}

value.deserialize(ctx, Visitor, name)
}
}

#[derive(PartialEq, Eq)]
pub struct Version<'a>(&'a str);

impl<'a> Version<'a> {
pub fn new(version: &'a str) -> Self {
Version(version)
}

fn parse_version(&self) -> Vec<u32> {
self.0
.split('.')
.filter_map(|part| part.parse::<u32>().ok())
.collect()
}
}

impl PartialOrd for Version<'_> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}

impl Ord for Version<'_> {
fn cmp(&self, other: &Self) -> Ordering {
let self_parts = self.parse_version();
let other_parts = other.parse_version();

for (a, b) in self_parts.iter().zip(other_parts.iter()) {
match a.cmp(b) {
Ordering::Equal => continue,
non_eq => return non_eq,
}
}

self_parts.len().cmp(&other_parts.len())
}
}

impl Display for Version<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::io::Error> {
write!(f, "{}", self.0)
}
}

pub type FilesIgnoreUnknownEnabled = Bool<false>;

/// The configuration of the filesystem
Expand Down
Loading

0 comments on commit 3c5ccac

Please sign in to comment.