Skip to content

Commit

Permalink
Add attributes option to no-raw-text rule (#4)
Browse files Browse the repository at this point in the history
* Add `attributes` option to `no-raw-text` rule

* fix
  • Loading branch information
ota-meshi committed Nov 30, 2021
1 parent acf4b24 commit 7a5a180
Show file tree
Hide file tree
Showing 5 changed files with 275 additions and 52 deletions.
45 changes: 45 additions & 0 deletions docs/rules/no-raw-text.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ This rule encourage i18n in about the application needs to be localized.
"@intlify/svelte/no-raw-text": [
"error",
{
"attributes": {
"/.+/": [
"title",
"aria-label",
"aria-placeholder",
"aria-roledescription",
"aria-valuetext"
],
"input": ["placeholder"],
"img": ["alt"]
},
"ignoreNodes": ["md-icon", "v-icon"],
"ignorePattern": "^[-#:()&]+$",
"ignoreText": ["EUR", "HKD", "USD"]
Expand All @@ -63,10 +74,44 @@ This rule encourage i18n in about the application needs to be localized.
}
```

- `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.
- `ignoreNodes`: specify nodes to ignore such as icon components
- `ignorePattern`: specify a regexp pattern that matches strings to ignore
- `ignoreText`: specify an array of strings to ignore

### `attributes`

<eslint-code-block>

<!-- eslint-skip -->

```svelte
<script>
/* eslint @intlify/svelte/no-raw-text: ['error', {attributes: { '/.+/': ['label'] }}] */
</script>
<!-- ✗ BAD -->
<MyInput label="hello" />
<AnyComponent label="hello" />
```

</eslint-code-block>

<eslint-code-block>

<!-- eslint-skip -->

```svelte
<script>
/* eslint @intlify/svelte/no-raw-text: ['error', {attributes: { 'MyInput': ['label'] }}] */
</script>
<!-- ✗ BAD -->
<MyInput label="hello" />
<!-- ✓ GOOD -->
<OtherComponent label="hello" />
```

</eslint-code-block>

## :rocket: Version

This rule was introduced in `@intlify/eslint-plugin-svelte` v0.0.1
Expand Down
197 changes: 152 additions & 45 deletions lib/rules/no-raw-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,63 @@ import type ESTree from 'estree'
import type { RuleContext, RuleListener } from '../types'
import { defineRule } from '../utils'

type AnyValue = ESTree.Literal['value']
type LiteralValue = ESTree.Literal['value']
type StaticTemplateLiteral = ESTree.TemplateLiteral & {
quasis: [ESTree.TemplateElement]
expressions: [/* empty */]
}
type TargetAttrs = { name: RegExp; attrs: Set<string> }
type Config = {
attributes: TargetAttrs[]
ignorePattern: RegExp
ignoreNodes: string[]
ignoreText: string[]
}
const RE_REGEXP_STR = /^\/(.+)\/(.*)$/u
function toRegExp(str: string): RegExp {
const parts = RE_REGEXP_STR.exec(str)
if (parts) {
return new RegExp(parts[1], parts[2])
}
return new RegExp(`^${escape(str)}$`)
}
const hasOnlyWhitespace = (value: string) => /^[\r\n\s\t\f\v]+$/.test(value)

function isValidValue(value: AnyValue, config: Config) {
return (
typeof value !== 'string' ||
hasOnlyWhitespace(value) ||
config.ignorePattern.test(value.trim()) ||
config.ignoreText.includes(value.trim())
/**
* Get the attribute to be verified from the element name.
*/
function getTargetAttrs(tagName: string, config: Config): Set<string> {
const result = []
for (const { name, attrs } of config.attributes) {
name.lastIndex = 0
if (name.test(tagName)) {
result.push(...attrs)
}
}

return new Set(result)
}

function isStaticTemplateLiteral(
node: ESTree.Expression | ESTree.Pattern
): node is StaticTemplateLiteral {
return Boolean(
node && node.type === 'TemplateLiteral' && node.expressions.length === 0
)
}

function testValue(value: LiteralValue, config: Config): boolean {
if (typeof value === 'string') {
return (
hasOnlyWhitespace(value) ||
config.ignorePattern.test(value.trim()) ||
config.ignoreText.includes(value.trim())
)
} else {
return false
}
}

function checkSvelteMustacheTagText(
context: RuleContext,
node: SvAST.SvelteMustacheTag,
Expand All @@ -34,56 +74,106 @@ function checkSvelteMustacheTagText(

if (node.parent.type === 'SvelteElement') {
// parent is element (e.g. <p>{ ... }</p>)
if (node.expression.type === 'Literal') {
const literalNode = node.expression
if (isValidValue(literalNode.value, config)) {
return
checkExpressionText(context, node.expression, config)
}
}

function checkExpressionText(
context: RuleContext,
expression: ESTree.Expression,
config: Config
) {
if (expression.type === 'Literal') {
checkLiteral(context, expression, config)
} else if (isStaticTemplateLiteral(expression)) {
checkLiteral(context, expression, config)
} else if (expression.type === 'ConditionalExpression') {
const targets = [expression.consequent, expression.alternate]
targets.forEach(target => {
if (target.type === 'Literal') {
checkLiteral(context, target, config)
} else if (isStaticTemplateLiteral(target)) {
checkLiteral(context, target, config)
}
})
}
}

context.report({
node: literalNode,
message: `raw text '${literalNode.value}' is used`
})
} else if (node.expression.type === 'ConditionalExpression') {
for (const target of [
node.expression.consequent,
node.expression.alternate
]) {
if (target.type !== 'Literal') {
continue
}
if (isValidValue(target.value, config)) {
continue
}
function checkSvelteLiteralOrText(
context: RuleContext,
literal: SvAST.SvelteLiteral | SvAST.SvelteText,
config: Config
) {
if (testValue(literal.value, config)) {
return
}

context.report({
node: target,
message: `raw text '${target.value}' is used`
})
}
}
const loc = literal.loc!
context.report({
loc,
message: `raw text '${literal.value}' is used`
})
}

function checkLiteral(
context: RuleContext,
literal: ESTree.Literal | StaticTemplateLiteral,
config: Config
) {
const value =
literal.type !== 'TemplateLiteral'
? literal.value
: literal.quasis[0].value.cooked

if (testValue(value, config)) {
return
}

const loc = literal.loc!
context.report({
loc,
message: `raw text '${value}' is used`
})
}
/**
* Parse attributes option
*/
function parseTargetAttrs(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
options: any
) {
const regexps: TargetAttrs[] = []
for (const tagName of Object.keys(options)) {
const attrs: Set<string> = new Set(options[tagName])
regexps.push({
name: toRegExp(tagName),
attrs
})
}
return regexps
}

function create(context: RuleContext): RuleListener {
const sourceCode = context.getSourceCode()
const config: Config = {
ignorePattern: /^[^\S\s]$/,
attributes: [],
ignorePattern: /^$/,
ignoreNodes: [],
ignoreText: []
}

if (context.options[0]?.ignorePattern) {
config.ignorePattern = new RegExp(context.options[0].ignorePattern, 'u')
}

if (context.options[0]?.ignoreNodes) {
config.ignoreNodes = context.options[0].ignoreNodes
}

if (context.options[0]?.ignoreText) {
config.ignoreText = context.options[0].ignoreText
}
if (context.options[0]?.attributes) {
config.attributes = parseTargetAttrs(context.options[0].attributes)
}

function isIgnore(node: SvAST.SvelteMustacheTag | SvAST.SvelteText) {
const element = getElement(node)
Expand All @@ -98,7 +188,8 @@ function create(context: RuleContext): RuleListener {
| SvAST.SvelteText['parent']
| SvAST.SvelteMustacheTag['parent']
| SvAST.SvelteElement
| SvAST.SvelteAwaitBlock = node.parent
| SvAST.SvelteAwaitBlock
| SvAST.SvelteElseBlockElseIf = node.parent
while (
target.type === 'SvelteIfBlock' ||
target.type === 'SvelteElseBlock' ||
Expand All @@ -118,6 +209,19 @@ function create(context: RuleContext): RuleListener {
}

return {
SvelteAttribute(node: SvAST.SvelteAttribute) {
if (node.value.length !== 1 || node.value[0].type !== 'SvelteLiteral') {
return
}
const nameNode = node.parent.parent.name
const tagName = sourceCode.text.slice(...nameNode.range!)
const attrName = node.key.name
if (!getTargetAttrs(tagName, config).has(attrName)) {
return
}

checkSvelteLiteralOrText(context, node.value[0], config)
},
SvelteMustacheTag(node: SvAST.SvelteMustacheTag) {
if (isIgnore(node)) {
return
Expand All @@ -129,15 +233,7 @@ function create(context: RuleContext): RuleListener {
if (isIgnore(node)) {
return
}

if (isValidValue(node.value, config)) {
return
}

context.report({
node,
message: `raw text '${node.value}' is used`
})
checkSvelteLiteralOrText(context, node, config)
}
}
}
Expand All @@ -154,6 +250,17 @@ export = defineRule('no-raw-text', {
{
type: 'object',
properties: {
attributes: {
type: 'object',
patternProperties: {
'^(?:\\S+|/.*/[a-z]*)$': {
type: 'array',
items: { type: 'string' },
uniqueItems: true
}
},
additionalProperties: false
},
ignoreNodes: {
type: 'array'
},
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
},
"dependencies": {
"debug": "^4.3.1",
"svelte-eslint-parser": "^0.4.1"
"svelte-eslint-parser": "^0.8.0"
},
"peerDependencies": {
"eslint": "^7.0.0 || ^8.0.0-0",
Expand Down Expand Up @@ -94,6 +94,7 @@
"generate": "ts-node --transpile-only scripts/update.ts && prettier . --write",
"lint": "eslint . --ext js,ts,vue,md --ignore-pattern \"/tests/fixtures\"",
"lint:docs": "prettier docs --check",
"format:docs": "prettier docs --write",
"release:prepare": "shipjs prepare",
"release:trigger": "shipjs trigger",
"test": "mocha --require ts-node/register \"./tests/**/*.ts\"",
Expand Down

0 comments on commit 7a5a180

Please sign in to comment.