Skip to content

Commit e3b89ee

Browse files
committed
Add component interface rule
1 parent 87f0ed6 commit e3b89ee

File tree

18 files changed

+1547
-1506
lines changed

18 files changed

+1547
-1506
lines changed

.changeset/thick-days-knock.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@devup/eslint-plugin": patch
3+
---
4+
5+
Add component-interface rule

package.json

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,31 +26,32 @@
2626
"author": "devfive",
2727
"license": "ISC",
2828
"dependencies": {
29-
"@eslint/js": "^9.17",
29+
"@eslint/js": "^9.20",
3030
"@tanstack/eslint-plugin-query": "^5",
31-
"eslint": "^9.17",
32-
"eslint-config-prettier": "^9",
31+
"eslint": "^9.20",
32+
"eslint-config-prettier": "^10",
3333
"eslint-plugin-prettier": "^5",
3434
"eslint-plugin-react": "^7",
3535
"eslint-plugin-react-hooks": "^5",
3636
"eslint-plugin-simple-import-sort": "^12",
3737
"eslint-plugin-unused-imports": "^4",
3838
"prettier": "^3",
39-
"typescript-eslint": "^8.18"
39+
"typescript-eslint": "^8.24"
4040
},
4141
"peerDependencies": {
4242
"eslint": "*"
4343
},
4444
"devDependencies": {
45+
"@changesets/cli": "^2.28.0",
4546
"@types/eslint": "^9.6",
4647
"@types/eslint__js": "^8.42",
47-
"@typescript-eslint/rule-tester": "^8.18",
48-
"@vitest/coverage-v8": "2.1.8",
48+
"@typescript-eslint/rule-tester": "^8.24",
49+
"@typescript-eslint/utils": "^8.24.1",
50+
"@vitest/coverage-v8": "3.0.5",
4951
"eslint-plugin-eslint-plugin": "^6.4.0",
50-
"typescript": "^5.7.2",
51-
"vite": "^6.0.5",
52-
"vite-plugin-dts": "^4.4.0",
53-
"vitest": "^2.1.8",
54-
"@changesets/cli": "^2.27.11"
52+
"typescript": "^5.7.3",
53+
"vite": "^6.1.0",
54+
"vite-plugin-dts": "^4.5.0",
55+
"vitest": "^3.0.5"
5556
}
5657
}

pnpm-lock.yaml

Lines changed: 865 additions & 1025 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/configs/__tests__/__snapshots__/recommended.test.ts.snap

Lines changed: 512 additions & 34 deletions
Large diffs are not rendered by default.

src/configs/recommended.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import simpleImportSort from 'eslint-plugin-simple-import-sort'
99
import unusedImports from 'eslint-plugin-unused-imports'
1010
import tseslint from 'typescript-eslint'
1111

12-
import { appPage, component, cssTs, layout, rscApi } from '../rules'
12+
import { appPage, component, componentInterface, rscApi } from '../rules'
1313

