Skip to content

Commit 49ba255

Browse files
leejs0823chlgmltnhong-seosumingsu40
authored
fix: 6 rules detail fix (#53)
* feat(#29): 중복 문제 해결 (#29) * feat(#29): 중복 문제 해결 * Revert "Merge branch 'develop' into feature/#29" * 🐛 fix: 삭제된 코드 revert --------- Co-authored-by: jungsun <[email protected]> * refactor(lint-manager): ESLint 설정 탐색 및 병합 로직 리팩토링 (#30) * feat: 빠른 수정 메뉴에서 disable 안보이게 설정 * refactor: 5가지 핵심 논리 규칙 안정성 및 UX 개선 (#35) LGTM * feat: 퀵픽스 메뉴 한글 통일 및 AI 프롬프트 개선 (#36) LGTM * 🚀 deploy: tag 기반 release 작업 정리를 위해 workflow 수정 * 📝 docs: license 파일 수정 및 불필요한 의존성 삭제, 의존성 라이브러리 정리 * 📝 docs: 직접 의존성만 추가하도록 수정 * 📝 docs: LICENSE.txt > LICENSE * 📝 docs: 오픈소스 라이선스 전문 추가 및 의존성 라이브러리 전부 추가 * fix: AI 파트 실행 오류 수정 (#40) * 🔧 chore: setup semantic-release for VSCode extension deployment * fix: publish.yml 수정 * fix: .vscodeignore에 env 추가 * Refactor/#48 filepath and config refactor (#49) * refactor(lint): 파일 경로 처리 및 ESLint 설정 탐색 로직 리팩토링 * refactor(lint): 파일 경로 처리 및 ESLint 설정 탐색 로직 리팩토링 * Refactor/#51 (#52) * refactor: 5가지 핵심 논리 규칙 안정성 및 UX 개선 * Refactor(a11y): 6개 로직 개선 --------- Co-authored-by: 최희수 <[email protected]> Co-authored-by: hong-seo <[email protected]> Co-authored-by: hansumin <[email protected]>
1 parent 5f1e238 commit 49ba255

File tree

6 files changed

+148
-79
lines changed

6 files changed

+148
-79
lines changed

src/rules/logic/aria-activedescendant-has-tabindex.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function ariaActivedescendantHasTabindexFix(
1818
fix.edit = new vscode.WorkspaceEdit();
1919

2020
// 열림 태그에 tabIndex="0" 추가
21-
const newCode = context.code.replace(/^<(\w+)/, `<$1 tabIndex="0"`);
21+
const newCode = context.code.replace(/^<(\w+)/, `<$1 tabIndex={0}`);
2222

2323
fix.edit.replace(context.document.uri, context.range, newCode);
2424
fix.diagnostics = [

src/rules/logic/click-events-have-key-events.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,19 @@ export function clickEventsHaveKeyEventsFix(
77
const { code, range, document } = context;
88

99
// 이미 onKeyDown 핸들러가 있으면 수정하지 않음
10-
if (/\bonKeyDown\s*=/.test(code)) {
11-
return [];
12-
}
10+
if (/\bonKeyDown\s*=/.test(code)) return [];
11+
12+
// onClick 핸들러 추출
13+
const onClickMatch = code.match(/\bonClick\s*=\s*(\{[^}]+\}|\{[\s\S]*?\})/);
14+
if (!onClickMatch) return [];
1315

14-
// onClick 핸들러와 그 내용을 추출
15-
const onClickMatch = code.match(/\bonClick\s*=\s*({[\s\S]*?})/);
16-
if (!onClickMatch) {
17-
return [];
16+
const onClickAttribute = onClickMatch[0];
17+
const onClickValue = onClickMatch[1];
18+
19+
// onClick 내용 정제
20+
let handlerCall = onClickValue.slice(1, -1).trim();
21+
if (!handlerCall.endsWith(")")) {
22+
handlerCall += "(event)";
1823
}
1924

2025
const onClickAttribute = onClickMatch[0]; // ex: onClick={() => alert('hi')}
@@ -23,26 +28,27 @@ export function clickEventsHaveKeyEventsFix(
2328
// onKeyDown 핸들러를 새로 생성
2429
const onKeyDownAttribute = ` onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { (${onClickValue.slice(1, -1)})(event); } }}`;
2530

26-
// 기존 onClick 속성 바로 뒤에 onKeyDown 속성을 추가
27-
const newCode = code.replace(onClickAttribute, onClickAttribute + onKeyDownAttribute);
31+
// onKeyDown 핸들러 생성
32+
const onKeyDownAttribute = ` onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { ${handlerCall}; } }}`;
2833

29-
if (newCode === code) {
30-
return [];
31-
}
34+
// 새로운 코드 생성
35+
const newCode = code.replace(onClickAttribute, onClickAttribute + onKeyDownAttribute);
3236

37+
// Quick Fix 생성
3338
const fix = new vscode.CodeAction(
3439
`키보드 이벤트(onKeyDown) 추가`,
3540
vscode.CodeActionKind.QuickFix
3641
);
42+
fix.isPreferred = true;
3743
fix.edit = new vscode.WorkspaceEdit();
3844
fix.edit.replace(document.uri, range, newCode);
3945
fix.diagnostics = [
4046
new vscode.Diagnostic(
4147
range,
42-
`클릭 가능한 요소에는 키보드 이벤트도 함께 제공해야 합니다.`,
48+
`클릭 이벤트만 있는 요소는 키보드 접근성이 부족할 수 있습니다. onKeyDown 핸들러를 추가하여 Enter/Space 키 입력을 처리하세요.`,
4349
vscode.DiagnosticSeverity.Warning
4450
),
4551
];
46-
52+
4753
return [fix];
48-
}
54+
}

src/rules/logic/interactive-supports-focus.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function interactiveSupportsFocusFix(
1717
);
1818
fix.edit = new vscode.WorkspaceEdit();
1919

20-
const newCode = context.code.replace(/^<(\w+)/, `<$1 tabIndex="0"`);
20+
const newCode = context.code.replace(/^<(\w+)/, `<$1 tabIndex={0}`);
2121

2222
fix.edit.replace(context.document.uri, context.range, newCode);
2323
fix.diagnostics = [

src/rules/logic/label-has-associated-control.ts

Lines changed: 82 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,42 +7,89 @@ export const labelHasAssociatedControlFix: RuleFixer = (
77
const { code, range, document, fullLine } = context;
88
const fixes: vscode.CodeAction[] = [];
99

10-
// 1. 같은 줄에 있는 input의 id를 찾아 for(htmlFor) 속성으로 연결하는 것이 가장 안정적입니다.
11-
// fullLine에서 id="some-id" 패턴을 찾습니다.
12-
const inputIdMatch = fullLine.match(/id="([^"]+)"/);
13-
14-
// 만약 연결할 input의 id를 찾았다면, for 속성을 추가하는 수정 제안을 생성합니다.
15-
if (inputIdMatch) {
16-
const inputId = inputIdMatch[1];
17-
18-
// <label> 태그에 for="some-id"를 추가합니다.
19-
const fixedCode = code.replace(
20-
/<label(.*?)>/i,
21-
`<label htmlFor="${inputId}"$1>`
22-
);
23-
24-
const fixFor = new vscode.CodeAction(
25-
`input id와 연결 (htmlFor="${inputId}")`,
26-
vscode.CodeActionKind.QuickFix
27-
);
28-
fixFor.edit = new vscode.WorkspaceEdit();
29-
fixFor.edit.replace(document.uri, range, fixedCode);
30-
fixFor.isPreferred = true; // 이 수정을 가장 우선적으로 추천합니다.
31-
32-
fixFor.diagnostics = [
33-
{
34-
message: `label의 htmlFor 속성을 input의 id와 연결하여 접근성을 향상시킵니다.`,
35-
range,
36-
severity: vscode.DiagnosticSeverity.Warning,
37-
source: "a11y-fixer",
38-
},
39-
];
40-
fixes.push(fixFor);
10+
// 1) 현재 라벨 태그(열리는 태그) 가져오기
11+
const labelMatch = code.match(/<label\b([^>]*)>/i);
12+
if (!labelMatch) {
13+
// 라벨 패턴을 찾지 못하면 종료
14+
return [];
4115
}
16+
const labelOpenTag = labelMatch[0]; // e.g. <label> or <label class="...">
17+
const labelAttrs = labelMatch[1] || ""; // 속성 문자열
4218

43-
// 2. 오류가 있던 '중첩' 로직은 제거하여 혼동을 방지합니다.
44-
// 만약 for 속성으로 연결할 id를 찾지 못했다면, AI 기반 수정(control-has-associated-label)이
45-
// 더 복잡한 문맥을 이해하고 해결책을 제시할 수 있으므로, 여기서는 추가적인 제안을 하지 않습니다.
19+
// 2) 이미 htmlFor / for 속성이 있으면 수정 제안 불필요
20+
if (/\bhtmlFor\s*=|\bfor\s*=/.test(labelAttrs)) {
21+
return [];
22+
}
23+
24+
// 3) 라벨과 연결된 input id를 찾기 — 현재 줄을 기준으로 아래/위로 탐색
25+
const startLine = range.start.line;
26+
const maxLookahead = 10; // 검색할 라인 수(요구에 따라 늘릴수 있음)
27+
let foundId: string | null = null;
28+
29+
// 우선 현재 라인(또는 fullLine)에 id가 있는지 확인
30+
const idInFullLine = fullLine.match(/id\s*=\s*["']([^"']+)["']/);
31+
if (idInFullLine) {
32+
foundId = idInFullLine[1];
33+
}
34+
35+
// 아래쪽으로 탐색
36+
if (!foundId) {
37+
for (let i = startLine; i <= Math.min(document.lineCount - 1, startLine + maxLookahead); i++) {
38+
const text = document.lineAt(i).text;
39+
const m = text.match(/id\s*=\s*["']([^"']+)["']/);
40+
if (m) {
41+
foundId = m[1];
42+
break;
43+
}
44+
}
45+
}
46+
47+
// 위쪽으로 탐색 (아래에서 못 찾았을 때)
48+
if (!foundId) {
49+
for (let i = startLine - 1; i >= Math.max(0, startLine - maxLookahead); i--) {
50+
const text = document.lineAt(i).text;
51+
const m = text.match(/id\s*=\s*["']([^"']+)["']/);
52+
if (m) {
53+
foundId = m[1];
54+
break;
55+
}
56+
}
57+
}
58+
59+
if (!foundId) {
60+
// 가까운 라인에서 id를 찾지 못하면 제안하지 않음
61+
return [];
62+
}
63+
64+
// 4) 치환: 기존 attrs를 보존하고 htmlFor 속성 추가
65+
// labelOpenTag 예시: "<label>" or "<label class=\"x\">"
66+
// 새 오프닝 태그를 안전하게 생성
67+
// 공백 처리: 만약 attrs가 비어있으면 한 칸 띄우지 않음
68+
const attrsPart = labelAttrs.trim();
69+
const newLabelOpenTag = attrsPart.length > 0
70+
? `<label ${attrsPart} htmlFor="${foundId}">`
71+
: `<label htmlFor="${foundId}">`;
72+
73+
// code 문자열 내에서 첫 번째 라벨 오프닝 태그를 대체
74+
const fixedCode = code.replace(labelOpenTag, newLabelOpenTag);
75+
76+
// 5) CodeAction 생성
77+
const fixFor = new vscode.CodeAction(
78+
`input id와 연결 (htmlFor="${foundId}")`,
79+
vscode.CodeActionKind.QuickFix
80+
);
81+
fixFor.edit = new vscode.WorkspaceEdit();
82+
fixFor.edit.replace(document.uri, range, fixedCode);
83+
fixFor.isPreferred = true;
84+
85+
fixFor.diagnostics = [
86+
new vscode.Diagnostic(
87+
range,
88+
`label의 htmlFor 속성을 input의 id("${foundId}")와 연결하여 접근성을 향상시킵니다.`,
89+
vscode.DiagnosticSeverity.Warning
90+
),
91+
];
4692

93+
fixes.push(fixFor);
4794
return fixes;
48-
};
95+
};

src/rules/logic/no-noninteractive-tabindex.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,34 @@ export const noNoninteractiveTabindexFix: RuleFixer = (
99
): vscode.CodeAction[] => {
1010
const { code, range, document } = context;
1111

12-
// tabIndex 속성과 값만 정확히 일치시키는 정규식으로 수정
13-
// 공백을 포함하여 'tabIndex' 속성만 정확히 제거하도록 개선
14-
const fixed = code.replace(/\s+tabIndex\s*=\s*(?:".*?"|'.*?'|{[^}]*})/, "");
12+
// ✅ 정확히 tabIndex={0} 또는 "0" 등을 매칭
13+
const tabIndexRegex = /\s*tabIndex\s*=\s*(?:"[^"]*"|'[^']*'|\{[^}]*\})/;
14+
const match = code.match(tabIndexRegex);
1515

16-
if (fixed === code) {
16+
if (!match) {
17+
console.warn(`[DEBUG] tabIndex not found in code: ${code}`);
1718
return [];
1819
}
1920

21+
const matchText = match[0];
22+
const startOffset = code.indexOf(matchText);
23+
const endOffset = startOffset + matchText.length;
24+
25+
// 실제로 교체할 범위 계산 (range 기준 상대 위치로)
26+
const tabIndexRange = new vscode.Range(
27+
document.positionAt(document.offsetAt(range.start) + startOffset),
28+
document.positionAt(document.offsetAt(range.start) + endOffset)
29+
);
30+
2031
const fix = new vscode.CodeAction(
2132
`tabIndex 속성 제거`,
2233
vscode.CodeActionKind.QuickFix
2334
);
2435

2536
fix.edit = new vscode.WorkspaceEdit();
26-
fix.edit.replace(document.uri, range, fixed);
27-
fix.isPreferred = true;
37+
fix.edit.replace(document.uri, tabIndexRange, "");
2838

39+
fix.isPreferred = true;
2940
fix.diagnostics = [
3041
{
3142
message: `tabIndex는 비인터랙티브 요소에 사용될 수 없습니다.`,
@@ -36,4 +47,4 @@ export const noNoninteractiveTabindexFix: RuleFixer = (
3647
];
3748

3849
return [fix];
39-
};
50+
};

src/rules/logic/tabindex-no-positive.ts

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,54 +6,59 @@ export function tabindexNoPositiveFix(
66
): vscode.CodeAction[] {
77
const fixes: vscode.CodeAction[] = [];
88

9-
const regex = /(tabIndex\s*=\s*)(["']?|\{?)(\d+)(["']?|\}?)/;
9+
// 캡처 그룹 1: 값 부분 (예: "{2}", "'2'", '"2"')
10+
const regex = /tabIndex\s*=\s*(\{?\s*['"]?\s*\d+\s*['"]?\s*\}?)/;
1011
const match = context.code.match(regex);
1112

12-
if (!match || match.length < 5) {
13+
// [수정됨]
14+
// match.length는 2여야 합니다 (match[0]: 전체, match[1]: 캡처 그룹).
15+
if (!match || match.length < 2) {
1316
console.warn(
1417
`[DEBUG - tabindexFix] Could not parse tabIndex value from context.code: '${context.code}'. Cannot provide fix.`
1518
);
1619
return [];
1720
}
1821

19-
const [, prefix, opener, originalValue, closer] = match;
22+
// 값 부분(match[1])에서 숫자만 추출합니다.
23+
const valueMatch = match[1].match(/\d+/);
24+
if (!valueMatch || parseInt(valueMatch[0], 10) <= 0) {
25+
// 숫자를 찾지 못했거나 양수가 아니면 종료
26+
return [];
27+
}
2028

21-
if (parseInt(originalValue, 10) <= 0) {
22-
return []; // 양수가 아니면 수정 제안을 만들지 않고 종료합니다.
23-
}
24-
// 매개변수 'targetValue'에 string 타입을 명시적으로 지정
25-
const createReplacementCode = (targetValue: string) => {
26-
return context.code.replace(match[0], `${prefix}"${targetValue}"`);
29+
// targetValue는 0 또는 -1 숫자입니다.
30+
const createReplacementCode = (targetValue: number) => {
31+
// [수정됨]
32+
// context.code에서 값 부분(match[1])을 새로운 JSX 표현식(예: {0})으로 교체합니다.
33+
// (이전 로직은 if문이 중복되어 하나로 통일했습니다.)
34+
return context.code.replace(match[1], `{${targetValue}}`);
2735
};
2836

29-
// 1. tabIndex를 "0"으로 변경하는 제안
37+
// 1. tabIndex를 {0}으로 변경하는 제안
3038
const fixToZero = new vscode.CodeAction(
31-
`tabIndex를 "0"으로 변경 (기본 포커스 순서 포함)`,
39+
`tabIndex를 {0}으로 변경 (기본 포커스 순서 포함)`,
3240
vscode.CodeActionKind.QuickFix
3341
);
3442
fixToZero.edit = new vscode.WorkspaceEdit();
3543
fixToZero.isPreferred = true;
36-
const newCodeZero = createReplacementCode("0");
37-
44+
const newCodeZero = createReplacementCode(0);
3845
fixToZero.edit.replace(context.document.uri, context.range, newCodeZero);
3946
fixToZero.diagnostics = [
4047
new vscode.Diagnostic(
4148
context.range,
42-
`tabIndex에 양수 값 사용은 접근성을 저해할 수 있습니다. (권장: tabIndex="0")`,
49+
`tabIndex에 양수 값 사용은 접근성을 저해할 수 있습니다. (권장: tabIndex={0})`,
4350
vscode.DiagnosticSeverity.Warning
4451
),
4552
];
4653
fixes.push(fixToZero);
4754

48-
// 2. tabIndex를 "-1"로 변경하는 제안
55+
// 2. tabIndex를 {-1}로 변경하는 제안
4956
const fixToMinusOne = new vscode.CodeAction(
50-
`tabIndex를 "-1"로 변경 (프로그래밍 방식 포커스만 가능)`,
57+
`tabIndex를 {-1}로 변경 (프로그래밍 방식 포커스만 가능)`,
5158
vscode.CodeActionKind.QuickFix
5259
);
5360
fixToMinusOne.edit = new vscode.WorkspaceEdit();
54-
55-
const newCodeMinusOne = createReplacementCode("-1");
56-
61+
const newCodeMinusOne = createReplacementCode(-1);
5762
fixToMinusOne.edit.replace(
5863
context.document.uri,
5964
context.range,
@@ -62,7 +67,7 @@ export function tabindexNoPositiveFix(
6267
fixToMinusOne.diagnostics = [
6368
new vscode.Diagnostic(
6469
context.range,
65-
`tabIndex에 양수 값 사용은 접근성을 저해할 수 있습니다. (대안: tabIndex="-1")`,
70+
`tabIndex에 양수 값 사용은 접근성을 저해할 수 있습니다. (대안: tabIndex={-1})`,
6671
vscode.DiagnosticSeverity.Warning
6772
),
6873
];

0 commit comments

Comments
 (0)