Skip to content

Commit

Permalink
feat: support validate vue component
Browse files Browse the repository at this point in the history
  • Loading branch information
Fan Tongshuai committed Sep 23, 2024
1 parent a42ec3e commit 8881300
Show file tree
Hide file tree
Showing 6 changed files with 3,280 additions and 38 deletions.
13 changes: 9 additions & 4 deletions docs/rules/no-literal-string.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ type MySchema = {
exclude?: string[];
};
} & {
mode?: 'jsx-text-only' | 'jsx-only' | 'all';
framework: 'react' | 'vue';
mode?: 'jsx-text-only' | 'jsx-only' | 'all' | 'vue-template-ony';
message?: string;
'should-validate-template'?: boolean;
};
Expand Down Expand Up @@ -118,10 +119,14 @@ const message = 'foob';

### Other options

- `framework` specifies the type of framework currently in use.
- `react` It defaults to 'react' which means you want to validate react component
- `vue` If you want to validate vue component, can set the value to be this
- `mode` provides a straightforward way to decides the range you want to validate literal strings.
It defaults to `jsx-text-only` which only forbids to write plain text in JSX markup
- `jsx-only` validates the JSX attributes as well
- `all` validates all literal strings
It defaults to `jsx-text-only` which only forbids to write plain text in JSX markup,available when framework option is 'react'
- `jsx-only` validates the JSX attributes as well,available when framework option is 'react'
- `all` validates all literal strings,available when the value of the framework option is 'react' and 'vue'
- `vue-template-only`, only validate vue component template part,available when framework option value is 'vue'.
- `message` defines the custom error message
- `should-validate-template` decides if we should validate the string templates

Expand Down
1 change: 1 addition & 0 deletions lib/options/defaults.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
module.exports = {
framework: 'react',
mode: 'jsx-text-only',
'jsx-components': {
include: [],
Expand Down
68 changes: 53 additions & 15 deletions lib/options/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,90 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"framework": {
"type": "string",
"enum": [
"vue",
"react"
]
},
"mode": {
"type": "string",
"enum": ["jsx-text-only", "jsx-only", "all"]
"enum": [
"jsx-text-only",
"jsx-only",
"all",
"vue-template-only"
]
},
"jsx-components": {
"type": "object",
"properties": {
"include": { "type": "array" },
"exclude": { "type": "array" }
"include": {
"type": "array"
},
"exclude": {
"type": "array"
}
}
},
"jsx-attributes": {
"type": "object",
"properties": {
"include": { "type": "array" },
"exclude": { "type": "array" }
"include": {
"type": "array"
},
"exclude": {
"type": "array"
}
}
},
"words": {
"type": "object",
"properties": {
"exclude": { "type": "array" }
"exclude": {
"type": "array"
}
}
},
"callees": {
"type": "object",
"properties": {
"include": { "type": "array" },
"exclude": { "type": "array" }
"include": {
"type": "array"
},
"exclude": {
"type": "array"
}
}
},
"object-properties": {
"type": "object",
"properties": {
"include": { "type": "array" },
"exclude": { "type": "array" }
"include": {
"type": "array"
},
"exclude": {
"type": "array"
}
}
},
"class-properties": {
"type": "object",
"properties": {
"include": { "type": "array" },
"exclude": { "type": "array" }
"include": {
"type": "array"
},
"exclude": {
"type": "array"
}
}
},
"message": { "type": "string" },
"should-validate-template": { "type": "boolean" }
"message": {
"type": "string"
},
"should-validate-template": {
"type": "boolean"
}
}
}
}
101 changes: 82 additions & 19 deletions lib/rules/no-literal-string.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,26 @@ function isValidLiteral(options, { value }) {
if (shouldSkip(options.words, trimed)) return true;
}

/**
* @param {VDirective | VAttribute} node
* @returns {string | null}
*/
function getAttributeName(node) {
if (!node.directive) {
return node.key.rawName;
}

if (
(node.key.name.name === 'bind' || node.key.name.name === 'model') &&
node.key.argument &&
node.key.argument.type === 'VIdentifier'
) {
return node.key.argument.rawName;
}

return null;
}

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
Expand Down Expand Up @@ -58,9 +78,12 @@ module.exports = {
mode,
'should-validate-template': validateTemplate,
message,
framework,
} = options;
const onlyValidateJSX = ['jsx-only', 'jsx-text-only'].includes(mode);

const onlyValidateVueTemplate = framework === 'vue' && mode !== 'all';