1414
export default [
1515
{
@@ -47,11 +47,10 @@ export default [
4747
'simple-import-sort': simpleImportSort,
4848
'@devup': {
4949
rules: {
50-
layout,
5150
component,
5251
'rsc-api': rscApi,
5352
'app-page': appPage,
54-
'css-ts': cssTs,
53+
'component-interface': componentInterface,
5554
},
5655
},
5756
},

src/rules/__tests__/index.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@ import * as index from '../index'
22
describe('export index', () => {
33
it('export', () => {
44
expect({ ...index }).toEqual({
5-
layout: expect.any(Object),
65
component: expect.any(Object),
6+
componentInterface: expect.any(Object),
77
rscApi: expect.any(Object),
88
appPage: expect.any(Object),
9-
cssTs: expect.any(Object),
109
})
1110
})
1211
})
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# component interface
2+
3+
## Description
4+
5+
컴포넌트의 props 가 빈 object 일 때 interface 를 추가합니다.
6+
7+
8+
## Example
9+
10+
```tsx
11+
export function Hello({}) {
12+
return <div>...</div>;
13+
}
14+
```
15+
16+
```tsx
17+
interface HelloProps {}
18+
export function Hello({}: HelloProps) {
19+
return <div>...</div>;
20+
}
21+
```
22+
23+
```
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { RuleTester } from '@typescript-eslint/rule-tester'
2+
3+
import { componentInterface } from '../index'
4+
5+
describe('component rule', () => {
6+
const ruleTester = new RuleTester({
7+
languageOptions: {
8+
ecmaVersion: 'latest',
9+
parserOptions: {
10+
ecmaFeatures: {
11+
jsx: true,
12+
},
13+
},
14+
},
15+
})
16+
ruleTester.run('component rule', componentInterface, {
17+
valid: [
18+
{
19+
code: 'interface HelloProps{}\ninterface Hello1Props{}\nexport function Hello({}:HelloProps){return <></>}',
20+
filename: 'src/components/hello.tsx',
21+
},
22+
],
23+
invalid: [
24+
{
25+
code: 'function Hello({}){return <></>}',
26+
output:
27+
'interface HelloProps{}\nfunction Hello({}:HelloProps){return <></>}',
28+
filename: 'src/components/hello.tsx',
29+
errors: [
30+
{
31+
messageId:
32+
'componentPropsShouldHaveTypeAnnotationWhenEmptyObjectPattern',
33+
},
34+
],
35+
},
36+
{
37+
code: 'export function Hello({}){return <></>}',
38+
output:
39+
'interface HelloProps{}\nexport function Hello({}:HelloProps){return <></>}',
40+
filename: 'src/components/hello.tsx',
41+
errors: [
42+
{
43+
messageId:
44+
'componentPropsShouldHaveTypeAnnotationWhenEmptyObjectPattern',
45+
},
46+
],
47+
},
48+
{
49+
code: 'export default function Hello({}){return <></>}',
50+
output:
51+
'interface HelloProps{}\nexport default function Hello({}:HelloProps){return <></>}',
52+
filename: 'src/components/hello.tsx',
53+
errors: [
54+
{
55+
messageId:
56+
'componentPropsShouldHaveTypeAnnotationWhenEmptyObjectPattern',
57+
},
58+
],
59+
},
60+
],
61+
})
62+
})
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { ESLintUtils } from '@typescript-eslint/utils'
2+
3+
const createRule = ESLintUtils.RuleCreator(
4+
(name) =>
5+
`https://github.com/dev-five-git/devup/tree/main/packages/eslint-plugin/src/rules/${name}`,
6+
)
7+
8+
export const componentInterface = createRule({
9+
name: 'component-interface',
10+
defaultOptions: [],
11+
meta: {
12+
schema: [],
13+
messages: {
14+
componentPropsShouldHaveTypeAnnotationWhenEmptyObjectPattern:
15+
'컴포넌트의 `props`가 비어있고 타입이 없을 경우 반드시 타입을 명시해야 합니다.',
16+
},
17+
type: 'problem',
18+
fixable: 'code',
19+
docs: {
20+
description:
21+
'required type annotation for component props when empty object pattern',
22+
},
23+
},
24+
create(context) {
25+
const filename = context.physicalFilename
26+
27+
if (!filename.endsWith('.tsx')) return {}
28+
29+
return {
30+
FunctionDeclaration(node) {
31+
const funcName = node.id?.name
32+
33+
if (
34+
funcName &&
35+
node.params.length === 1 &&
36+
node.params[0].type === 'ObjectPattern' &&
37+
!node.params[0].typeAnnotation &&
38+
node.params[0].properties.length === 0 &&
39+
(node.parent.type === 'Program' ||
40+
node.parent.type === 'ExportNamedDeclaration' ||
41+
node.parent.type === 'ExportDefaultDeclaration') &&
42+
/^[A-Z]/.test(funcName)
43+
) {
44+
context.report({
45+
node,
46+
messageId:
47+
'componentPropsShouldHaveTypeAnnotationWhenEmptyObjectPattern',
48+
fix(fixer) {
49+
return [
50+
fixer.insertTextAfter(node.params[0], `:${funcName}Props`),
51+
// 1줄 전
52+
fixer.insertTextBefore(
53+
node.parent ?? node,
54+
`interface ${funcName}Props{}\n`,
55+
),
56+
]
57+
},
58+
})
59+
}
60+
},
61+
}
62+
},
63+
})

src/rules/component/__tests__/index.test.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -214,18 +214,6 @@ describe('component rule', () => {
214214
},
215215
],
216216
},
217-
{
218-
code: 'export function Hello({}){return <></>}',
219-
output:
220-
'interface HelloProps{}\nexport function Hello({}:HelloProps){return <></>}',
221-
filename: 'src/components/hello.tsx',
222-
errors: [
223-
{
224-
messageId:
225-
'componentPropsShouldHaveTypeAnnotationWhenEmptyObjectPattern',
226-
},
227-
],
228-
},
229217
],
230218
})
231219
})

0 commit comments

Comments
 (0)