lint Vue templates as well
the argument detection doesn't work inside templates when invoked via the `<I18n>` component, because it's too complicated for me now
This commit is contained in:
parent
f11536c927
commit
b0bc24f01b
@ -50,6 +50,15 @@ function findCallExpression(node) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// same, but for Vue expressions (`<I18n :src="i18n.ts.foo">`)
|
||||||
|
function findVueExpression(node) {
|
||||||
|
if (!node.parent) return null;
|
||||||
|
|
||||||
|
if (node.parent.type.match(/^VExpr/) && node.parent.expression === node) return node.parent;
|
||||||
|
if (node.parent.type === 'MemberExpression') return findVueExpression(node.parent);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function areArgumentsOneObject(node) {
|
function areArgumentsOneObject(node) {
|
||||||
return node.arguments.length === 1 &&
|
return node.arguments.length === 1 &&
|
||||||
node.arguments[0].type === 'ObjectExpression';
|
node.arguments[0].type === 'ObjectExpression';
|
||||||
@ -82,14 +91,12 @@ function setDifference(a,b) {
|
|||||||
|
|
||||||
/* the actual rule body
|
/* the actual rule body
|
||||||
*/
|
*/
|
||||||
function theRule(context) {
|
function theRuleBody(context,node) {
|
||||||
// we get the locale/translations via the options; it's the data
|
// we get the locale/translations via the options; it's the data
|
||||||
// that goes into a specific language's JSON file, see
|
// that goes into a specific language's JSON file, see
|
||||||
// `scripts/build-assets.mjs`
|
// `scripts/build-assets.mjs`
|
||||||
const locale = context.options[0];
|
const locale = context.options[0];
|
||||||
return {
|
|
||||||
// for all object member access that have an identifier 'i18n'...
|
|
||||||
'MemberExpression:has(> Identifier[name=i18n])': (node) => {
|
|
||||||
// sometimes we get MemberExpression nodes that have a
|
// sometimes we get MemberExpression nodes that have a
|
||||||
// *descendent* with the right identifier: skip them, we'll get
|
// *descendent* with the right identifier: skip them, we'll get
|
||||||
// the right ones as well
|
// the right ones as well
|
||||||
@ -117,9 +124,14 @@ function theRule(context) {
|
|||||||
// the translation structure)
|
// the translation structure)
|
||||||
if (typeof(translation) !== 'string') return;
|
if (typeof(translation) !== 'string') return;
|
||||||
|
|
||||||
|
const callExpression = findCallExpression(node);
|
||||||
|
const vueExpression = findVueExpression(node);
|
||||||
|
|
||||||
// some more checks on how the translation is called
|
// some more checks on how the translation is called
|
||||||
if (method == 'ts') {
|
if (method === 'ts') {
|
||||||
if (translation.match(/\{/)) {
|
// the `<I18n> component gets parametric translations via
|
||||||
|
// `i18n.ts.*`, but we error out elsewhere
|
||||||
|
if (translation.match(/\{/) && !vueExpression) {
|
||||||
context.report({
|
context.report({
|
||||||
node,
|
node,
|
||||||
message: `translation for ${pathStr} is parametric, but called via 'ts'`,
|
message: `translation for ${pathStr} is parametric, but called via 'ts'`,
|
||||||
@ -127,7 +139,7 @@ function theRule(context) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (findCallExpression(node)) {
|
if (callExpression) {
|
||||||
context.report({
|
context.report({
|
||||||
node,
|
node,
|
||||||
message: `translation for ${pathStr} is not parametric, but is called as a function`,
|
message: `translation for ${pathStr} is not parametric, but is called as a function`,
|
||||||
@ -135,7 +147,7 @@ function theRule(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (method == 'tsx') {
|
if (method === 'tsx') {
|
||||||
if (!translation.match(/\{/)) {
|
if (!translation.match(/\{/)) {
|
||||||
context.report({
|
context.report({
|
||||||
node,
|
node,
|
||||||
@ -144,8 +156,7 @@ function theRule(context) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const callExpression = findCallExpression(node);
|
if (!callExpression && !vueExpression) {
|
||||||
if (!callExpression) {
|
|
||||||
context.report({
|
context.report({
|
||||||
node,
|
node,
|
||||||
message: `translation for ${pathStr} is parametric, but not called as a function`,
|
message: `translation for ${pathStr} is parametric, but not called as a function`,
|
||||||
@ -153,6 +164,11 @@ function theRule(context) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we're not currently checking arguments when used via the
|
||||||
|
// `<I18n>` component, because it's too complicated (also, it
|
||||||
|
// would have to be done inside the `if (method === 'ts')`)
|
||||||
|
if (!callExpression) return;
|
||||||
|
|
||||||
if (!areArgumentsOneObject(callExpression)) {
|
if (!areArgumentsOneObject(callExpression)) {
|
||||||
context.report({
|
context.report({
|
||||||
node,
|
node,
|
||||||
@ -191,8 +207,25 @@ function theRule(context) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function theRule(context) {
|
||||||
|
// we get the locale/translations via the options; it's the data
|
||||||
|
// that goes into a specific language's JSON file, see
|
||||||
|
// `scripts/build-assets.mjs`
|
||||||
|
const locale = context.options[0];
|
||||||
|
|
||||||
|
// for all object member access that have an identifier 'i18n'...
|
||||||
|
return context.getSourceCode().parserServices.defineTemplateBodyVisitor(
|
||||||
|
{
|
||||||
|
// this is for <template> bits, needs work
|
||||||
|
'MemberExpression:has(Identifier[name=i18n])': (node) => theRuleBody(context, node),
|
||||||
},
|
},
|
||||||
};
|
{
|
||||||
|
// this is for normal code
|
||||||
|
'MemberExpression:has(Identifier[name=i18n])': (node) => theRuleBody(context, node),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -3,31 +3,46 @@ const localeRule = require("./locale");
|
|||||||
|
|
||||||
const locale = { foo: { bar: 'ok', baz: 'good {x}' }, top: '123' };
|
const locale = { foo: { bar: 'ok', baz: 'good {x}' }, top: '123' };
|
||||||
|
|
||||||
const ruleTester = new RuleTester();
|
const ruleTester = new RuleTester({
|
||||||
|
languageOptions: {
|
||||||
|
parser: require('vue-eslint-parser'),
|
||||||
|
ecmaVersion: 2015,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function testCase(code,errors) {
|
||||||
|
return { code, errors, options: [ locale ], filename: 'test.ts' };
|
||||||
|
}
|
||||||
|
function testCaseVue(code,errors) {
|
||||||
|
return { code, errors, options: [ locale ], filename: 'test.vue' };
|
||||||
|
}
|
||||||
|
|
||||||
ruleTester.run(
|
ruleTester.run(
|
||||||
'sharkey-locale',
|
'sharkey-locale',
|
||||||
localeRule,
|
localeRule,
|
||||||
{
|
{
|
||||||
valid: [
|
valid: [
|
||||||
{code: 'i18n.ts.foo.bar', options: [locale] },
|
testCase('i18n.ts.foo.bar'),
|
||||||
// we don't detect the problem here, but should still accept it
|
// we don't detect the problem here, but should still accept it
|
||||||
{code: 'i18n.ts.foo["something"]', options: [locale] },
|
testCase('i18n.ts.foo["something"]'),
|
||||||
{code: 'i18n.ts.top', options: [locale] },
|
testCase('i18n.ts.top'),
|
||||||
{code: 'i18n.tsx.foo.baz({x:1})', options: [locale] },
|
testCase('i18n.tsx.foo.baz({x:1})'),
|
||||||
{code: 'whatever.i18n.ts.blah.blah', options: [locale] },
|
testCase('whatever.i18n.ts.blah.blah'),
|
||||||
{code: 'whatever.i18n.tsx.does.not.matter', options: [locale] },
|
testCase('whatever.i18n.tsx.does.not.matter'),
|
||||||
{code: 'whatever(i18n.ts.foo.bar)', options: [locale] },
|
testCase('whatever(i18n.ts.foo.bar)'),
|
||||||
|
testCaseVue('<template><p>{{ i18n.ts.foo.bar }}</p></template>'),
|
||||||
|
testCaseVue('<template><I18n :src="i18n.ts.foo.baz"/></template>'),
|
||||||
],
|
],
|
||||||
invalid: [
|
invalid: [
|
||||||
{code: 'i18n.ts.not', options: [locale], errors: 1 },
|
testCase('i18n.ts.not', 1),
|
||||||
{code: 'i18n.tsx.deep.not', options: [locale], errors: 1 },
|
testCase('i18n.tsx.deep.not', 1),
|
||||||
{code: 'i18n.tsx.deep.not({x:12})', options: [locale], errors: 1 },
|
testCase('i18n.tsx.deep.not({x:12})', 1),
|
||||||
{code: 'i18n.tsx.top({x:1})', options: [locale], errors: 1 },
|
testCase('i18n.tsx.top({x:1})', 1),
|
||||||
{code: 'i18n.ts.foo.baz', options: [locale], errors: 1 },
|
testCase('i18n.ts.foo.baz', 1),
|
||||||
{code: 'i18n.tsx.foo.baz', options: [locale], errors: 1 },
|
testCase('i18n.tsx.foo.baz', 1),
|
||||||
{code: 'i18n.tsx.foo.baz({y:2})', options: [locale], errors: 2 },
|
testCase('i18n.tsx.foo.baz({y:2})', 2),
|
||||||
|
testCaseVue('<template><p>{{ i18n.ts.not }}</p></template>', 1),
|
||||||
|
testCaseVue('<template><I18n :src="i18n.ts.not"/></template>', 1),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user