//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------
Expand Down Expand Up @@ -210,6 +233,22 @@ module.exports = {
},
// ─────────────────────────────────────────────────────────────────

//
// ─── Vue ──────────────────────────────────────────────────
//
VElement(node) {
indicatorStack.push(
shouldSkip(options['jsx-components'], node.rawName)
);
},
'VElement:exit': endIndicator,
VAttribute(node) {
const attrName = getAttributeName(node);
indicatorStack.push(shouldSkip(options['jsx-attributes'], attrName));
},
'VAttribute:exit': endIndicator,
// ─────────────────────────────────────────────────────────────────

//
// ─── TYPESCRIPT ──────────────────────────────────────────────────
//
Expand Down Expand Up @@ -284,12 +323,6 @@ module.exports = {
);
},
'TaggedTemplateExpression:exit': endIndicator,

'SwitchCase > Literal'(node) {
indicatorStack.push(true);
},
'SwitchCase > Literal:exit': endIndicator,

'AssignmentExpression[left.type="MemberExpression"]'(node) {
// allow Enum['value']
indicatorStack.push(
Expand All @@ -299,20 +332,12 @@ module.exports = {
'AssignmentExpression[left.type="MemberExpression"]:exit'(node) {
endIndicator();
},
'MemberExpression > Literal'(node) {
// allow Enum['value']
indicatorStack.push(true);
},
'MemberExpression > Literal:exit'(node) {
endIndicator();
},

TemplateLiteral(node) {
if (!validateTemplate) {
return;
}

if (filterOutJSX(node)) {
if (framework === 'react' && filterOutJSX(node)) {
return;
}

Expand All @@ -324,16 +349,39 @@ module.exports = {
return true; // break
});
},
Literal(node) {
// allow Enum['value'] and literal that follows the 'case' keyword in a switch statement.
if (['MemberExpression', 'SwitchCase'].includes(node?.parent?.type)) {
return;
}

'Literal:exit'(node) {
if (filterOutJSX(node)) {
if (framework === 'react' && filterOutJSX(node)) {
return;
}

if (onlyValidateVueTemplate) {
const parents = context.getAncestors();
if (
parents.length &&
parents.every(
item =>
![
'VElement',
'VAttribute',
'VText',
'VExpressionContainer',
].includes(item.type)
)
) {
return true;
}
}

// ignore `var a = { "foo": 123 }`
if (node.parent.key === node) {
return;
}

validateBeforeReport(node);
},
};
Expand All @@ -345,14 +393,29 @@ module.exports = {
VText(node) {
scriptVisitor['JSXText'](node);
},
VLiteral(node) {
scriptVisitor['JSXText'](node);
},
VElement(node) {
scriptVisitor['VElement'](node);
},
'VElement:exit'(node) {
scriptVisitor['VElement:exit'](node);
},
VAttribute(node) {
scriptVisitor['VAttribute'](node);
},
'VAttribute:exit'(node) {
scriptVisitor['VAttribute:exit'](node);
},
'VExpressionContainer CallExpression'(node) {
scriptVisitor['CallExpression'](node);
},
'VExpressionContainer CallExpression:exit'(node) {
scriptVisitor['CallExpression:exit'](node);
},
'VExpressionContainer Literal:exit'(node) {
scriptVisitor['Literal:exit'](node);
'VExpressionContainer Literal'(node) {
scriptVisitor['Literal'](node);
},
},
scriptVisitor
Expand Down
24 changes: 24 additions & 0 deletions tests/lib/rules/no-literal-string/vue.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ vueTester.run('no-literal-string: vue', rule, {
code: '<template>{{ i18next.t("abc") }}</template>',
options: [{ mode: 'all' }],
},
{
code:
'<template><myVueComponent string-prop="this is a string literal"></myVueComponent><template>',
options: [
{
framework: 'vue',
mode: 'vue-template-only',
'jsx-attributes': { exclude: ['string-prop'] },
},
],
errors: 0,
},
],
invalid: [
{
Expand All @@ -30,5 +42,17 @@ vueTester.run('no-literal-string: vue', rule, {
options: [{ mode: 'all' }],
errors: 1,
},
{
code:
'<template><myVueComponent string-prop="this is a string literal"></myVueComponent><template>',
options: [{ mode: 'all' }],
errors: 1,
},
{
code:
'<template><div>{{ "this is a string literal in mustaches" }}</div><template>',
options: [{ framework: 'vue', mode: 'vue-template-only' }],
errors: 1,
},
],
});
Loading

0 comments on commit 8881300

Please sign in to comment.