mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(formatter_json): reject unary expressions other than signed numbers (#27728)
Should reject invalid JSON like `{ "foo": -true }`.
This commit is contained in:
1 parent
22cd4cdaee
commit
99077ef835
3 files changed
+40
-12
No files matched your search
@@ -1,4 +1,4 @@
|
||||
use oxc_ast::ast::Expression;
|
||||
use oxc_ast::ast::{Expression, UnaryExpression};
|
||||
use oxc_formatter_core::{Format, Formatter, builders::FormatWith, builders::text, write};
|
||||
use oxc_span::{GetSpan, Span};
|
||||
|
||||
@@ -98,7 +98,7 @@ impl<'a> Format<'a, JsonFormatContext<'a>> for FmtJsonValue<'a, '_> {
|
||||
// Prettier's `json` parser routes through the JS estree printer,
|
||||
// which keeps both `+` and `-` operators while recursing into the argument
|
||||
// so the inner number is normalized (`-1.0e+2` → `-1.0e2`).
|
||||
Expression::UnaryExpression(unary) => {
|
||||
Expression::UnaryExpression(unary) if is_signed_number(unary) => {
|
||||
write!(f, unary.operator.as_str());
|
||||
FmtJsonValue { expression: &unary.argument }.fmt(f);
|
||||
}
|
||||
@@ -124,9 +124,25 @@ impl<'a> Format<'a, JsonFormatContext<'a>> for FmtJsonValue<'a, '_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` for `+` / `-` before a number, `Infinity` or `NaN`, the only unary forms Prettier accepts.
|
||||
/// Others are invalid, and printing them would change or break the source (`typeof x` -> `typeofx`, `-(-1)` -> `--1`).
|
||||
fn is_signed_number(unary: &UnaryExpression<'_>) -> bool {
|
||||
unary.operator.is_arithmetic()
|
||||
&& match &unary.argument {
|
||||
Expression::NumericLiteral(_) => true,
|
||||
Expression::Identifier(ident) => matches!(ident.name.as_str(), "Infinity" | "NaN"),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// `Format` adapter for JSON-invalid AST nodes: records a diagnostic via
|
||||
/// [`JsonFormatContext::report_invalid_json`] and emits the node's source verbatim
|
||||
/// via [`FormatSuppressedNode`] so IR construction can still complete.
|
||||
///
|
||||
/// NOTE: Validation happens at format time, so a node under `prettier-ignore` / `oxfmt-ignore` is never checked
|
||||
/// (e.g. `// prettier-ignore\n-true` is accepted, Prettier rejects it at parse time).
|
||||
/// Accepted since it is rare and the source is kept as-is;
|
||||
/// validating at parse time would need another AST walk.
|
||||
pub struct FormatInvalidJson(pub Span);
|
||||
|
||||
impl<'a> Format<'a, JsonFormatContext<'a>> for FormatInvalidJson {
|
||||
|
||||
@@ -22,7 +22,7 @@ use oxc_syntax::operator::UnaryOperator;
|
||||
use crate::context::JsonFormatContext;
|
||||
|
||||
use super::{
|
||||
FormatInvalidJson, JsonFormatter, format_with, literal::FmtJsonString,
|
||||
FormatInvalidJson, JsonFormatter, format_with, is_signed_number, literal::FmtJsonString,
|
||||
number_string_round_trips, write_quoted_str,
|
||||
};
|
||||
|
||||
@@ -43,17 +43,13 @@ impl<'a> Format<'a, JsonFormatContext<'a>> for FmtJsonStringifyValue<'a, '_> {
|
||||
Expression::StringLiteral(lit) => write_string(lit, f),
|
||||
Expression::ArrayExpression(array) => write_array(array, f),
|
||||
Expression::ObjectExpression(object) => write_object(object, f),
|
||||
Expression::UnaryExpression(unary) => match unary.operator {
|
||||
Expression::UnaryExpression(unary) if is_signed_number(unary) => {
|
||||
// `JSON.parse` rejects a leading `+`, so Prettier drops it.
|
||||
UnaryOperator::UnaryPlus => {
|
||||
Self { expression: &unary.argument }.fmt(f);
|
||||
}
|
||||
UnaryOperator::UnaryNegation => {
|
||||
if unary.operator == UnaryOperator::UnaryNegation {
|
||||
write!(f, "-");
|
||||
Self { expression: &unary.argument }.fmt(f);
|
||||
}
|
||||
_ => write!(f, FormatInvalidJson(unary.span)),
|
||||
},
|
||||
Self { expression: &unary.argument }.fmt(f);
|
||||
}
|
||||
// JSON5 `Infinity` / `NaN`, JSON6 `undefined`; anything else is invalid
|
||||
// (Prettier rejects other identifiers at parse time, we report at format time).
|
||||
Expression::Identifier(ident)
|
||||
|
||||
+17
-1
@@ -1,7 +1,7 @@
|
||||
use std::path::Path;
|
||||
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_formatter_json::{JsonFormatOptions, format, parse_for_format};
|
||||
use oxc_formatter_json::{JsonFormatOptions, JsonVariant, format, parse_for_format};
|
||||
use oxc_formatter_tests::{FixtureFormatter, OptionSet, build_fixture_snapshot};
|
||||
|
||||
mod options;
|
||||
@@ -57,3 +57,19 @@ fn test_file(path: &Path) {
|
||||
|
||||
// Include auto-generated test functions from build.rs
|
||||
include!(concat!(env!("OUT_DIR"), "/generated_tests.rs"));
|
||||
|
||||
// ---
|
||||
|
||||
/// JSON-invalid syntax the lenient JS parse accepts must surface as `Err` (the oxfmt fallback trigger).
|
||||
#[test]
|
||||
fn invalid_json_is_err() {
|
||||
// `json` / `jsonc` / `json5` share one printer, `json-stringify` has its own
|
||||
for variant in [JsonVariant::Json, JsonVariant::JsonStringify] {
|
||||
let options = JsonFormatOptions { variant, ..Default::default() };
|
||||
// Unary other than `+` / `-` before a number, `Infinity` or `NaN`
|
||||
for source in ["!true", "typeof Infinity", "-true", "-undefined", "-(-1)", "+ +1"] {
|
||||
let allocator = Allocator::default();
|
||||
assert!(format(&allocator, source, options).is_err(), "{variant:?}: {source:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user