From b2ec1ad7ac2a8842a847202b39f11d9b2b410a38 Mon Sep 17 00:00:00 2001 From: Victor <78874691+victor-teles@users.noreply.github.com> Date: Tue, 3 Oct 2023 18:16:01 -0300 Subject: [PATCH] fix(formatter/member-chain): regex formatting (#474) --- .../src/utils/member_chain/simple_argument.rs | 32 +- .../member-chain/static_member_regex.js | 24 ++ .../member-chain/static_member_regex.js.snap | 87 +++++ .../js/method-chain/issue-11298.js.snap | 41 --- .../js/method-chain/issue-4125.js.snap | 347 ------------------ 5 files changed, 139 insertions(+), 392 deletions(-) create mode 100644 crates/biome_js_formatter/tests/specs/js/module/expression/member-chain/static_member_regex.js create mode 100644 crates/biome_js_formatter/tests/specs/js/module/expression/member-chain/static_member_regex.js.snap delete mode 100644 crates/biome_js_formatter/tests/specs/prettier/js/method-chain/issue-11298.js.snap delete mode 100644 crates/biome_js_formatter/tests/specs/prettier/js/method-chain/issue-4125.js.snap diff --git a/crates/biome_js_formatter/src/utils/member_chain/simple_argument.rs b/crates/biome_js_formatter/src/utils/member_chain/simple_argument.rs index ef042e34b151..e98a31a1192f 100644 --- a/crates/biome_js_formatter/src/utils/member_chain/simple_argument.rs +++ b/crates/biome_js_formatter/src/utils/member_chain/simple_argument.rs @@ -1,8 +1,8 @@ use crate::utils::is_call_like_expression; use biome_js_syntax::{ - AnyJsArrayElement, AnyJsCallArgument, AnyJsExpression, AnyJsName, AnyJsObjectMember, - AnyJsObjectMemberName, AnyJsTemplateElement, JsSpread, JsStaticMemberExpressionFields, - JsTemplateExpression, JsUnaryOperator, + AnyJsArrayElement, AnyJsCallArgument, AnyJsExpression, AnyJsLiteralExpression, AnyJsName, + AnyJsObjectMember, AnyJsObjectMemberName, AnyJsTemplateElement, JsSpread, + JsStaticMemberExpressionFields, JsTemplateExpression, JsUnaryOperator, }; use biome_rowan::{AstSeparatedList, SyntaxResult}; @@ -14,7 +14,9 @@ use biome_rowan::{AstSeparatedList, SyntaxResult}; /// /// Criteria are different: /// - *complex*: if the chain of simple arguments exceeds the depth 2 or higher +/// - *complex*: if the argument is a [JsRegexLiteralExpression] with len() greater than 5 /// - *simple*: the argument is a literal +/// - *simple*: the argument is a [JsRegexLiteralExpression] with len() less than 5 /// - *simple*: the argument is a [JsThisExpression] /// - *simple*: the argument is a [JsIdentifierExpression] /// - *simple*: the argument is a [JsSuperExpression] @@ -57,6 +59,7 @@ impl SimpleArgument { if depth >= 2 { return false; } + if self.is_simple_literal() { return true; } @@ -73,6 +76,7 @@ impl SimpleArgument { .unwrap_or(false) || self.is_simple_call_like_expression(depth).unwrap_or(false) || self.is_simple_object_expression(depth) + || self.is_simple_regex_expression() } fn is_simple_call_like_expression(&self, depth: u8) -> SyntaxResult { @@ -161,6 +165,19 @@ impl SimpleArgument { } } + fn is_simple_regex_expression(&self) -> bool { + if let SimpleArgument::Expression(AnyJsExpression::AnyJsLiteralExpression( + AnyJsLiteralExpression::JsRegexLiteralExpression(regex), + )) = self + { + if let Ok((pattern, _)) = regex.decompose() { + return pattern.text().len() <= 5; + } + } + + false + } + fn is_simple_array_expression(&self, depth: u8) -> bool { if let SimpleArgument::Expression(AnyJsExpression::JsArrayExpression(array_expression)) = self @@ -193,13 +210,20 @@ impl SimpleArgument { return true; } + if let SimpleArgument::Expression(AnyJsExpression::AnyJsLiteralExpression( + AnyJsLiteralExpression::JsRegexLiteralExpression(_), + )) = self + { + return false; + } + matches!( self, SimpleArgument::Expression( AnyJsExpression::AnyJsLiteralExpression(_) | AnyJsExpression::JsThisExpression(_) | AnyJsExpression::JsIdentifierExpression(_) - | AnyJsExpression::JsSuperExpression(_), + | AnyJsExpression::JsSuperExpression(_) ) ) } diff --git a/crates/biome_js_formatter/tests/specs/js/module/expression/member-chain/static_member_regex.js b/crates/biome_js_formatter/tests/specs/js/module/expression/member-chain/static_member_regex.js new file mode 100644 index 000000000000..cd2b8bc1c3ec --- /dev/null +++ b/crates/biome_js_formatter/tests/specs/js/module/expression/member-chain/static_member_regex.js @@ -0,0 +1,24 @@ +// 5-len regex +z.string().min(2).max(200).regex(/[a-z]/gm, { + message: 'Only English alphabet symbols and hyphen allowed', +}) + +// <5-len regex +z.string().min(2).max(200).regex(/\w+/gm, { + message: 'Only English alphabet symbols and hyphen allowed', +}) + + +// >5-len regex +z.string().min(2).max(200).regex(/^[a-zA-Z\-]+$/gm, { + message: 'Only English alphabet symbols and hyphen allowed', +}) + +const a = { + locales: z.record( + localeKeySchema, + z.string().min(2).regex(/^[a-zA-Z\-]+$/gm, { + message: 'Only English alphabet symbols and hyphen allowed', + }) + ), +} diff --git a/crates/biome_js_formatter/tests/specs/js/module/expression/member-chain/static_member_regex.js.snap b/crates/biome_js_formatter/tests/specs/js/module/expression/member-chain/static_member_regex.js.snap new file mode 100644 index 000000000000..ba5ab783a3f0 --- /dev/null +++ b/crates/biome_js_formatter/tests/specs/js/module/expression/member-chain/static_member_regex.js.snap @@ -0,0 +1,87 @@ +--- +source: crates/biome_formatter_test/src/snapshot_builder.rs +info: js/module/expression/member-chain/static_member-regex.js +--- + +# Input + +```js +// 5-len regex +z.string().min(2).max(200).regex(/[a-z]/gm, { + message: 'Only English alphabet symbols and hyphen allowed', +}) + +// <5-len regex +z.string().min(2).max(200).regex(/\w+/gm, { + message: 'Only English alphabet symbols and hyphen allowed', +}) + + +// >5-len regex +z.string().min(2).max(200).regex(/^[a-zA-Z\-]+$/gm, { + message: 'Only English alphabet symbols and hyphen allowed', +}) + +const a = { + locales: z.record( + localeKeySchema, + z.string().min(2).regex(/^[a-zA-Z\-]+$/gm, { + message: 'Only English alphabet symbols and hyphen allowed', + }) + ), +} + +``` + + +============================= + +# Outputs + +## Output 1 + +----- +Indent style: Tab +Indent width: 2 +Line width: 80 +Quote style: Double Quotes +JSX quote style: Double Quotes +Quote properties: As needed +Trailing comma: All +Semicolons: Always +Arrow parentheses: Always +----- + +```js +// 5-len regex +z.string().min(2).max(200).regex(/[a-z]/gm, { + message: "Only English alphabet symbols and hyphen allowed", +}); + +// <5-len regex +z.string().min(2).max(200).regex(/\w+/gm, { + message: "Only English alphabet symbols and hyphen allowed", +}); + +// >5-len regex +z.string() + .min(2) + .max(200) + .regex(/^[a-zA-Z\-]+$/gm, { + message: "Only English alphabet symbols and hyphen allowed", + }); + +const a = { + locales: z.record( + localeKeySchema, + z + .string() + .min(2) + .regex(/^[a-zA-Z\-]+$/gm, { + message: "Only English alphabet symbols and hyphen allowed", + }), + ), +}; +``` + + diff --git a/crates/biome_js_formatter/tests/specs/prettier/js/method-chain/issue-11298.js.snap b/crates/biome_js_formatter/tests/specs/prettier/js/method-chain/issue-11298.js.snap deleted file mode 100644 index 84f8c7c9dadf..000000000000 --- a/crates/biome_js_formatter/tests/specs/prettier/js/method-chain/issue-11298.js.snap +++ /dev/null @@ -1,41 +0,0 @@ ---- -source: crates/biome_formatter_test/src/snapshot_builder.rs -info: js/method-chain/issue-11298.js ---- - -# Input - -```js -foo1(/𠮟𠮟𠮟/).foo2(bar).foo3(baz); - -foo1(/叱叱叱/).foo2(bar).foo3(baz); - -``` - - -# Prettier differences - -```diff ---- Prettier -+++ Biome -@@ -1,7 +1,3 @@ --foo1(/𠮟𠮟𠮟/) -- .foo2(bar) -- .foo3(baz); -+foo1(/𠮟𠮟𠮟/).foo2(bar).foo3(baz); - --foo1(/叱叱叱/) -- .foo2(bar) -- .foo3(baz); -+foo1(/叱叱叱/).foo2(bar).foo3(baz); -``` - -# Output - -```js -foo1(/𠮟𠮟𠮟/).foo2(bar).foo3(baz); - -foo1(/叱叱叱/).foo2(bar).foo3(baz); -``` - - diff --git a/crates/biome_js_formatter/tests/specs/prettier/js/method-chain/issue-4125.js.snap b/crates/biome_js_formatter/tests/specs/prettier/js/method-chain/issue-4125.js.snap deleted file mode 100644 index 45fc3df3c340..000000000000 --- a/crates/biome_js_formatter/tests/specs/prettier/js/method-chain/issue-4125.js.snap +++ /dev/null @@ -1,347 +0,0 @@ ---- -source: crates/biome_formatter_test/src/snapshot_builder.rs -info: js/method-chain/issue-4125.js ---- - -# Input - -```js -// examples from https://github.com/prettier/prettier/issues/4125 - -const sha256 = (data) => crypto.createHash("sha256").update(data).digest("hex"); - -req.checkBody('id').isInt().optional(); -req.checkBody('name').notEmpty().optional(); - -const x = moment().add(1, 'day').valueOf() - -// should stay on one line: -const y = obj.foo(1).foo(2).foo(3); -const z = obj.foo(-1).foo(import('2')).foo(!x).check(/[A-Z]/); - -// better on multiple lines: -somePromise.then(format).then((val)=>doSomething(val)).catch((err)=>handleError(err)) - -// you can still force multi-line chaining with a comment: -const sha256_2 = (data) => - crypto // breakme - .createHash("sha256") - .update(data) - .digest("hex"); - -// examples from https://github.com/prettier/prettier/pull/4765 - -if ($(el).attr("href").includes("/wiki/")) { -} - -if ($(el).attr("href").includes("/wiki/")) { - if ($(el).attr("xyz").includes("/whatever/")) { - if ($(el).attr("hello").includes("/world/")) { - } - } -} - -const parseNumbers = s => s.split('').map(Number).sort() - -function palindrome(a, b) { - return a.slice().reverse().join(',') === b.slice().sort().join(','); -} - -// examples from https://github.com/prettier/prettier/issues/1565 - -d3.select("body").selectAll("p").data([1, 2, 3]).enter().style("color", "white"); - -Object.keys(props).filter(key => key in own === false).reduce((a, key) => { - a[key] = props[key]; - return a; -}, {}) - -point().x(4).y(3).z(6).plot(); - -assert.equal(this.$().text().trim(), '1000'); - -something().then(() => doSomethingElse()).then(result => dontForgetThisAsWell(result)) - -db.branch( - db.table('users').filter({ email }).count(), - db.table('users').filter({ email: 'a@b.com' }).count(), - db.table('users').insert({ email }), - db.table('users').filter({ email }), -) - -sandbox.stub(config, 'get').withArgs('env').returns('dev') - -const date = moment.utc(userInput).hour(0).minute(0).second(0) - -fetchUser(id) - .then(fetchAccountForUser) - .catch(handleFetchError) - -fetchUser(id) // - .then(fetchAccountForUser) - .catch(handleFetchError) - -// examples from https://github.com/prettier/prettier/issues/3107 - -function HelloWorld() { - window.FooClient.setVars({ - locale: getFooLocale({ page }), - authorizationToken: data.token, - }).initVerify('foo_container'); - - fejax.ajax({ - url: '/verification/', - dataType: 'json', - }).then( - (data) => { - this.setState({ isLoading: false }); - this.initWidget(data); - }, - (data) => { - this.logImpression('foo_fetch_error', data); - Flash.error(I18n.t('offline_identity.foo_issue')); - }, - ); -} - -action$.ofType(ActionTypes.SEARCHED_USERS) - .map(action => action.payload.query) - .filter(q => !!q) - .switchMap(q => - Observable.timer(800) // debounce - .takeUntil(action$.ofType(ActionTypes.CLEARED_SEARCH_RESULTS)) - .mergeMap(() => - Observable.merge( - Observable.of(replace(`?q=${q}`)), - ajax - .getJSON(`https://api.github.com/search/users?q=${q}`) - .map(res => res.items) - .map(receiveUsers) - ) - ) - ); - -window.FooClient - .setVars({ - locale: getFooLocale({ page }), - authorizationToken: data.token, - }) - .initVerify('foo_container'); - -it('gets triggered by mouseenter', () => { - const wrapper = shallow(); - wrapper.dive().find(Button).prop(); -}); - -const a1 = x.a(true).b(null).c(123) -const a2 = x.d('').e(``).f(g) -const a3 = x.d('').e(`${123}`).f(g) -const a4 = x.h(i.j).k(l()).m([n, o]) -class X { - y() { - const j = x.a(this).b(super.cde()).f(/g/).h(new i()).j(); - } -} - -// should break when call expressions get complex -x.a().b([c, [d, [e]]]).f() -x.a().b(c(d(e()))).f() -x.a().b(`${c(d())}`).f() - -xyz.a().b().c(a(a(b(c(d().p).p).p).p)) - -var l = base - .replace(/^\w*:\/\//, '') - .replace(/\/$/, '') - .split('/').length - - -``` - - -# Prettier differences - -```diff ---- Prettier -+++ Biome -@@ -161,7 +161,4 @@ - .b() - .c(a(a(b(c(d().p).p).p).p)); - --var l = base -- .replace(/^\w*:\/\//, "") -- .replace(/\/$/, "") -- .split("/").length; -+var l = base.replace(/^\w*:\/\//, "").replace(/\/$/, "").split("/").length; -``` - -# Output - -```js -// examples from https://github.com/prettier/prettier/issues/4125 - -const sha256 = (data) => crypto.createHash("sha256").update(data).digest("hex"); - -req.checkBody("id").isInt().optional(); -req.checkBody("name").notEmpty().optional(); - -const x = moment().add(1, "day").valueOf(); - -// should stay on one line: -const y = obj.foo(1).foo(2).foo(3); -const z = obj.foo(-1).foo(import("2")).foo(!x).check(/[A-Z]/); - -// better on multiple lines: -somePromise - .then(format) - .then((val) => doSomething(val)) - .catch((err) => handleError(err)); - -// you can still force multi-line chaining with a comment: -const sha256_2 = (data) => - crypto // breakme - .createHash("sha256") - .update(data) - .digest("hex"); - -// examples from https://github.com/prettier/prettier/pull/4765 - -if ($(el).attr("href").includes("/wiki/")) { -} - -if ($(el).attr("href").includes("/wiki/")) { - if ($(el).attr("xyz").includes("/whatever/")) { - if ($(el).attr("hello").includes("/world/")) { - } - } -} - -const parseNumbers = (s) => s.split("").map(Number).sort(); - -function palindrome(a, b) { - return a.slice().reverse().join(",") === b.slice().sort().join(","); -} - -// examples from https://github.com/prettier/prettier/issues/1565 - -d3.select("body") - .selectAll("p") - .data([1, 2, 3]) - .enter() - .style("color", "white"); - -Object.keys(props) - .filter((key) => key in own === false) - .reduce((a, key) => { - a[key] = props[key]; - return a; - }, {}); - -point().x(4).y(3).z(6).plot(); - -assert.equal(this.$().text().trim(), "1000"); - -something() - .then(() => doSomethingElse()) - .then((result) => dontForgetThisAsWell(result)); - -db.branch( - db.table("users").filter({ email }).count(), - db.table("users").filter({ email: "a@b.com" }).count(), - db.table("users").insert({ email }), - db.table("users").filter({ email }), -); - -sandbox.stub(config, "get").withArgs("env").returns("dev"); - -const date = moment.utc(userInput).hour(0).minute(0).second(0); - -fetchUser(id).then(fetchAccountForUser).catch(handleFetchError); - -fetchUser(id) // - .then(fetchAccountForUser) - .catch(handleFetchError); - -// examples from https://github.com/prettier/prettier/issues/3107 - -function HelloWorld() { - window.FooClient.setVars({ - locale: getFooLocale({ page }), - authorizationToken: data.token, - }).initVerify("foo_container"); - - fejax - .ajax({ - url: "/verification/", - dataType: "json", - }) - .then( - (data) => { - this.setState({ isLoading: false }); - this.initWidget(data); - }, - (data) => { - this.logImpression("foo_fetch_error", data); - Flash.error(I18n.t("offline_identity.foo_issue")); - }, - ); -} - -action$ - .ofType(ActionTypes.SEARCHED_USERS) - .map((action) => action.payload.query) - .filter((q) => !!q) - .switchMap((q) => - Observable.timer(800) // debounce - .takeUntil(action$.ofType(ActionTypes.CLEARED_SEARCH_RESULTS)) - .mergeMap(() => - Observable.merge( - Observable.of(replace(`?q=${q}`)), - ajax - .getJSON(`https://api.github.com/search/users?q=${q}`) - .map((res) => res.items) - .map(receiveUsers), - ), - ), - ); - -window.FooClient.setVars({ - locale: getFooLocale({ page }), - authorizationToken: data.token, -}).initVerify("foo_container"); - -it("gets triggered by mouseenter", () => { - const wrapper = shallow(); - wrapper.dive().find(Button).prop(); -}); - -const a1 = x.a(true).b(null).c(123); -const a2 = x.d("").e(``).f(g); -const a3 = x.d("").e(`${123}`).f(g); -const a4 = x.h(i.j).k(l()).m([n, o]); -class X { - y() { - const j = x.a(this).b(super.cde()).f(/g/).h(new i()).j(); - } -} - -// should break when call expressions get complex -x.a() - .b([c, [d, [e]]]) - .f(); -x.a() - .b(c(d(e()))) - .f(); -x.a() - .b(`${c(d())}`) - .f(); - -xyz - .a() - .b() - .c(a(a(b(c(d().p).p).p).p)); - -var l = base.replace(/^\w*:\/\//, "").replace(/\/$/, "").split("/").length; -``` - -