Skip to content

Commit 7a5a180

Browse files
authored
Add attributes option to no-raw-text rule (#4)
* Add `attributes` option to `no-raw-text` rule * fix
1 parent acf4b24 commit 7a5a180

File tree

5 files changed

+275
-52
lines changed

5 files changed

+275
-52
lines changed

docs/rules/no-raw-text.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@ This rule encourage i18n in about the application needs to be localized.
5555
"@intlify/svelte/no-raw-text": [
5656
"error",
5757
{
58+
"attributes": {
59+
"/.+/": [
60+
"title",
61+
"aria-label",
62+
"aria-placeholder",
63+
"aria-roledescription",
64+
"aria-valuetext"
65+
],
66+
"input": ["placeholder"],
67+
"img": ["alt"]
68+
},
5869
"ignoreNodes": ["md-icon", "v-icon"],
5970
"ignorePattern": "^[-#:()&]+$",
6071
"ignoreText": ["EUR", "HKD", "USD"]
@@ -63,10 +74,44 @@ This rule encourage i18n in about the application needs to be localized.
6374
}
6475
```
6576

77+
- `attributes`: An object whose keys are tag name or patterns and value is an array of attributes to check for that tag name. Default empty.
6678
- `ignoreNodes`: specify nodes to ignore such as icon components
6779
- `ignorePattern`: specify a regexp pattern that matches strings to ignore
6880
- `ignoreText`: specify an array of strings to ignore
6981

82+
### `attributes`
83+
84+
<eslint-code-block>
85+
86+
<!-- eslint-skip -->
87+
88+
```svelte
89+
<script>
90+
/* eslint @intlify/svelte/no-raw-text: ['error', {attributes: { '/.+/': ['label'] }}] */
91+
</script>
92+
<!-- ✗ BAD -->
93+
<MyInput label="hello" />
94+
<AnyComponent label="hello" />
95+
```
96+
97+
</eslint-code-block>
98+
99+
<eslint-code-block>
100+
101+
<!-- eslint-skip -->
102+
103+
```svelte
104+
<script>
105+
/* eslint @intlify/svelte/no-raw-text: ['error', {attributes: { 'MyInput': ['label'] }}] */
106+
</script>
107+
<!-- ✗ BAD -->
108+
<MyInput label="hello" />
109+
<!-- ✓ GOOD -->
110+
<OtherComponent label="hello" />
111+
```
112+
113+
</eslint-code-block>
114+
70115
## :rocket: Version
71116

72117
This rule was introduced in `@intlify/eslint-plugin-svelte` v0.0.1

lib/rules/no-raw-text.ts

Lines changed: 152 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,63 @@ import type ESTree from 'estree'
66
import type { RuleContext, RuleListener } from '../types'
77
import { defineRule } from '../utils'
88

9-
type AnyValue = ESTree.Literal['value']
9+
type LiteralValue = ESTree.Literal['value']
10+
type StaticTemplateLiteral = ESTree.TemplateLiteral & {
11+
quasis: [ESTree.TemplateElement]
12+
expressions: [/* empty */]
13+
}
14+
type TargetAttrs = { name: RegExp; attrs: Set<string> }
1015
type Config = {
16+
attributes: TargetAttrs[]
1117
ignorePattern: RegExp
1218
ignoreNodes: string[]
1319
ignoreText: string[]
1420
}
21+
const RE_REGEXP_STR = /^\/(.+)\/(.*)$/u
22+
function toRegExp(str: string): RegExp {
23+
const parts = RE_REGEXP_STR.exec(str)
24+
if (parts) {
25+
return new RegExp(parts[1], parts[2])
26+
}
27+
return new RegExp(`^${escape(str)}$`)
28+
}
1529
const hasOnlyWhitespace = (value: string) => /^[\r\n\s\t\f\v]+$/.test(value)
1630

17-
function isValidValue(value: AnyValue, config: Config) {
18-
return (
19-
typeof value !== 'string' ||
20-
hasOnlyWhitespace(value) ||
21-
config.ignorePattern.test(value.trim()) ||
22-
config.ignoreText.includes(value.trim())
31+
/**
32+
* Get the attribute to be verified from the element name.
33+
*/
34+
function getTargetAttrs(tagName: string, config: Config): Set<string> {
35+
const result = []
36+
for (const { name, attrs } of config.attributes) {
37+
name.lastIndex = 0
38+
if (name.test(tagName)) {
39+
result.push(...attrs)
40+
}
41+
}
42+
43+
return new Set(result)
44+
}
45+
46+
function isStaticTemplateLiteral(
47+
node: ESTree.Expression | ESTree.Pattern
48+
): node is StaticTemplateLiteral {
49+
return Boolean(
50+
node && node.type === 'TemplateLiteral' && node.expressions.length === 0
2351
)
2452
}
2553

54+
function testValue(value: LiteralValue, config: Config): boolean {
55+
if (typeof value === 'string') {
56+
return (
57+
hasOnlyWhitespace(value) ||
58+
config.ignorePattern.test(value.trim()) ||
59+
config.ignoreText.includes(value.trim())
60+
)
61+
} else {
62+
return false
63+
}
64+
}
65+
2666
function checkSvelteMustacheTagText(
2767
context: RuleContext,
2868
node: SvAST.SvelteMustacheTag,
@@ -34,56 +74,106 @@ function checkSvelteMustacheTagText(
3474

3575
if (node.parent.type === 'SvelteElement') {
3676
// parent is element (e.g. <p>{ ... }</p>)
37-
if (node.expression.type === 'Literal') {
38-
const literalNode = node.expression
39-
if (isValidValue(literalNode.value, config)) {
40-
return
77+
checkExpressionText(context, node.expression, config)
78+
}
79+
}
80+
81+
function checkExpressionText(
82+
context: RuleContext,
83+
expression: ESTree.Expression,
84+
config: Config
85+
) {
86+
if (expression.type === 'Literal') {
87+
checkLiteral(context, expression, config)
88+
} else if (isStaticTemplateLiteral(expression)) {
89+
checkLiteral(context, expression, config)
90+
} else if (expression.type === 'ConditionalExpression') {
91+
const targets = [expression.consequent, expression.alternate]
92+
targets.forEach(target => {
93+
if (target.type === 'Literal') {
94+
checkLiteral(context, target, config)
95+
} else if (isStaticTemplateLiteral(target)) {
96+
checkLiteral(context, target, config)
4197
}
98+
})
99+
}
100+
}
42101

43-
context.report({
44-
node: literalNode,
45-
message: `raw text '${literalNode.value}' is used`
46-
})
47-
} else if (node.expression.type === 'ConditionalExpression') {
48-
for (const target of [
49-
node.expression.consequent,
50-
node.expression.alternate
51-
]) {
52-
if (target.type !== 'Literal') {
53-
continue
54-
}
55-
if (isValidValue(target.value, config)) {
56-
continue
57-
}
102+
function checkSvelteLiteralOrText(
103+
context: RuleContext,
104+
literal: SvAST.SvelteLiteral | SvAST.SvelteText,
105+
config: Config
106+
) {
107+
if (testValue(literal.value, config)) {
108+
return
109+
}
58110

59-
context.report({
60-
node: target,
61-
message: `raw text '${target.value}' is used`
62-
})
63-
}
64-
}
111+
const loc = literal.loc!
112+
context.report({
113+
loc,
114+
message: `raw text '${literal.value}' is used`
115+
})
116+
}
117+
118+
function checkLiteral(
119+
context: RuleContext,
120+
literal: ESTree.Literal | StaticTemplateLiteral,
121+
config: Config
122+
) {
123+
const value =
124+
literal.type !== 'TemplateLiteral'
125+
? literal.value
126+
: literal.quasis[0].value.cooked
127+
128+
if (testValue(value, config)) {
129+
return
130+
}
131+
132+
const loc = literal.loc!
133+
context.report({
134+
loc,
135+
message: `raw text '${value}' is used`
136+
})
137+
}
138+
/**
139+
* Parse attributes option
140+
*/
141+
function parseTargetAttrs(
142+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
143+
options: any
144+
) {
145+
const regexps: TargetAttrs[] = []
146+
for (const tagName of Object.keys(options)) {
147+
const attrs: Set<string> = new Set(options[tagName])
148+
regexps.push({
149+
name: toRegExp(tagName),
150+
attrs
151+
})
65152
}
153+
return regexps
66154
}
67155

68156
function create(context: RuleContext): RuleListener {
69157
const sourceCode = context.getSourceCode()
70158
const config: Config = {
71-
ignorePattern: /^[^\S\s]$/,
159+
attributes: [],
160+
ignorePattern: /^$/,
72161
ignoreNodes: [],
73162
ignoreText: []
74163
}
75164

76165
if (context.options[0]?.ignorePattern) {
77166
config.ignorePattern = new RegExp(context.options[0].ignorePattern, 'u')
78167
}
79-
80168
if (context.options[0]?.ignoreNodes) {
81169
config.ignoreNodes = context.options[0].ignoreNodes
82170
}
83-
84171
if (context.options[0]?.ignoreText) {
85172
config.ignoreText = context.options[0].ignoreText
86173
}
174+
if (context.options[0]?.attributes) {
175+
config.attributes = parseTargetAttrs(context.options[0].attributes)
176+
}
87177

88178
function isIgnore(node: SvAST.SvelteMustacheTag | SvAST.SvelteText) {
89179
const element = getElement(node)
@@ -98,7 +188,8 @@ function create(context: RuleContext): RuleListener {
98188
| SvAST.SvelteText['parent']
99189
| SvAST.SvelteMustacheTag['parent']
100190
| SvAST.SvelteElement
101-
| SvAST.SvelteAwaitBlock = node.parent
191+
| SvAST.SvelteAwaitBlock
192+
| SvAST.SvelteElseBlockElseIf = node.parent
102193
while (
103194
target.type === 'SvelteIfBlock' ||
104195
target.type === 'SvelteElseBlock' ||
@@ -118,6 +209,19 @@ function create(context: RuleContext): RuleListener {
118209
}
119210

120211
return {
212+
SvelteAttribute(node: SvAST.SvelteAttribute) {
213+
if (node.value.length !== 1 || node.value[0].type !== 'SvelteLiteral') {
214+
return
215+
}
216+
const nameNode = node.parent.parent.name
217+
const tagName = sourceCode.text.slice(...nameNode.range!)
218+
const attrName = node.key.name
219+
if (!getTargetAttrs(tagName, config).has(attrName)) {
220+
return
221+
}
222+
223+
checkSvelteLiteralOrText(context, node.value[0], config)
224+
},
121225
SvelteMustacheTag(node: SvAST.SvelteMustacheTag) {
122226
if (isIgnore(node)) {
123227
return
@@ -129,15 +233,7 @@ function create(context: RuleContext): RuleListener {
129233
if (isIgnore(node)) {
130234
return
131235
}
132-
133-
if (isValidValue(node.value, config)) {
134-
return
135-
}
136-
137-
context.report({
138-
node,
139-
message: `raw text '${node.value}' is used`
140-
})
236+
checkSvelteLiteralOrText(context, node, config)
141237
}
142238
}
143239
}
@@ -154,6 +250,17 @@ export = defineRule('no-raw-text', {
154250
{
155251
type: 'object',
156252
properties: {
253+
attributes: {
254+
type: 'object',
255+
patternProperties: {
256+
'^(?:\\S+|/.*/[a-z]*)$': {
257+
type: 'array',
258+
items: { type: 'string' },
259+
uniqueItems: true
260+
}
261+
},
262+
additionalProperties: false
263+
},
157264
ignoreNodes: {
158265
type: 'array'
159266
},

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
},
2626
"dependencies": {
2727
"debug": "^4.3.1",
28-
"svelte-eslint-parser": "^0.4.1"
28+
"svelte-eslint-parser": "^0.8.0"
2929
},
3030
"peerDependencies": {
3131
"eslint": "^7.0.0 || ^8.0.0-0",
@@ -94,6 +94,7 @@
9494
"generate": "ts-node --transpile-only scripts/update.ts && prettier . --write",
9595
"lint": "eslint . --ext js,ts,vue,md --ignore-pattern \"/tests/fixtures\"",
9696
"lint:docs": "prettier docs --check",
97+
"format:docs": "prettier docs --write",
9798
"release:prepare": "shipjs prepare",
9899
"release:trigger": "shipjs trigger",
99100
"test": "mocha --require ts-node/register \"./tests/**/*.ts\"",

0 commit comments

Comments
 (0)