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: Vue SFC support #3

Merged
merged 26 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@
},
"devDependencies": {
"@types/eslint": "^8.56.10",
"@types/estree": "^1.0.5",
"@types/node": "^20.14.11",
"alias-imports": "^1.1.0",
"clean-pkg-json": "^1.2.0",
"dot-prop": "^8.0.2",
"eslint-plugin-vue": "^9.27.0",
"eslint7": "npm:eslint@7.0.0",
"eslint8": "npm:eslint@^8.56.0",
"execa": "^8.0.1",
Expand All @@ -59,6 +61,7 @@
"outdent": "^0.8.0",
"pkgroll": "^2.4.1",
"tsx": "^4.16.2",
"typescript": "^5.5.3"
"typescript": "^5.5.3",
"vue-eslint-parser": "^9.4.3"
}
}
17 changes: 13 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion src/@types/eslint.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SourceCode } from 'eslint';
import type { AST } from 'vue-eslint-parser';

declare module 'eslint' {

Expand All @@ -18,4 +18,10 @@ declare module 'eslint' {
options: Linter.FixOptions,
): LintMessage[];
}

interface SourceCode {
parserServices: {
getDocumentFragment?: () => AST.VDocumentFragment;
};
}
}
136 changes: 99 additions & 37 deletions src/rules/fix-later/fix-later.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
import eslint, { type Linter, type SourceCode } from 'eslint';
import { getSeverity } from './utils/eslint.js';
import {
getSeverity,
groupMessagesByLine,
} from './utils/eslint';
import {
insertIgnoreAboveLine,
insertIgnoreSameLine,
} from './utils/fixer';
import {
gitBlame,
type GitBlame,
} from './utils/git';
import {
interpolateString,
} from './utils/interpolate-string';
import { ruleId, ruleOptions } from './rule-meta';
insertCommentAboveLine,
insertCommentSameLine,
} from './utils/fixer.js';
import { gitBlame, type GitBlame } from './utils/git.js';
import { interpolateString } from './utils/interpolate-string.js';
import { ruleId, ruleOptions } from './rule-meta.js';
import { getVueElement } from './utils/vue.js';

type LintMessage = Linter.LintMessage | Linter.SuppressedLintMessage;

const allowedErrorPattern = /^Definition for rule '[^']+' was not found\.$/;

const getRuleIds = (
lintMessages: LintMessage[],
) => {
const ruleIds: string[] = [];
for (const message of lintMessages) {
if (message.ruleId && !ruleIds.includes(message.ruleId)) {
ruleIds.push(message.ruleId);
}
}
return ruleIds;
};

const suppressFileErrors = (

Check warning on line 28 in src/rules/fix-later/fix-later.ts

View workflow job for this annotation

GitHub Actions / Test

Arrow function has a complexity of 20. Maximum allowed is 10
code: string,
sourceCode: SourceCode,
extractedConfig: Linter.Config,
Expand Down Expand Up @@ -79,69 +84,126 @@
return messages;
}

if (processMessages.length === 0) {
return messages;
}

const { commentTemplate } = ruleOptions;
const messagesGroupedByLine = groupMessagesByLine(processMessages);

