Skip to content
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
2 changes: 1 addition & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })

Expand Down
19 changes: 18 additions & 1 deletion lib/rules/require-scoped.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getStyleContexts, getCommentDirectivesReporter } from "../styles"
import { RuleContext, AST } from "../types"
import { RuleContext, AST, TokenStore } from "../types"

module.exports = {
meta: {
Expand All @@ -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",
Expand All @@ -24,6 +26,7 @@ module.exports = {
return {}
}
const reporter = getCommentDirectivesReporter(context)
const tokenStore = context.parserServices.getTemplateBodyTokenStore?.() as TokenStore

/**
* Reports the given node.
Expand All @@ -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",
)
},
},
],
})
}

Expand Down
14 changes: 13 additions & 1 deletion lib/styles/context/comment-directive/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ReportDescriptor,
RuleContext,
SourceLocation,
ReportDescriptorSourceLocation,
} from "../../../types"
import { StyleContext } from "../style"
import { VCSSCommentNode } from "../../ast"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
72 changes: 63 additions & 9 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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<Rule.Fix>
}
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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/utils/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down
41 changes: 41 additions & 0 deletions tests/lib/rules/require-scoped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: `
<template>
</template>
<style scoped>
</style>
`,
},
],
},
],
},
{
code: `
<template>
</template>
<style />
`,
errors: [
{
messageId: "missing",
line: 4,
column: 13,
endLine: 4,
endColumn: 22,
// eslint-disable-next-line @mysticatea/ts/ban-ts-ignore, spaced-comment
/// @ts-ignore
suggestions: [
{
desc: "Add `scoped` attribute.",
output: `
<template>
</template>
<style scoped/>
`,
},
],
},
],
},
Expand Down
4 changes: 2 additions & 2 deletions tests/lib/utils/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,14 @@ describe("Check if the struct of all rules is correct", () => {
})

for (const rule of allRules) {
it(rule.meta.docs.ruleId, () => {
it(rule.meta.docs?.ruleId || "", () => {
assert.ok(Boolean(rule.meta.docs.ruleId), "Did not set `ruleId`")
assert.ok(
Boolean(rule.meta.docs.ruleName),
"Did not set `ruleName`",
)
assert.ok(
Boolean(dirRules[rule.meta.docs.ruleId]),
Boolean(dirRules[rule.meta.docs?.ruleId || ""]),
"Did not exist rule",
)
})
Expand Down