Skip to content

Commit a6e768a

Browse files
authored
Added suggestion to require-scoped (#17)
1 parent a875531 commit a6e768a

File tree

7 files changed

+139
-15
lines changed

7 files changed

+139
-15
lines changed

lib/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const configs = {
88
}
99

1010
const rules = ruleList.reduce((obj, r) => {
11-
obj[r.meta.docs.ruleName] = r
11+
obj[r.meta.docs?.ruleName || ""] = r
1212
return obj
1313
}, {} as { [key: string]: Rule })
1414

lib/rules/require-scoped.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getStyleContexts, getCommentDirectivesReporter } from "../styles"
2-
import { RuleContext, AST } from "../types"
2+
import { RuleContext, AST, TokenStore } from "../types"
33

44
module.exports = {
55
meta: {
@@ -10,10 +10,12 @@ module.exports = {
1010
default: "warn",
1111
url:
1212
"https://future-architect.github.io/eslint-plugin-vue-scoped-css/rules/require-scoped.html",
13+
suggestion: true,
1314
},
1415
fixable: null,
1516
messages: {
1617
missing: "Missing `scoped` attribute.",
18+
add: "Add `scoped` attribute.",
1719
},
1820
schema: [],
1921
type: "suggestion",
@@ -24,6 +26,7 @@ module.exports = {
2426
return {}
2527
}
2628
const reporter = getCommentDirectivesReporter(context)
29+
const tokenStore = context.parserServices.getTemplateBodyTokenStore?.() as TokenStore
2730

2831
/**
2932
* Reports the given node.
@@ -34,6 +37,20 @@ module.exports = {
3437
node: node.startTag,
3538
messageId: "missing",
3639
data: {},
40+
suggest: [
41+
{
42+
messageId: "add",
43+
fix(fixer) {
44+
const close = tokenStore.getLastToken(node.startTag)
45+
return fixer.insertTextBefore(
46+
// eslint-disable-next-line @mysticatea/ts/ban-ts-ignore, spaced-comment
47+
/// @ts-ignore
48+
close,
49+
" scoped",
50+
)
51+
},
52+
},
53+
],
3754
})
3855
}
3956

lib/styles/context/comment-directive/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
ReportDescriptor,
44
RuleContext,
55
SourceLocation,
6+
ReportDescriptorSourceLocation,
67
} from "../../../types"
78
import { StyleContext } from "../style"
89
import { VCSSCommentNode } from "../../ast"
@@ -208,7 +209,9 @@ export class CommentDirectives {
208209
* @returns {boolean} `true` if rule is enabled
209210
*/
210211
public isEnabled(rule: string, descriptor: ReportDescriptor) {
211-
const loc = descriptor.loc || (descriptor.node && descriptor.node.loc)
212+
const loc = hasSourceLocation(descriptor)
213+
? descriptor.loc
214+
: descriptor.node?.loc
212215
if (!loc) {
213216
return false
214217
}
@@ -316,3 +319,12 @@ function compareLoc(a: LineAndColumnData, b: LineAndColumnData) {
316319
}
317320
return compare(a.column, b.column)
318321
}
322+
323+
/**
324+
* Checks whether the given descriptor has loc property
325+
*/
326+
function hasSourceLocation(
327+
descriptor: ReportDescriptor,
328+
): descriptor is ReportDescriptor & ReportDescriptorSourceLocation {
329+
return (descriptor as ReportDescriptorSourceLocation).loc != null
330+
}

lib/types.ts

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import postcss from "postcss"
44
import selectorParser from "postcss-selector-parser"
55
// eslint-disable-next-line @mysticatea/node/no-extraneous-import
66
import { ScopeManager } from "eslint-scope"
7+
import { Rule } from "eslint"
78

89
export { AST }
910

@@ -13,11 +14,12 @@ export type Rule = {
1314
docs: {
1415
description: string
1516
category: string
16-
ruleId: string
17-
ruleName: string
17+
ruleId?: string
18+
ruleName?: string
1819
default?: string
1920
replacedBy?: string[]
2021
url: string
22+
suggestion?: true
2123
}
2224
deprecated?: boolean
2325
fixable?: "code" | "whitespace" | null
@@ -64,7 +66,7 @@ interface ParserServices {
6466
* Get the token store of the template body.
6567
* @returns The token store of template body.
6668
*/
67-
// getTemplateBodyTokenStore(): TokenStore
69+
getTemplateBodyTokenStore?: () => TokenStore
6870

6971
/**
7072
* Get the root document fragment.
@@ -80,13 +82,45 @@ export interface RuleContext {
8082
getFilename: () => string
8183
parserServices: ParserServices
8284
}
83-
export type ReportDescriptor = {
84-
loc?: SourceLocation | { line: number; column: number }
85-
node?: AST.HasLocation
86-
messageId?: string
87-
message?: string
88-
data?: { [key: string]: any }
85+
86+
export type ReportSuggestion = ({ messageId: string } | { desc: string }) & {
87+
fix?(fixer: Rule.RuleFixer): null | Rule.Fix | IterableIterator<Rule.Fix>
88+
}
89+
export type ReportDescriptorNodeLocation = { node: AST.HasLocation }
90+
export type ReportDescriptorSourceLocation = {
91+
loc: SourceLocation | { line: number; column: number }
8992
}
93+
94+
export type ReportDescriptorLocation =
95+
| ReportDescriptorNodeLocation
96+
| ReportDescriptorSourceLocation
97+
98+
export type ReportDescriptor = ReportDescriptorLocation &
99+
Rule.ReportDescriptorOptions &
100+
Rule.ReportDescriptorMessage & {
101+
suggest?: ReportSuggestion[]
102+
}
103+
104+
type FilterPredicate = (tokenOrComment: AST.Token) => boolean
105+
106+
type CursorWithSkipOptions =
107+
| number
108+
| FilterPredicate
109+
| {
110+
includeComments?: boolean
111+
filter?: FilterPredicate
112+
skip?: number
113+
}
114+
115+
// type CursorWithCountOptions =
116+
// | number
117+
// | FilterPredicate
118+
// | {
119+
// includeComments?: boolean
120+
// filter?: FilterPredicate
121+
// count?: number
122+
// }
123+
90124
export interface SourceCode {
91125
text: string
92126
ast: AST.ESLintProgram
@@ -105,6 +139,26 @@ export interface SourceCode {
105139
getLocFromIndex(index: number): LineAndColumnData
106140

107141
getIndexFromLoc(location: LineAndColumnData): number
142+
143+
getFirstToken(
144+
node: AST.Node,
145+
options?: CursorWithSkipOptions,
146+
): AST.Token | null
147+
}
148+
export interface TokenStore {
149+
getFirstToken(
150+
node: AST.Node,
151+
options?: CursorWithSkipOptions,
152+
): AST.Token | null
153+
getLastToken(
154+
node: AST.Node,
155+
options?: CursorWithSkipOptions,
156+
): AST.Token | null
157+
getTokens(
158+
node: AST.Node,
159+
beforeCount?: number,
160+
afterCount?: number,
161+
): AST.Token[]
108162
}
109163
type HasPostCSSSource = {
110164
source: postcss.NodeSource

lib/utils/rules.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export function collectRules(category?: string): { [key: string]: string } {
4646
(!category || rule.meta.docs.category === category) &&
4747
!rule.meta.deprecated
4848
) {
49-
obj[rule.meta.docs.ruleId] = rule.meta.docs.default || "error"
49+
obj[rule.meta.docs.ruleId || ""] = rule.meta.docs.default || "error"
5050
}
5151
return obj
5252
}, {} as { [key: string]: string })

tests/lib/rules/require-scoped.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,47 @@ tester.run("require-scoped", rule, {
3737
column: 13,
3838
endLine: 4,
3939
endColumn: 20,
40+
// eslint-disable-next-line @mysticatea/ts/ban-ts-ignore, spaced-comment
41+
/// @ts-ignore
42+
suggestions: [
43+
{
44+
desc: "Add `scoped` attribute.",
45+
output: `
46+
<template>
47+
</template>
48+
<style scoped>
49+
</style>
50+
`,
51+
},
52+
],
53+
},
54+
],
55+
},
56+
{
57+
code: `
58+
<template>
59+
</template>
60+
<style />
61+
`,
62+
errors: [
63+
{
64+
messageId: "missing",
65+
line: 4,
66+
column: 13,
67+
endLine: 4,
68+
endColumn: 22,
69+
// eslint-disable-next-line @mysticatea/ts/ban-ts-ignore, spaced-comment
70+
/// @ts-ignore
71+
suggestions: [
72+
{
73+
desc: "Add `scoped` attribute.",
74+
output: `
75+
<template>
76+
</template>
77+
<style scoped/>
78+
`,
79+
},
80+
],
4081
},
4182
],
4283
},

tests/lib/utils/rules.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,14 @@ describe("Check if the struct of all rules is correct", () => {
4343
})
4444

4545
for (const rule of allRules) {
46-
it(rule.meta.docs.ruleId, () => {
46+
it(rule.meta.docs?.ruleId || "", () => {
4747
assert.ok(Boolean(rule.meta.docs.ruleId), "Did not set `ruleId`")
4848
assert.ok(
4949
Boolean(rule.meta.docs.ruleName),
5050
"Did not set `ruleName`",
5151
)
5252
assert.ok(
53-
Boolean(dirRules[rule.meta.docs.ruleId]),
53+
Boolean(dirRules[rule.meta.docs?.ruleId || ""]),
5454
"Did not exist rule",
5555
)
5656
})

0 commit comments

Comments
 (0)