diff --git a/lib/index.ts b/lib/index.ts index 422daa29..c1c4f639 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -8,7 +8,7 @@ const configs = { } const rules = ruleList.reduce((obj, r) => { - obj[r.meta.docs.ruleName] = r + obj[r.meta.docs?.ruleName || ""] = r return obj }, {} as { [key: string]: Rule }) diff --git a/lib/rules/require-scoped.ts b/lib/rules/require-scoped.ts index 661fd35f..b39e6ced 100644 --- a/lib/rules/require-scoped.ts +++ b/lib/rules/require-scoped.ts @@ -1,5 +1,5 @@ import { getStyleContexts, getCommentDirectivesReporter } from "../styles" -import { RuleContext, AST } from "../types" +import { RuleContext, AST, TokenStore } from "../types" module.exports = { meta: { @@ -10,10 +10,12 @@ module.exports = { default: "warn", url: "https://future-architect.github.io/eslint-plugin-vue-scoped-css/rules/require-scoped.html", + suggestion: true, }, fixable: null, messages: { missing: "Missing `scoped` attribute.", + add: "Add `scoped` attribute.", }, schema: [], type: "suggestion", @@ -24,6 +26,7 @@ module.exports = { return {} } const reporter = getCommentDirectivesReporter(context) + const tokenStore = context.parserServices.getTemplateBodyTokenStore?.() as TokenStore /** * Reports the given node. @@ -34,6 +37,20 @@ module.exports = { node: node.startTag, messageId: "missing", data: {}, + suggest: [ + { + messageId: "add", + fix(fixer) { + const close = tokenStore.getLastToken(node.startTag) + return fixer.insertTextBefore( + // eslint-disable-next-line @mysticatea/ts/ban-ts-ignore, spaced-comment + /// @ts-ignore + close, + " scoped", + ) + }, + }, + ], }) } diff --git a/lib/styles/context/comment-directive/index.ts b/lib/styles/context/comment-directive/index.ts index 6e27073d..72611927 100644 --- a/lib/styles/context/comment-directive/index.ts +++ b/lib/styles/context/comment-directive/index.ts @@ -3,6 +3,7 @@ import { ReportDescriptor, RuleContext, SourceLocation, + ReportDescriptorSourceLocation, } from "../../../types" import { StyleContext } from "../style" import { VCSSCommentNode } from "../../ast" @@ -208,7 +209,9 @@ export class CommentDirectives { * @returns {boolean} `true` if rule is enabled */ public isEnabled(rule: string, descriptor: ReportDescriptor) { - const loc = descriptor.loc || (descriptor.node && descriptor.node.loc) + const loc = hasSourceLocation(descriptor) + ? descriptor.loc + : descriptor.node?.loc if (!loc) { return false } @@ -316,3 +319,12 @@ function compareLoc(a: LineAndColumnData, b: LineAndColumnData) { } return compare(a.column, b.column) } + +/** + * Checks whether the given descriptor has loc property + */ +function hasSourceLocation( + descriptor: ReportDescriptor, +): descriptor is ReportDescriptor & ReportDescriptorSourceLocation { + return (descriptor as ReportDescriptorSourceLocation).loc != null +} diff --git a/lib/types.ts b/lib/types.ts index 0fe648a0..ecc9c55b 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -4,6 +4,7 @@ import postcss from "postcss" import selectorParser from "postcss-selector-parser" // eslint-disable-next-line @mysticatea/node/no-extraneous-import import { ScopeManager } from "eslint-scope" +import { Rule } from "eslint" export { AST } @@ -13,11 +14,12 @@ export type Rule = { docs: { description: string category: string - ruleId: string - ruleName: string + ruleId?: string + ruleName?: string default?: string replacedBy?: string[] url: string + suggestion?: true } deprecated?: boolean fixable?: "code" | "whitespace" | null @@ -64,7 +66,7 @@ interface ParserServices { * Get the token store of the template body. * @returns The token store of template body. */ - // getTemplateBodyTokenStore(): TokenStore + getTemplateBodyTokenStore?: () => TokenStore /** * Get the root document fragment. @@ -80,13 +82,45 @@ export interface RuleContext { getFilename: () => string parserServices: ParserServices } -export type ReportDescriptor = { - loc?: SourceLocation | { line: number; column: number } - node?: AST.HasLocation - messageId?: string - message?: string - data?: { [key: string]: any } + +export type ReportSuggestion = ({ messageId: string } | { desc: string }) & { + fix?(fixer: Rule.RuleFixer): null | Rule.Fix | IterableIterator +} +export type ReportDescriptorNodeLocation = { node: AST.HasLocation } +export type ReportDescriptorSourceLocation = { + loc: SourceLocation | { line: number; column: number } } + +export type ReportDescriptorLocation = + | ReportDescriptorNodeLocation + | ReportDescriptorSourceLocation + +export type ReportDescriptor = ReportDescriptorLocation & + Rule.ReportDescriptorOptions & + Rule.ReportDescriptorMessage & { + suggest?: ReportSuggestion[] + } + +type FilterPredicate = (tokenOrComment: AST.Token) => boolean + +type CursorWithSkipOptions = + | number + | FilterPredicate + | { + includeComments?: boolean + filter?: FilterPredicate + skip?: number + } + +// type CursorWithCountOptions = +// | number +// | FilterPredicate +// | { +// includeComments?: boolean +// filter?: FilterPredicate +// count?: number +// } + export interface SourceCode { text: string ast: AST.ESLintProgram @@ -105,6 +139,26 @@ export interface SourceCode { getLocFromIndex(index: number): LineAndColumnData getIndexFromLoc(location: LineAndColumnData): number + + getFirstToken( + node: AST.Node, + options?: CursorWithSkipOptions, + ): AST.Token | null +} +export interface TokenStore { + getFirstToken( + node: AST.Node, + options?: CursorWithSkipOptions, + ): AST.Token | null + getLastToken( + node: AST.Node, + options?: CursorWithSkipOptions, + ): AST.Token | null + getTokens( + node: AST.Node, + beforeCount?: number, + afterCount?: number, + ): AST.Token[] } type HasPostCSSSource = { source: postcss.NodeSource diff --git a/lib/utils/rules.ts b/lib/utils/rules.ts index 81a4564a..87f35038 100644 --- a/lib/utils/rules.ts +++ b/lib/utils/rules.ts @@ -46,7 +46,7 @@ export function collectRules(category?: string): { [key: string]: string } { (!category || rule.meta.docs.category === category) && !rule.meta.deprecated ) { - obj[rule.meta.docs.ruleId] = rule.meta.docs.default || "error" + obj[rule.meta.docs.ruleId || ""] = rule.meta.docs.default || "error" } return obj }, {} as { [key: string]: string }) diff --git a/tests/lib/rules/require-scoped.ts b/tests/lib/rules/require-scoped.ts index ab6e97de..0d0236a5 100644 --- a/tests/lib/rules/require-scoped.ts +++ b/tests/lib/rules/require-scoped.ts @@ -37,6 +37,47 @@ tester.run("require-scoped", rule, { column: 13, endLine: 4, endColumn: 20, + // eslint-disable-next-line @mysticatea/ts/ban-ts-ignore, spaced-comment + /// @ts-ignore + suggestions: [ + { + desc: "Add `scoped` attribute.", + output: ` + + + `, + }, + ], + }, + ], + }, + { + code: ` + +