Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support validate literal strings in vue component #134

Merged
merged 2 commits into from
Nov 24, 2024
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
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
8 changes: 6 additions & 2 deletions lib/options/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"framework":{
"type":"string",
"enum": ["react","vue"]
},
"mode": {
"type": "string",
"enum": ["jsx-text-only", "jsx-only", "all"]
"enum": ["jsx-text-only", "jsx-only", "all","vue-template-only"]
},
"jsx-components": {
"type": "object",
Expand Down Expand Up @@ -50,4 +54,4 @@
"message": { "type": "string" },
"should-validate-template": { "type": "boolean" }
}
}
}
102 changes: 83 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,13 @@ module.exports = {
mode,
'should-validate-template': validateTemplate,
message,
framework,
} = options;
const onlyValidateJSX = ['jsx-only', 'jsx-text-only'].includes(mode);

const onlyValidateVueTemplate =
framework === 'vue' && mode === 'vue-template-only';

//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------
Expand Down Expand Up @@ -210,6 +234,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 +324,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 +333,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 +350,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 +394,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
Loading
Loading