Skip to content
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

Merged
merged 37 commits into from
Dec 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
d97adbf
chore: eslint 설정 파일 확장자 변경 #429
novice0840 Dec 5, 2024
2388b6b
chore: eslint 8 -> 9 migration #429
novice0840 Dec 6, 2024
9646244
chore: eslint plugin 추가 #429
novice0840 Dec 6, 2024
10e8e46
chore: eslint plugin flag configuration 모드로 변경 #429
novice0840 Dec 6, 2024
11deca3
chore: 테스트 용 커스텀 규칙 연동 #429
novice0840 Dec 6, 2024
33ee3b1
feat: eslint enforce-is-boolean 규칙 추가 #429
novice0840 Dec 13, 2024
f65f67f
fix: stories 이름이 붙은 파일에만 eslint-plugin-storybook 적용 #429
novice0840 Dec 13, 2024
6c323fd
fix: eslint files 경로 변경 #429
novice0840 Dec 13, 2024
27df64a
fix: enforce-is-boolean 조건에 함수 반환값 검사 추가 #429
novice0840 Dec 13, 2024
0c863c5
build: process 전역 객체를 인식하기 위해 @types/node 패키지 추가 #429
novice0840 Dec 15, 2024
5af175f
feat: enforce-is-boolean 규칙 예외 케이스 추가가 #429
novice0840 Dec 19, 2024
fd55fa4
fix: import/named 에러 해결결 #429
novice0840 Dec 19, 2024
6f20ef9
fix: 사용하지 않는 eslint-storybook 규칙 off #429
novice0840 Dec 19, 2024
6d79a86
fix: url 파일에 사용하지 않는 코드 제거거 #429
novice0840 Dec 19, 2024
e325fa2
fix: interface import 시 type 명시 #429
novice0840 Dec 19, 2024
b634d81
fix: @types/node 추가해 process, Node 전역 객체 인식 못하는 문제 해결결 #429
novice0840 Dec 19, 2024
fd72c23
fix: context, component 분리해 eslint fast refresh 문제 해결결 #429
novice0840 Dec 19, 2024
eab383d
fix: modal provider, context 각각 파일로 분리하여 eslint-fast-refresh 에러 해결결 #429
novice0840 Dec 19, 2024
33de947
fix: eslint 에러 해결결 #429
novice0840 Dec 19, 2024
cde54b8
fix: eslint-ddangkong recommended 설정 추가가 #429
novice0840 Dec 19, 2024
daf5d56
feat: variable-naming 규칙 추가 #429
novice0840 Dec 20, 2024
7f1aef2
feat: page-folder-match 규칙 추가 #429
novice0840 Dec 20, 2024
00f30c9
feat: eslint component-folder-match 규칙 추가 #429
novice0840 Dec 20, 2024
0c3a053
feat: page-folder-match 규칙 추가 #429
novice0840 Dec 20, 2024
08cbd4a
feat: eslint recommend-arrow-function 규칙 추가 #429
novice0840 Dec 20, 2024
a5146a7
refactor: eslint custom rules 메세지 추가 #429
novice0840 Dec 20, 2024
7b6b7ac
feat: eslint component-props-interface 규칙 추가 #429
novice0840 Dec 20, 2024
7edad06
fix: eslint 에러 해결 #429
novice0840 Dec 20, 2024
3ff4b0b
refactor: component-props-interface 규칙 문서 한글로 변경 #429
novice0840 Dec 20, 2024
0f05799
fix: 불필요한 코드 제거 #429
novice0840 Dec 20, 2024
4b24efb
fix: test 코드 상에서 무한루프 버그 해결 #429
novice0840 Dec 20, 2024
f082295
chore: eslint-plugin-prttier 추가 #429
novice0840 Dec 23, 2024
8893b6f
refactor: contexts 폴더 분리 #429
novice0840 Dec 23, 2024
5654b19
fix: mockServiceWorker 파일 복구 #429
novice0840 Dec 23, 2024
fa8964e
fix: 불필요한 패키지 제거 #429
novice0840 Dec 23, 2024
9ccd5d8
refactor: eslint-plugin-ddangkong 폴더명 변경 #429
novice0840 Dec 23, 2024
bcbd0e3
refactor: ModalProps 타입 수정 #429
novice0840 Dec 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 0 additions & 76 deletions frontend/.eslintrc.json

This file was deleted.

42 changes: 42 additions & 0 deletions frontend/eslint-plugin-ddangkong/component-folder-match.js
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,
},
});
}
}
},
};
},
};
79 changes: 79 additions & 0 deletions frontend/eslint-plugin-ddangkong/component-props-interface.js
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,
},
});
}
},
};
},
};
60 changes: 60 additions & 0 deletions frontend/eslint-plugin-ddangkong/enforce-is-boolean.js
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,
},
});
}
}
},
};
},
};
37 changes: 37 additions & 0 deletions frontend/eslint-plugin-ddangkong/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const enforceIsBoolean = require('./enforce-is-boolean');
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;
42 changes: 42 additions & 0 deletions frontend/eslint-plugin-ddangkong/page-folder-match.js
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,
},
});
}
}
},
};
},
};
44 changes: 44 additions & 0 deletions frontend/eslint-plugin-ddangkong/recommend-arrow-function.js
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',
},
});
},
};
},
};
Loading
Loading