Skip to content

Commit

Permalink
feat: new ts/quote-props rule (#275)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
so1ve and antfu authored Feb 5, 2024
1 parent a4296ea commit a56b798
Show file tree
Hide file tree
Showing 19 changed files with 225 additions and 15 deletions.
1 change: 1 addition & 0 deletions packages/eslint-plugin-ts/configs/disable-legacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const config: Linter.FlatConfig = {
'@typescript-eslint/no-extra-semi': 0,
'@typescript-eslint/object-curly-spacing': 0,
'@typescript-eslint/padding-line-between-statements': 0,
'@typescript-eslint/quote-props': 0,
'@typescript-eslint/quotes': 0,
'@typescript-eslint/semi': 0,
'@typescript-eslint/space-before-blocks': 0,
Expand Down
11 changes: 11 additions & 0 deletions packages/eslint-plugin-ts/dts/rule-options.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { RuleOptions as NoExtraParensRuleOptions } from '../rules/no-extra-
import type { RuleOptions as NoExtraSemiRuleOptions } from '../rules/no-extra-semi/types'
import type { RuleOptions as ObjectCurlySpacingRuleOptions } from '../rules/object-curly-spacing/types'
import type { RuleOptions as PaddingLineBetweenStatementsRuleOptions } from '../rules/padding-line-between-statements/types'
import type { RuleOptions as QuotePropsRuleOptions } from '../rules/quote-props/types'
import type { RuleOptions as QuotesRuleOptions } from '../rules/quotes/types'
import type { RuleOptions as SemiRuleOptions } from '../rules/semi/types'
import type { RuleOptions as SpaceBeforeBlocksRuleOptions } from '../rules/space-before-blocks/types'
Expand Down Expand Up @@ -103,6 +104,11 @@ export interface RuleOptions {
* @see https://eslint.style/rules/ts/padding-line-between-statements
*/
'@stylistic/ts/padding-line-between-statements': PaddingLineBetweenStatementsRuleOptions
/**
* Require quotes around object literal, type literal, interfaces and enums property names
* @see https://eslint.style/rules/ts/quote-props
*/
'@stylistic/ts/quote-props': QuotePropsRuleOptions
/**
* Enforce the consistent use of either backticks, double, or single quotes
* @see https://eslint.style/rules/ts/quotes
Expand Down Expand Up @@ -216,6 +222,11 @@ export interface UnprefixedRuleOptions {
* @see https://eslint.style/rules/ts/padding-line-between-statements
*/
'padding-line-between-statements': PaddingLineBetweenStatementsRuleOptions
/**
* Require quotes around object literal, type literal, interfaces and enums property names
* @see https://eslint.style/rules/ts/quote-props
*/
'quote-props': QuotePropsRuleOptions
/**
* Enforce the consistent use of either backticks, double, or single quotes
* @see https://eslint.style/rules/ts/quotes
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin-ts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"./rules/no-extra-semi": "./dist/no-extra-semi.js",
"./rules/object-curly-spacing": "./dist/object-curly-spacing.js",
"./rules/padding-line-between-statements": "./dist/padding-line-between-statements.js",
"./rules/quote-props": "./dist/quote-props.js",
"./rules/quotes": "./dist/quotes.js",
"./rules/semi": "./dist/semi.js",
"./rules/space-before-blocks": "./dist/space-before-blocks.js",
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin-ts/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
| [`@stylistic/ts/no-extra-semi`](./rules/no-extra-semi) | Disallow unnecessary semicolons || |
| [`@stylistic/ts/object-curly-spacing`](./rules/object-curly-spacing) | Enforce consistent spacing inside braces || |
| [`@stylistic/ts/padding-line-between-statements`](./rules/padding-line-between-statements) | Require or disallow padding lines between statements || |
| [`@stylistic/ts/quote-props`](./rules/quote-props) | Require quotes around object literal, type literal, interfaces and enums property names || |
| [`@stylistic/ts/quotes`](./rules/quotes) | Enforce the consistent use of either backticks, double, or single quotes || |
| [`@stylistic/ts/semi`](./rules/semi) | Require or disallow semicolons instead of ASI || |
| [`@stylistic/ts/space-before-blocks`](./rules/space-before-blocks) | Enforce consistent spacing before blocks || |
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin-ts/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import noExtraParens from './no-extra-parens/no-extra-parens'
import noExtraSemi from './no-extra-semi/no-extra-semi'
import objectCurlySpacing from './object-curly-spacing/object-curly-spacing'
import paddingLineBetweenStatements from './padding-line-between-statements/padding-line-between-statements'
import quoteProps from './quote-props/quote-props'
import quotes from './quotes/quotes'
import semi from './semi/semi'
import spaceBeforeBlocks from './space-before-blocks/space-before-blocks'
Expand All @@ -41,6 +42,7 @@ export default {
'no-extra-semi': noExtraSemi,
'object-curly-spacing': objectCurlySpacing,
'padding-line-between-statements': paddingLineBetweenStatements,
'quote-props': quoteProps,
'quotes': quotes,
'semi': semi,
'space-before-blocks': spaceBeforeBlocks,
Expand Down
6 changes: 6 additions & 0 deletions packages/eslint-plugin-ts/rules/quote-props/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
description: 'Require quotes around object literal, type literal, interfaces and enums property names.'
---

This rule extends the base [`quote-props`](/rules/js/quote-props) rule.
It adds support for TypeScript's type literals, interfaces and enums.
78 changes: 78 additions & 0 deletions packages/eslint-plugin-ts/rules/quote-props/quote-props.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { RuleTester } from '@typescript-eslint/rule-tester'

import rule from './quote-props'

const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
})

ruleTester.run('quote-props', rule, {
valid: [
'type x = { "a": 1, b(): void, "c"(): void }',
'interface x { "a": 1, b(): void, "c"(): void }',
'enum x { "a" }',

{ code: 'type x = { a: 1, "b-b": 1 }', options: ['as-needed'] },
{ code: 'interface x { a: 1, "b-b": 1 }', options: ['as-needed'] },
{ code: 'enum x { a = 1, "b-b" = 2 }', options: ['as-needed'] },

{ code: 'type x = { "a": 1, "b-b": 1 }', options: ['consistent-as-needed'] },
{ code: 'interface x { "a": 1, "b-b": 1 }', options: ['consistent-as-needed'] },
{ code: 'enum x { "a" = 1, "b-b" = 2 }', options: ['consistent-as-needed'] },
],
invalid: [
{
code: 'type x = { a: 1 }',
output: 'type x = { "a": 1 }',
errors: [{ messageId: 'unquotedPropertyFound' }],
},
{
code: 'interface x { a: 1 }',
output: 'interface x { "a": 1 }',
errors: [{ messageId: 'unquotedPropertyFound' }],
},
{
code: 'enum x { a = 1 }',
output: 'enum x { "a" = 1 }',
errors: [{ messageId: 'unquotedPropertyFound' }],
},

{
code: 'type x = { "a": 1 }',
output: 'type x = { a: 1 }',
options: ['as-needed'],
errors: [{ messageId: 'unnecessarilyQuotedProperty' }],
},
{
code: 'interface x { "a": 1, "b-b": 1 }',
output: 'interface x { a: 1, "b-b": 1 }',
options: ['as-needed'],
errors: [{ messageId: 'unnecessarilyQuotedProperty' }],
},
{
code: 'enum x { "a" = 1, "b-b" = 2 }',
output: 'enum x { a = 1, "b-b" = 2 }',
options: ['as-needed'],
errors: [{ messageId: 'unnecessarilyQuotedProperty' }],
},

{
code: 'type x = { a: 1, "b-b": 1 }',
output: 'type x = { "a": 1, "b-b": 1 }',
options: ['consistent-as-needed'],
errors: [{ messageId: 'inconsistentlyQuotedProperty' }],
},
{
code: 'interface x { a: 1, "b-b": 1 }',
output: 'interface x { "a": 1, "b-b": 1 }',
options: ['consistent-as-needed'],
errors: [{ messageId: 'inconsistentlyQuotedProperty' }],
},
{
code: 'enum x { a = 1, "b-b" = 2 }',
output: 'enum x { "a" = 1, "b-b" = 2 }',
options: ['consistent-as-needed'],
errors: [{ messageId: 'inconsistentlyQuotedProperty' }],
},
],
})
79 changes: 79 additions & 0 deletions packages/eslint-plugin-ts/rules/quote-props/quote-props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { AST_NODE_TYPES } from '@typescript-eslint/utils'

import { createRule } from '../../utils'
import { getESLintCoreRule } from '../../utils/getESLintCoreRule'
import type { MessageIds, RuleOptions } from './types'

const baseRule = getESLintCoreRule('quote-props')

export default createRule<RuleOptions, MessageIds>({
name: 'quote-props',
meta: {
...baseRule.meta,
docs: {
description: 'Require quotes around object literal, type literal, interfaces and enums property names',
extendsBaseRule: true,
},
},
defaultOptions: ['always'],
create(context) {
const rules = baseRule.create(context)

return {
...rules,
TSPropertySignature(node) {
return rules.Property!({
...node,
type: AST_NODE_TYPES.Property,
shorthand: false,
method: false,
kind: 'init',
value: null as any,
})
},
TSMethodSignature(node) {
return rules.Property!({
...node,
type: AST_NODE_TYPES.Property,
shorthand: false,
method: true,
kind: 'init',
value: null as any,
})
},
TSEnumMember(node) {
return rules.Property!({
...node,
type: AST_NODE_TYPES.Property,
key: node.id as any,
optional: false,
shorthand: false,
method: false,
kind: 'init',
value: null as any,
})
},
TSTypeLiteral(node) {
return rules.ObjectExpression!({
...node,
type: AST_NODE_TYPES.ObjectExpression,
properties: node.members as any,
})
},
TSInterfaceBody(node) {
return rules.ObjectExpression!({
...node,
type: AST_NODE_TYPES.ObjectExpression,
properties: node.body as any,
})
},
TSEnumDeclaration(node) {
return rules.ObjectExpression!({
...node,
type: AST_NODE_TYPES.ObjectExpression,
properties: node.members.map(member => ({ ...member, key: member.id })) as any,
})
},
}
},
})
18 changes: 18 additions & 0 deletions packages/eslint-plugin-ts/rules/quote-props/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* GENERATED, DO NOT EDIT DIRECTLY */

export type Schema0 =
| []
| ['always' | 'as-needed' | 'consistent' | 'consistent-as-needed']
| []
| ['always' | 'as-needed' | 'consistent' | 'consistent-as-needed']
| [
'always' | 'as-needed' | 'consistent' | 'consistent-as-needed',
{
keywords?: boolean
unnecessary?: boolean
numbers?: boolean
},
]

export type RuleOptions = Schema0
export type MessageIds = 'requireQuotesDueToReservedWord' | 'inconsistentlyQuotedProperty' | 'unnecessarilyQuotedProperty' | 'unquotedReservedProperty' | 'unquotedNumericProperty' | 'unquotedPropertyFound' | 'redundantQuoting'
13 changes: 13 additions & 0 deletions packages/metadata/src/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1411,6 +1411,19 @@ export const packages: Readonly<PackageInfo[]> = Object.freeze([
}
}
},
{
"name": "quote-props",
"ruleId": "@stylistic/ts/quote-props",
"originalId": "@typescript-eslint/quote-props",
"entry": "packages/eslint-plugin-ts/rules/quote-props/quote-props.ts",
"docsEntry": "packages/eslint-plugin-ts/rules/quote-props/README.md",
"meta": {
"fixable": "code",
"docs": {
"description": "Require quotes around object literal, type literal, interfaces and enums property names"
}
}
},
{
"name": "quotes",
"ruleId": "@stylistic/ts/quotes",
Expand Down
2 changes: 1 addition & 1 deletion tests/configs/fixtures/output/all/javascript.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ let a, b, c, d, foo;

if (a ||
b ||
c || d
c || d
) {

foo();
Expand Down
14 changes: 7 additions & 7 deletions tests/configs/fixtures/output/all/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ export {};

// Define a TypeScript interface
interface Person< T = string, K = number > {
name: string;
age: number;
"name": string;
"age": number;
}

type Tuple = [ foo: number, bar: String | Boolean];
Expand Down Expand Up @@ -37,9 +37,9 @@ function identity< T > (arg: T): T {

// TypeScript enum
enum EnumFoo {
aaa,
bbb,
ccc
"aaa",
"bbb",
"ccc"
}

// Use the generic function with type inference
Expand All @@ -48,8 +48,8 @@ log(result);

// Use optional properties in an interface
interface Car {
make: string;
model?: string;
"make": string;
"model"?: string;
}

// Create objects using the interface
Expand Down
2 changes: 1 addition & 1 deletion tests/configs/fixtures/output/default/jsx.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ export function HelloWorld({
{ (silent) ? '.' : '!'}
</div>
)
}
}
2 changes: 1 addition & 1 deletion tests/configs/fixtures/output/default/tsx.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ export function jsx2() {
</p>
</a>
)
}
}
2 changes: 1 addition & 1 deletion tests/configs/fixtures/output/default/vue-ts.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ let counter = ref<number | string>(0)
const incrementCounter = () => {
counter.value++
}
</script>
</script>
2 changes: 1 addition & 1 deletion tests/configs/fixtures/output/default/vue.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ let counter = ref(0)
const incrementCounter = () => {
counter.value++
}
</script>
</script>
2 changes: 1 addition & 1 deletion tests/configs/fixtures/output/tab-quotes-semi/jsx.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ export function HelloWorld({
{ (silent) ? "." : "!"}
</div>
);
}
}
2 changes: 1 addition & 1 deletion tests/configs/fixtures/output/tab-quotes-semi/vue-ts.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ let counter = ref<number | string>(0);
const incrementCounter = () => {
counter.value++;
};
</script>
</script>
2 changes: 1 addition & 1 deletion tests/configs/fixtures/output/tab-quotes-semi/vue.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ let counter = ref(0);
const incrementCounter = () => {
counter.value++;
};
</script>
</script>

0 comments on commit a56b798

Please sign in to comment.