Add an option to modify when Prettier quotes object properties (#5934)

* Implement quote-props=preserve

* Implement quote-props=consistent

* Add tests

* Enable quoteProps in playground

* Add documentation

* Cache objectHasStringProp calculations

* Fixup changelog
master
Lucas Azzola 2019-03-06 20:33:08 +11:00 committed by GitHub
parent 3a9006659e
commit f526c47b1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 745 additions and 13 deletions

View File

@ -42,6 +42,44 @@ Examples:
-->
- JavaScript: Add an option to modify when Prettier quotes object properties ([#5934] by [@azz])
**`--quote-props <as-needed|preserve|consistent>`**
`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.
<!-- prettier-ignore -->
```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])

View File

@ -69,6 +69,20 @@ See the [strings rationale](rationale.md#strings) for more information.
| ------- | ---------------- | --------------------- |
| `false` | `--single-quote` | `singleQuote: <bool>` |
## 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"` | <code>--quote-props <as-needed&#124;consistent&#124;preserve></code> | <code>quoteProps: "<as-needed&#124;consistent&#124;preserve>"</code> |
## JSX Quotes
Use single quotes instead of double quotes in JSX.

View File

@ -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,

View File

@ -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" ||

View File

@ -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'
};
================================================================================
`;

View File

@ -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"
};

View File

@ -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
});

View File

@ -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"
};

View File

@ -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;
};
================================================================================
`;

View File

@ -0,0 +1,11 @@
run_spec(__dirname, ["typescript"], {
quoteProps: "as-needed"
});
run_spec(__dirname, ["typescript"], {
quoteProps: "preserve"
});
run_spec(__dirname, ["typescript"], {
quoteProps: "consistent"
});

View File

@ -0,0 +1,9 @@
type T = {
0: string;
5: number;
}
type U = {
0: string;
"5": number;
}

View File

@ -79,6 +79,9 @@ Format options:
--prose-wrap <always|never|preserve>
How to wrap prose.
Defaults to preserve.
--quote-props <as-needed|consistent|preserve>
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 <always|never|preserve>
How to wrap prose.
Defaults to preserve.
--quote-props <as-needed|consistent|preserve>
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.

View File

@ -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 <as-needed|consistent|preserve>
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`] = `

View File

@ -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).

View File

@ -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,

View File

@ -33,6 +33,7 @@ const ENABLED_OPTIONS = [
"bracketSpacing",
"jsxSingleQuote",
"jsxBracketSameLine",
"quoteProps",
"arrowParens",
"trailingComma",
"proseWrap",