diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 2131c474..a5266f9f 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -42,6 +42,44 @@ Examples: --> +- JavaScript: Add an option to modify when Prettier quotes object properties ([#5934] by [@azz]) + **`--quote-props `** + + `as-needed` **(default)** - Only add quotes around object properties where required. Current behaviour. + `preserve` - Respect the input. This is useful for users of Google's Closure Compiler in Advanced Mode, which treats quoted properties differently. + `consistent` - If _at least one_ property in an object requires quotes, quote all properties - this is like ESLint's [`consistent-as-needed`](https://eslint.org/docs/rules/quote-props) option. + + + ```js + // Input + const headers = { + accept: "application/json", + "content-type": "application/json", + "origin": "prettier.io" + }; + + // Output --quote-props=as-needed + const headers = { + accept: "application/json", + "content-type": "application/json", + origin: "prettier.io" + }; + + // Output --quote-props=consistent + const headers = { + "accept": "application/json", + "content-type": "application/json", + "origin": "prettier.io" + }; + + // Output --quote-props=preserve + const headers = { + accept: "application/json", + "content-type": "application/json", + "origin": "prettier.io" + }; + ``` + - CLI: Honor stdin-filepath when outputting error messages. - Markdown: Do not align table contents if it exceeds the print width and `--prose-wrap never` is set ([#5701] by [@chenshuai2144]) diff --git a/docs/options.md b/docs/options.md index 467cffe8..46450310 100644 --- a/docs/options.md +++ b/docs/options.md @@ -69,6 +69,20 @@ See the [strings rationale](rationale.md#strings) for more information. | ------- | ---------------- | --------------------- | | `false` | `--single-quote` | `singleQuote: ` | +## Quote Props + +Change when properties in objects are quoted. + +Valid options: + +- `"as-needed"` - Only add quotes around object properties where required. +- `"consistent"` - If at least one property in an object requires quotes, quote all properties. +- `"preserve"` - Respect the input use of quotes in object properties. + +| Default | CLI Override | API Override | +| ------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- | +| `"as-needed"` | --quote-props | quoteProps: "" | + ## JSX Quotes Use single quotes instead of double quotes in JSX. diff --git a/src/language-js/options.js b/src/language-js/options.js index a20e269f..daf365f9 100644 --- a/src/language-js/options.js +++ b/src/language-js/options.js @@ -48,6 +48,28 @@ module.exports = { default: false, description: "Use single quotes in JSX." }, + quoteProps: { + since: "1.17.0", + category: CATEGORY_JAVASCRIPT, + type: "choice", + default: "as-needed", + description: "Change when properties in objects are quoted.", + choices: [ + { + value: "as-needed", + description: "Only add quotes around object properties where required." + }, + { + value: "consistent", + description: + "If at least one property in an object requires quotes, quote all properties." + }, + { + value: "preserve", + description: "Respect the input use of quotes in object properties." + } + ] + }, trailingComma: { since: "0.0.0", category: CATEGORY_JAVASCRIPT, diff --git a/src/language-js/printer-estree.js b/src/language-js/printer-estree.js index 7fd02f19..e74a13bd 100644 --- a/src/language-js/printer-estree.js +++ b/src/language-js/printer-estree.js @@ -1,6 +1,7 @@ "use strict"; const assert = require("assert"); + // TODO(azz): anything that imports from main shouldn't be in a `language-*` dir. const comments = require("../main/comments"); const { @@ -45,6 +46,8 @@ const { hasFlowShorthandAnnotationComment } = require("./utils"); +const needsQuoteProps = new WeakMap(); + const { builders: { concat, @@ -3679,31 +3682,41 @@ function printStatementSequence(path, options, print) { function printPropertyKey(path, options, print) { const node = path.getNode(); + const parent = path.getParentNode(); const key = node.key; + if (options.quoteProps === "consistent" && !needsQuoteProps.has(parent)) { + const objectHasStringProp = ( + parent.properties || + parent.body || + parent.members + ).some( + prop => + prop.key && + prop.key.type !== "Identifier" && + !isStringPropSafeToCoerceToIdentifier(prop, options) + ); + needsQuoteProps.set(parent, objectHasStringProp); + } + if ( key.type === "Identifier" && !node.computed && - options.parser === "json" + (options.parser === "json" || + (options.quoteProps === "consistent" && needsQuoteProps.get(parent))) ) { // a -> "a" + const prop = printString(JSON.stringify(key.name), options); return path.call( - keyPath => - comments.printComments( - keyPath, - () => JSON.stringify(key.name), - options - ), + keyPath => comments.printComments(keyPath, () => prop, options), "key" ); } if ( - isStringLiteral(key) && - isIdentifierName(key.value) && - !node.computed && - options.parser !== "json" && - !(options.parser === "typescript" && node.type === "ClassProperty") + isStringPropSafeToCoerceToIdentifier(node, options) && + (options.quoteProps === "as-needed" || + (options.quoteProps === "consistent" && !needsQuoteProps.get(parent))) ) { // 'a' -> a return path.call( @@ -6255,6 +6268,16 @@ function isLiteral(node) { ); } +function isStringPropSafeToCoerceToIdentifier(node, options) { + return ( + isStringLiteral(node.key) && + isIdentifierName(node.key.value) && + !node.computed && + options.parser !== "json" && + !(options.parser === "typescript" && node.type === "ClassProperty") + ); +} + function isNumericLiteral(node) { return ( node.type === "NumericLiteral" || diff --git a/tests/quote_props/__snapshots__/jsfmt.spec.js.snap b/tests/quote_props/__snapshots__/jsfmt.spec.js.snap new file mode 100644 index 00000000..2d21433e --- /dev/null +++ b/tests/quote_props/__snapshots__/jsfmt.spec.js.snap @@ -0,0 +1,379 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`classes.js 1`] = ` +====================================options===================================== +parsers: ["flow", "babel"] +printWidth: 80 +quoteProps: "as-needed" + | printWidth +=====================================input====================================== +class A { + a = "a" +}; + +class B { + 'b' = "b" +}; + +class C { + c1 = "c1" + 'c2' = "c2" +}; + +class D { + d1 = "d1" + 'd-2' = "d2" +}; + +=====================================output===================================== +class A { + a = "a"; +} + +class B { + b = "b"; +} + +class C { + c1 = "c1"; + c2 = "c2"; +} + +class D { + d1 = "d1"; + "d-2" = "d2"; +} + +================================================================================ +`; + +exports[`classes.js 2`] = ` +====================================options===================================== +parsers: ["flow", "babel"] +printWidth: 80 +quoteProps: "preserve" + | printWidth +=====================================input====================================== +class A { + a = "a" +}; + +class B { + 'b' = "b" +}; + +class C { + c1 = "c1" + 'c2' = "c2" +}; + +class D { + d1 = "d1" + 'd-2' = "d2" +}; + +=====================================output===================================== +class A { + a = "a"; +} + +class B { + "b" = "b"; +} + +class C { + c1 = "c1"; + "c2" = "c2"; +} + +class D { + d1 = "d1"; + "d-2" = "d2"; +} + +================================================================================ +`; + +exports[`classes.js 3`] = ` +====================================options===================================== +parsers: ["flow", "babel"] +printWidth: 80 +quoteProps: "consistent" + | printWidth +=====================================input====================================== +class A { + a = "a" +}; + +class B { + 'b' = "b" +}; + +class C { + c1 = "c1" + 'c2' = "c2" +}; + +class D { + d1 = "d1" + 'd-2' = "d2" +}; + +=====================================output===================================== +class A { + a = "a"; +} + +class B { + b = "b"; +} + +class C { + c1 = "c1"; + c2 = "c2"; +} + +class D { + "d1" = "d1"; + "d-2" = "d2"; +} + +================================================================================ +`; + +exports[`classes.js 4`] = ` +====================================options===================================== +parsers: ["flow", "babel"] +printWidth: 80 +quoteProps: "consistent" +singleQuote: true + | printWidth +=====================================input====================================== +class A { + a = "a" +}; + +class B { + 'b' = "b" +}; + +class C { + c1 = "c1" + 'c2' = "c2" +}; + +class D { + d1 = "d1" + 'd-2' = "d2" +}; + +=====================================output===================================== +class A { + a = 'a'; +} + +class B { + b = 'b'; +} + +class C { + c1 = 'c1'; + c2 = 'c2'; +} + +class D { + 'd1' = 'd1'; + 'd-2' = 'd2'; +} + +================================================================================ +`; + +exports[`objects.js 1`] = ` +====================================options===================================== +parsers: ["flow", "babel"] +printWidth: 80 +quoteProps: "as-needed" + | printWidth +=====================================input====================================== +const a = { + a: "a" +}; + +const b = { + 'b': "b" +}; + +const c = { + c1: "c1", + 'c2': "c2" +}; + +const d = { + d1: "d1", + 'd-2': "d2" +}; + +=====================================output===================================== +const a = { + a: "a" +}; + +const b = { + b: "b" +}; + +const c = { + c1: "c1", + c2: "c2" +}; + +const d = { + d1: "d1", + "d-2": "d2" +}; + +================================================================================ +`; + +exports[`objects.js 2`] = ` +====================================options===================================== +parsers: ["flow", "babel"] +printWidth: 80 +quoteProps: "preserve" + | printWidth +=====================================input====================================== +const a = { + a: "a" +}; + +const b = { + 'b': "b" +}; + +const c = { + c1: "c1", + 'c2': "c2" +}; + +const d = { + d1: "d1", + 'd-2': "d2" +}; + +=====================================output===================================== +const a = { + a: "a" +}; + +const b = { + "b": "b" +}; + +const c = { + c1: "c1", + "c2": "c2" +}; + +const d = { + d1: "d1", + "d-2": "d2" +}; + +================================================================================ +`; + +exports[`objects.js 3`] = ` +====================================options===================================== +parsers: ["flow", "babel"] +printWidth: 80 +quoteProps: "consistent" + | printWidth +=====================================input====================================== +const a = { + a: "a" +}; + +const b = { + 'b': "b" +}; + +const c = { + c1: "c1", + 'c2': "c2" +}; + +const d = { + d1: "d1", + 'd-2': "d2" +}; + +=====================================output===================================== +const a = { + a: "a" +}; + +const b = { + b: "b" +}; + +const c = { + c1: "c1", + c2: "c2" +}; + +const d = { + "d1": "d1", + "d-2": "d2" +}; + +================================================================================ +`; + +exports[`objects.js 4`] = ` +====================================options===================================== +parsers: ["flow", "babel"] +printWidth: 80 +quoteProps: "consistent" +singleQuote: true + | printWidth +=====================================input====================================== +const a = { + a: "a" +}; + +const b = { + 'b': "b" +}; + +const c = { + c1: "c1", + 'c2': "c2" +}; + +const d = { + d1: "d1", + 'd-2': "d2" +}; + +=====================================output===================================== +const a = { + a: 'a' +}; + +const b = { + b: 'b' +}; + +const c = { + c1: 'c1', + c2: 'c2' +}; + +const d = { + 'd1': 'd1', + 'd-2': 'd2' +}; + +================================================================================ +`; diff --git a/tests/quote_props/classes.js b/tests/quote_props/classes.js new file mode 100644 index 00000000..11744713 --- /dev/null +++ b/tests/quote_props/classes.js @@ -0,0 +1,17 @@ +class A { + a = "a" +}; + +class B { + 'b' = "b" +}; + +class C { + c1 = "c1" + 'c2' = "c2" +}; + +class D { + d1 = "d1" + 'd-2' = "d2" +}; diff --git a/tests/quote_props/jsfmt.spec.js b/tests/quote_props/jsfmt.spec.js new file mode 100644 index 00000000..0c8f1ad3 --- /dev/null +++ b/tests/quote_props/jsfmt.spec.js @@ -0,0 +1,15 @@ +run_spec(__dirname, ["flow", "babel"], { + quoteProps: "as-needed" +}); + +run_spec(__dirname, ["flow", "babel"], { + quoteProps: "preserve" +}); + +run_spec(__dirname, ["flow", "babel"], { + quoteProps: "consistent" +}); +run_spec(__dirname, ["flow", "babel"], { + quoteProps: "consistent", + singleQuote: true +}); diff --git a/tests/quote_props/objects.js b/tests/quote_props/objects.js new file mode 100644 index 00000000..9a3b9be0 --- /dev/null +++ b/tests/quote_props/objects.js @@ -0,0 +1,17 @@ +const a = { + a: "a" +}; + +const b = { + 'b': "b" +}; + +const c = { + c1: "c1", + 'c2': "c2" +}; + +const d = { + d1: "d1", + 'd-2': "d2" +}; diff --git a/tests/quote_props_typescript/__snapshots__/jsfmt.spec.js.snap b/tests/quote_props_typescript/__snapshots__/jsfmt.spec.js.snap new file mode 100644 index 00000000..0e573f70 --- /dev/null +++ b/tests/quote_props_typescript/__snapshots__/jsfmt.spec.js.snap @@ -0,0 +1,94 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`types.ts 1`] = ` +====================================options===================================== +parsers: ["typescript"] +printWidth: 80 +quoteProps: "as-needed" + | printWidth +=====================================input====================================== +type T = { + 0: string; + 5: number; +} + +type U = { + 0: string; + "5": number; +} + +=====================================output===================================== +type T = { + 0: string; + 5: number; +}; + +type U = { + 0: string; + "5": number; +}; + +================================================================================ +`; + +exports[`types.ts 2`] = ` +====================================options===================================== +parsers: ["typescript"] +printWidth: 80 +quoteProps: "preserve" + | printWidth +=====================================input====================================== +type T = { + 0: string; + 5: number; +} + +type U = { + 0: string; + "5": number; +} + +=====================================output===================================== +type T = { + 0: string; + 5: number; +}; + +type U = { + 0: string; + "5": number; +}; + +================================================================================ +`; + +exports[`types.ts 3`] = ` +====================================options===================================== +parsers: ["typescript"] +printWidth: 80 +quoteProps: "consistent" + | printWidth +=====================================input====================================== +type T = { + 0: string; + 5: number; +} + +type U = { + 0: string; + "5": number; +} + +=====================================output===================================== +type T = { + 0: string; + 5: number; +}; + +type U = { + 0: string; + "5": number; +}; + +================================================================================ +`; diff --git a/tests/quote_props_typescript/jsfmt.spec.js b/tests/quote_props_typescript/jsfmt.spec.js new file mode 100644 index 00000000..b2443baa --- /dev/null +++ b/tests/quote_props_typescript/jsfmt.spec.js @@ -0,0 +1,11 @@ +run_spec(__dirname, ["typescript"], { + quoteProps: "as-needed" +}); + +run_spec(__dirname, ["typescript"], { + quoteProps: "preserve" +}); + +run_spec(__dirname, ["typescript"], { + quoteProps: "consistent" +}); diff --git a/tests/quote_props_typescript/types.ts b/tests/quote_props_typescript/types.ts new file mode 100644 index 00000000..2d37a43d --- /dev/null +++ b/tests/quote_props_typescript/types.ts @@ -0,0 +1,9 @@ +type T = { + 0: string; + 5: number; +} + +type U = { + 0: string; + "5": number; +} diff --git a/tests_integration/__tests__/__snapshots__/early-exit.js.snap b/tests_integration/__tests__/__snapshots__/early-exit.js.snap index 1cb09a26..458a18be 100644 --- a/tests_integration/__tests__/__snapshots__/early-exit.js.snap +++ b/tests_integration/__tests__/__snapshots__/early-exit.js.snap @@ -79,6 +79,9 @@ Format options: --prose-wrap How to wrap prose. Defaults to preserve. + --quote-props + Change when properties in objects are quoted. + Defaults to as-needed. --no-semi Do not print semicolons, except at the beginning of lines which may need them. --single-quote Use single quotes instead of double quotes. Defaults to false. @@ -229,6 +232,9 @@ Format options: --prose-wrap How to wrap prose. Defaults to preserve. + --quote-props + Change when properties in objects are quoted. + Defaults to as-needed. --no-semi Do not print semicolons, except at the beginning of lines which may need them. --single-quote Use single quotes instead of double quotes. Defaults to false. diff --git a/tests_integration/__tests__/__snapshots__/help-options.js.snap b/tests_integration/__tests__/__snapshots__/help-options.js.snap index 9a0ffd45..4f43f9fa 100644 --- a/tests_integration/__tests__/__snapshots__/help-options.js.snap +++ b/tests_integration/__tests__/__snapshots__/help-options.js.snap @@ -421,6 +421,25 @@ Default: preserve exports[`show detailed usage with --help prose-wrap (write) 1`] = `Array []`; +exports[`show detailed usage with --help quote-props (stderr) 1`] = `""`; + +exports[`show detailed usage with --help quote-props (stdout) 1`] = ` +"--quote-props + + Change when properties in objects are quoted. + +Valid options: + + as-needed Only add quotes around object properties where required. + consistent If at least one property in an object requires quotes, quote all properties. + preserve Respect the input use of quotes in object properties. + +Default: as-needed +" +`; + +exports[`show detailed usage with --help quote-props (write) 1`] = `Array []`; + exports[`show detailed usage with --help range-end (stderr) 1`] = `""`; exports[`show detailed usage with --help range-end (stdout) 1`] = ` diff --git a/tests_integration/__tests__/__snapshots__/schema.js.snap b/tests_integration/__tests__/__snapshots__/schema.js.snap index 75a5705d..26b422c7 100644 --- a/tests_integration/__tests__/__snapshots__/schema.js.snap +++ b/tests_integration/__tests__/__snapshots__/schema.js.snap @@ -279,6 +279,30 @@ Multiple values are accepted.", }, ], }, + "quoteProps": Object { + "default": "as-needed", + "description": "Change when properties in objects are quoted.", + "oneOf": Array [ + Object { + "description": "Only add quotes around object properties where required.", + "enum": Array [ + "as-needed", + ], + }, + Object { + "description": "If at least one property in an object requires quotes, quote all properties.", + "enum": Array [ + "consistent", + ], + }, + Object { + "description": "Respect the input use of quotes in object properties.", + "enum": Array [ + "preserve", + ], + }, + ], + }, "rangeEnd": Object { "default": Infinity, "description": "Format code ending at a given character offset (exclusive). diff --git a/tests_integration/__tests__/__snapshots__/support-info.js.snap b/tests_integration/__tests__/__snapshots__/support-info.js.snap index 96189705..0a9974b6 100644 --- a/tests_integration/__tests__/__snapshots__/support-info.js.snap +++ b/tests_integration/__tests__/__snapshots__/support-info.js.snap @@ -622,7 +622,27 @@ exports[`API getSupportInfo() with version 1.16.0 -> undefined 1`] = ` \\"default\\": undefined, \\"type\\": \\"choice\\", }, - \\"pluginSearchDirs\\": Object {" + \\"pluginSearchDirs\\": Object { +@@ -165,10 +169,19 @@ + \\"preserve\\", + ], + \\"default\\": \\"preserve\\", + \\"type\\": \\"choice\\", + }, ++ \\"quoteProps\\": Object { ++ \\"choices\\": Array [ ++ \\"as-needed\\", ++ \\"consistent\\", ++ \\"preserve\\", ++ ], ++ \\"default\\": \\"as-needed\\", ++ \\"type\\": \\"choice\\", ++ }, + \\"rangeEnd\\": Object { + \\"default\\": Infinity, + \\"range\\": Object { + \\"end\\": Infinity, + \\"start\\": 0," `; exports[`CLI --support-info (stderr) 1`] = `""`; @@ -1230,6 +1250,29 @@ exports[`CLI --support-info (stdout) 1`] = ` \\"since\\": \\"1.8.2\\", \\"type\\": \\"choice\\" }, + { + \\"category\\": \\"JavaScript\\", + \\"choices\\": [ + { + \\"description\\": \\"Only add quotes around object properties where required.\\", + \\"value\\": \\"as-needed\\" + }, + { + \\"description\\": \\"If at least one property in an object requires quotes, quote all properties.\\", + \\"value\\": \\"consistent\\" + }, + { + \\"description\\": \\"Respect the input use of quotes in object properties.\\", + \\"value\\": \\"preserve\\" + } + ], + \\"default\\": \\"as-needed\\", + \\"description\\": \\"Change when properties in objects are quoted.\\", + \\"name\\": \\"quoteProps\\", + \\"pluginDefaults\\": {}, + \\"since\\": \\"1.17.0\\", + \\"type\\": \\"choice\\" + }, { \\"category\\": \\"Special\\", \\"default\\": null, diff --git a/website/playground/Playground.js b/website/playground/Playground.js index eb0c8939..631287dc 100644 --- a/website/playground/Playground.js +++ b/website/playground/Playground.js @@ -33,6 +33,7 @@ const ENABLED_OPTIONS = [ "bracketSpacing", "jsxSingleQuote", "jsxBracketSameLine", + "quoteProps", "arrowParens", "trailingComma", "proseWrap",