-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[FEAT] eslint을 커스텀해서 팀 코드 컨벤션 적용 #441
Changes from 31 commits
d97adbf
2388b6b
9646244
10e8e46
11deca3
33ee3b1
f65f67f
6c323fd
27df64a
0c863c5
5af175f
fd55fa4
6f20ef9
6d79a86
e325fa2
b634d81
fd72c23
eab383d
33de947
cde54b8
daf5d56
7f1aef2
00f30c9
0c3a053
08cbd4a
a5146a7
7b6b7ac
7edad06
3ff4b0b
0f05799
4b24efb
f082295
8893b6f
5654b19
fa8964e
9ccd5d8
bcbd0e3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
const path = require('path'); | ||
const fs = require('fs'); | ||
|
||
module.exports = { | ||
meta: { | ||
type: 'problem', | ||
docs: { | ||
description: | ||
'components 폴더 안에는 컴포넌트 폴더와 해당 폴더명과 일치하는 tsx 파일이 있어야 한다.', | ||
recommended: true, | ||
}, | ||
message: { | ||
noMatch: `components 폴더 안에 "{{dirName}}" 폴더와 이에 해당하는 "{{expectedFileName}}" 파일이 있어야 합니다.`, | ||
}, | ||
}, | ||
create(context) { | ||
return { | ||
Program(node) { | ||
const filePath = context.filename; // 현재 파일의 경로 | ||
const dirName = path.basename(path.dirname(filePath)); // 현재 파일이 속한 폴더 이름 | ||
const parentDir = path.dirname(path.dirname(filePath)); // 상위 디렉터리 | ||
|
||
// 부모 폴더가 "components"로 끝나는 폴더만 검사 | ||
if (parentDir.endsWith('components')) { | ||
const expectedFileName = `${dirName}.tsx`; | ||
const expectedFilePath = path.join(parentDir, dirName, expectedFileName); | ||
|
||
if (!fs.existsSync(expectedFilePath)) { | ||
context.report({ | ||
node, | ||
messageId: 'noMatch', | ||
data: { | ||
dirName, | ||
expectedFileName, | ||
}, | ||
}); | ||
} | ||
} | ||
}, | ||
}; | ||
}, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
module.exports = { | ||
meta: { | ||
type: 'suggestion', // 규칙의 성격 | ||
docs: { | ||
description: '컴포넌트의 props 타입은 interface로 나타내며 이름은 컴포넌트 명 + props이다.', | ||
recommended: true, | ||
}, | ||
messages: { | ||
invalidPropsName: "The props interface의'{{actual}}' 이름을 '{{expected}}'로 변경해주세요.", | ||
}, | ||
}, | ||
|
||
create(context) { | ||
return { | ||
ExportDefaultDeclaration(node) { | ||
const sourceCode = context.getSourceCode(); | ||
const filePath = context.getFilename(); | ||
|
||
// export default 변수 이름 추출 | ||
const componentName = node.declaration.name; | ||
|
||
// 컴포넌트 이름이 대문자로 시작하지 않으면 검사하지 않음 | ||
if (!/^[A-Z]/.test(componentName)) { | ||
return; | ||
} | ||
|
||
// components 폴더가 아닌 경우 검사하지 않음 | ||
if (!filePath.includes('components')) { | ||
return; | ||
} | ||
|
||
// 컴포넌트 이름으로 선언된 변수 찾기 | ||
const matchingDeclaration = sourceCode.ast.body.find( | ||
(item) => | ||
item.type === 'VariableDeclaration' && | ||
item.declarations.some((decl) => decl.id.name === componentName), | ||
); | ||
|
||
if (!matchingDeclaration) { | ||
return; | ||
} | ||
|
||
// 파라미터에 사용된 인터페이스 확인 | ||
let usedInterface = null; | ||
|
||
matchingDeclaration.declarations.forEach((decl) => { | ||
if (decl.init?.type === 'ArrowFunctionExpression' && decl.init.params) { | ||
decl.init.params.forEach((param) => { | ||
if (param.typeAnnotation?.type === 'TSTypeAnnotation') { | ||
const typeNode = param.typeAnnotation.typeAnnotation; | ||
if (typeNode.type === 'TSTypeReference') { | ||
const interfaceName = typeNode.typeName.name; | ||
if (interfaceName !== 'PropsWithChildren') { | ||
usedInterface = interfaceName; | ||
} | ||
} | ||
} | ||
}); | ||
} | ||
}); | ||
|
||
// 올바른 인터페이스 이름 계산 | ||
const expectedInterfaceName = `${componentName}Props`; | ||
|
||
// 사용된 인터페이스가 올바르지 않으면 에러 발생 | ||
if (usedInterface && usedInterface !== expectedInterfaceName) { | ||
context.report({ | ||
node, | ||
messageId: 'invalidPropsName', | ||
data: { | ||
actual: usedInterface, | ||
expected: expectedInterfaceName, | ||
}, | ||
}); | ||
} | ||
}, | ||
}; | ||
}, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
module.exports = { | ||
meta: { | ||
type: 'problem', | ||
docs: { | ||
description: | ||
'boolean 값 또는 boolean을 반환하는 함수는 변수명이 반드시 "is"로 시작해야 합니다.', | ||
recommended: true, | ||
}, | ||
messages: { | ||
isPrefix: `변수명: "{{name}}". boolean 값 또는 boolean을 반환하는 함수는 "is"로 시작해야 합니다.`, | ||
}, | ||
}, | ||
create(context) { | ||
const isBooleanReturningFunction = (node) => { | ||
// 화살표 함수 또는 함수 표현식에서 boolean 반환 여부 확인 | ||
if (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') { | ||
if ( | ||
node.body.type === 'Literal' && | ||
typeof node.body.value === 'boolean' // true/false 리터럴 반환 | ||
) { | ||
return true; | ||
} | ||
if ( | ||
node.body.type === 'BinaryExpression' && | ||
['==', '===', '!=', '!==', '>', '>=', '<', '<='].includes(node.body.operator) // BinaryExpression 반환 | ||
) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
}; | ||
|
||
return { | ||
VariableDeclarator(node) { | ||
const init = node.init; | ||
if (!init) return; // 초기화되지 않은 변수는 건너뜀 | ||
|
||
if ( | ||
(init.type === 'Literal' && typeof init.value === 'boolean') || // true/false 리터럴 확인 | ||
(init.type === 'BinaryExpression' && | ||
['==', '===', '!=', '!==', '>', '>=', '<', '<='].includes(init.operator)) || // BinaryExpression 결과가 boolean인지 확인 | ||
isBooleanReturningFunction(init) // boolean을 반환하는 함수 확인 | ||
) { | ||
const variableName = node.id.name; | ||
|
||
// 변수명이 "is"로 시작하지 않는 경우 에러 발생 | ||
if (!variableName.startsWith('is')) { | ||
context.report({ | ||
node, | ||
messageId: 'isPrefix', | ||
data: { | ||
name: variableName, | ||
}, | ||
}); | ||
} | ||
} | ||
}, | ||
}; | ||
}, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
const enforceIsBoolean = require('./enforce-is-boolean'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🌸 칭찬 🌸와우 이렇게 디테일하게 eslint rule을 지정할 수 있군요 대박,,,,너무 좋네요,,,,, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🙏 제안 🙏이건 간단한 제안인데 해당 파일이 대표 파일 느낌인 것 같아서 파일명을 index.js 로 설정해도 괜찮을 것 같네용 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 폴더명과 파일명이 일치하니까 그게 좋을 것 같네요 수정했습니다! |
||
const variableNaming = require('./variable-naming'); | ||
const pageFolderMatch = require('./page-folder-match'); | ||
const componentFolderMatch = require('./component-folder-match'); | ||
const recommendArrowFunction = require('./recommend-arrow-function'); | ||
const componentPropsInterface = require('./component-props-interface'); | ||
|
||
const plugin = { | ||
configs: { | ||
recommended: [ | ||
{ | ||
plugins: { | ||
ddangkong: { | ||
rules: { | ||
'enforce-is-boolean': enforceIsBoolean, | ||
'variable-naming': variableNaming, | ||
'page-folder-match': pageFolderMatch, | ||
'component-folder-match': componentFolderMatch, | ||
'recommend-arrow-function': recommendArrowFunction, | ||
'component-props-interface': componentPropsInterface, | ||
}, | ||
}, | ||
}, | ||
rules: { | ||
'ddangkong/enforce-is-boolean': 'error', | ||
'ddangkong/variable-naming': 'error', | ||
'ddangkong/page-folder-match': 'error', | ||
'ddangkong/component-folder-match': 'error', | ||
'ddangkong/component-props-interface': 'error', | ||
'ddangkong/recommend-arrow-function': 'warn', | ||
}, | ||
}, | ||
], | ||
}, | ||
}; | ||
|
||
module.exports = plugin; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
const path = require('path'); | ||
const fs = require('fs'); | ||
|
||
module.exports = { | ||
meta: { | ||
type: 'problem', | ||
docs: { | ||
description: | ||
'pages 폴더 안에는 Page로 끝나는 폴더와 해당 폴더명과 일치하는 tsx 파일이 있어야 한다.', | ||
recommended: true, | ||
}, | ||
messages: { | ||
noMatch: `pages 폴더 안에 "{{dirName}}" 폴더와 이에 해당하는 "{{expectedFileName}}" 파일이 있어야 합니다.`, | ||
}, | ||
}, | ||
create(context) { | ||
return { | ||
Program(node) { | ||
const filePath = context.filename; // 현재 파일의 경로 | ||
const dirName = path.basename(path.dirname(filePath)); // 현재 파일이 속한 폴더 이름 | ||
|
||
// "Page"로 끝나는 폴더만 검사 | ||
if (dirName.endsWith('Page')) { | ||
const parentDir = path.dirname(path.dirname(filePath)); // 상위 디렉터리 | ||
const expectedFileName = `${dirName}.tsx`; | ||
const expectedFilePath = path.join(parentDir, dirName, expectedFileName); | ||
|
||
if (!fs.existsSync(expectedFilePath) || !parentDir.endsWith('pages')) { | ||
context.report({ | ||
node, | ||
messageId: 'noMatch', | ||
data: { | ||
dirName, | ||
expectedFileName, | ||
}, | ||
}); | ||
} | ||
} | ||
}, | ||
}; | ||
}, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
module.exports = { | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
description: '함수를 선언할 때는 화살표 함수의 사용을 권장합니다.', | ||
recommended: true, | ||
}, | ||
messages: { | ||
noFunctionKeyword: '{{name}} 함수를 선언할 때는 화살표 함수의 사용을 권장합니다.', | ||
}, | ||
}, | ||
|
||
create(context) { | ||
return { | ||
FunctionDeclaration(node) { | ||
// 클래스 메서드는 제외 | ||
if (node.parent.type === 'ClassBody') return; | ||
|
||
context.report({ | ||
node, | ||
messageId: 'noFunctionKeyword', | ||
data: { | ||
name: node.id ? node.id.name : 'anonymous', | ||
}, | ||
}); | ||
}, | ||
FunctionExpression(node) { | ||
// 클래스 메서드는 제외 | ||
if (node.parent.type === 'MethodDefinition') return; | ||
|
||
// 객체 메서드도 제외 | ||
if (node.parent.type === 'Property' && node.parent.method === true) return; | ||
|
||
context.report({ | ||
node, | ||
messageId: 'noFunctionKeyword', | ||
data: { | ||
name: node.id ? node.id.name : 'anonymous', | ||
}, | ||
}); | ||
}, | ||
}; | ||
}, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🌸 칭찬 🌸
오옹 이런 방식으로 eslint rule 들이 만들어지는군요,,,한번 공부해봐야겠네용
머지되면 콘솔 찍어보면서 입력으로 들어오는 값들을 이해해보겠숨돠