for (const lineGroup of messagesGroupedByLine) {
const rulesToDisable = lineGroup
.map(rule => rule.ruleId)
.join(', ');
// The number is the line where the disable comment should be inserted
const groupedByLine: Record<string, {
line: LintMessage[];
start: LintMessage[];
end: LintMessage[];
}> = {};
const addMessage = (
key: string | number,
type: 'line' | 'start' | 'end',
message: LintMessage,
) => {
if (!groupedByLine[key]) {
groupedByLine[key] = {
line: [],
start: [],
end: [],
};
}
groupedByLine[key][type].push(message);
};

const [message] = lineGroup;
for (const message of processMessages) {
const reportedIndex = sourceCode.getIndexFromLoc({
line: message.line,
column: message.column - 1,
});
const reportedNode = sourceCode.getNodeByRangeIndex(reportedIndex);
if (!reportedNode) {
continue;
if (reportedNode) {
addMessage(message.line, 'line', message);
} else {
// Vue.js template
const vueDocumentFragment = sourceCode.parserServices.getDocumentFragment?.();
const templateNode = getVueElement(reportedIndex, vueDocumentFragment);

if (templateNode) {
addMessage(templateNode.loc.start.line, 'start', message);
addMessage(templateNode.loc.end.line + 1, 'end', message);
}
}
}

const getLineComment = (
message: LintMessage,
): string => {
let blameData: GitBlame | undefined;
const comment = interpolateString(
commentTemplate,
{
'eslint-disable': `${ruleOptions.disableDirective} ${rulesToDisable}`,
get blame() {
if (filename && !blameData) {
blameData = gitBlame(filename, message.line, message.endLine ?? message.line);
}
return blameData;
},
// TODO: codeowners
},
(_match, key) => {
throw new Error(`Can't find key: ${key}`);
},
);

const lineStart = sourceCode.getIndexFromLoc({
line: message.line,
return comment;
};

for (const key in groupedByLine) {
if (!Object.hasOwn(groupedByLine, key)) {
continue;
}

const groupedMessages = groupedByLine[key];
const comments = [];
if (groupedMessages.line.length > 0) {
const rulesToDisable = getRuleIds(groupedMessages.line).join(', ');
comments.push(`${ruleOptions!.disableDirective} ${rulesToDisable} -- ${getLineComment(groupedMessages.line[0])}`);
}
if (groupedMessages.start.length > 0) {
const rulesToDisable = getRuleIds(groupedMessages.start).join(', ');
comments.push(`<!-- eslint-disable ${rulesToDisable} -- ${getLineComment(groupedMessages.start[0])} -->`);
}
if (groupedMessages.end.length > 0) {
const rulesToDisable = getRuleIds(groupedMessages.end).join(', ');
comments.push(`<!-- eslint-enable ${rulesToDisable} -->`);
}

const lineStartIndex = sourceCode.getIndexFromLoc({
line: Number(key),
column: 0,
});

const comment = comments.join('\n');
messages.push({
ruleId,
severity: ruleSeverity,
message: `Suppressing errors: ${rulesToDisable}`,
line: message.line,
column: message.column,
message: 'Suppressing errors',
line: 0,
column: 0,
fix: (
ruleOptions.insertDisableComment === 'above-line'
? insertIgnoreAboveLine(
(
ruleOptions.insertDisableComment === 'above-line'
|| groupedMessages.start.length > 0
|| groupedMessages.end.length > 0
)
? insertCommentAboveLine(
code,
lineStart,
lineStartIndex,
comment,
)
: insertIgnoreSameLine(
: insertCommentSameLine(
code,
lineStart,
lineStartIndex,
comment,
)
),
});

// In practice, ESLint runs multiple times so the suppressed rules
// don't need to be removed
}

return messages;
Expand All @@ -156,7 +218,7 @@
_verifyWithProcessor,
} = eslint.Linter.prototype;

eslint.Linter.prototype._verifyWithoutProcessors = function (

Check warning on line 221 in src/rules/fix-later/fix-later.ts

View workflow job for this annotation

GitHub Actions / Test

Unexpected unnamed function
textOrSourceCode,
config,
options,
Expand Down Expand Up @@ -187,7 +249,7 @@
* So a plugin's postprocess may remove suppressed errors
* We want to filter out after that
*/
eslint.Linter.prototype._verifyWithProcessor = function (

Check warning on line 252 in src/rules/fix-later/fix-later.ts

View workflow job for this annotation

GitHub Actions / Test

Unexpected unnamed function
textOrSourceCode,
config,
options,
Expand Down
48 changes: 33 additions & 15 deletions src/rules/fix-later/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type Rule } from 'eslint';
import './fix-later.js';
import type { SourceLocation } from 'estree';
import { setRuleUsage, normalizeOptions } from './rule-meta.js';
import { interpolateString } from './utils/interpolate-string.js';
import { escapeRegExp } from './utils/escape-regexp.js';
Expand Down Expand Up @@ -36,14 +37,12 @@ export const fixLater = {
const options = normalizeOptions(context.options[0]);
setRuleUsage(context.id, options);

const descriptionDelimiter = '--';
const descriptionDelimiter = ' -- ';

const wildCard = Math.random().toString(36).slice(2);
const template = interpolateString(
options.commentTemplate,
{
'eslint-disable': `${options.disableDirective} ${wildCard}`,
},
{},
() => wildCard,
);
const suppressCommentPattern = new RegExp(`^${escapeRegExp(template).replaceAll(wildCard, '.+?')}$`);
Expand All @@ -52,26 +51,45 @@ export const fixLater = {
const sourceCode = context.sourceCode ?? context.getSourceCode();
const comments = sourceCode.getAllComments();

for (const comment of comments) {
// comment.value doesn't contain the syntax of the comment
const commentString = sourceCode.text.slice(comment.range![0], comment.range![1]);

if (!suppressCommentPattern.test(commentString)) {
continue;
const reportCommentDescription = (
commentString: string,
commentLocation: SourceLocation,
) => {
const descriptionIndex = commentString.indexOf(descriptionDelimiter);
if (descriptionIndex === -1) {
return;
}

const descriptionIndex = commentString.indexOf(descriptionDelimiter);
const description = commentString.slice(
descriptionIndex + descriptionDelimiter.length,
).trim();
const description = commentString.slice(descriptionIndex + descriptionDelimiter.length);
if (!suppressCommentPattern.test(description)) {
return;
}

context.report({
loc: comment.loc!,
loc: commentLocation,
messageId: 'remindToFix',
data: {
description,
},
});
};

for (const comment of comments) {
// comment.value doesn't contain the syntax of the comment
const commentString = sourceCode.text.slice(comment.range![0] + 2, comment.range![1]).trim();
if (commentString.startsWith('eslint-disable-')) {
reportCommentDescription(commentString, comment.loc!);
}
}

const vueDocument = sourceCode.parserServices.getDocumentFragment?.();
if (vueDocument) {
for (const comment of vueDocument.comments) {
const commentText = comment.value.trim();
if (commentText.startsWith('eslint-disable')) {
reportCommentDescription(commentText, comment.loc);
}
}
}

return {};
Expand Down
8 changes: 4 additions & 4 deletions src/rules/fix-later/rule-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { name } from '../../../package.json';
type RuleOptions = {
includeWarnings: boolean;
insertDisableComment: 'above-line' | 'end-of-line';
disableDirective: 'eslint-disable-line' | 'eslint-disable-next-line';
disableDirective: '// eslint-disable-line' | '// eslint-disable-next-line';
commentTemplate: string;
};

Expand All @@ -26,10 +26,10 @@ export const normalizeOptions = (
insertDisableComment,
disableDirective: (
insertDisableComment === 'above-line'
? 'eslint-disable-next-line'
: 'eslint-disable-line'
? '// eslint-disable-next-line'
: '// eslint-disable-line'
),
commentTemplate: `// {{ eslint-disable }} -- ${commentTemplate}`,
commentTemplate,
};
};

Expand Down
Loading
Loading