mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-11-04 07:24:13 +00:00 
			
		
		
		
	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
					
				
					 2 changed files with 168 additions and 120 deletions
				
			
		| 
						 | 
				
			
			@ -50,6 +50,15 @@ function findCallExpression(node) {
 | 
			
		|||
	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) {
 | 
			
		||||
	return node.arguments.length === 1 &&
 | 
			
		||||
		node.arguments[0].type === 'ObjectExpression';
 | 
			
		||||
| 
						 | 
				
			
			@ -82,14 +91,12 @@ function setDifference(a,b) {
 | 
			
		|||
 | 
			
		||||
/* the actual rule body
 | 
			
		||||
 */
 | 
			
		||||
function theRule(context) {
 | 
			
		||||
function theRuleBody(context,node) {
 | 
			
		||||
	// 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];
 | 
			
		||||
	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
 | 
			
		||||
	// *descendent* with the right identifier: skip them, we'll get
 | 
			
		||||
	// the right ones as well
 | 
			
		||||
| 
						 | 
				
			
			@ -117,9 +124,14 @@ function theRule(context) {
 | 
			
		|||
	// the translation structure)
 | 
			
		||||
	if (typeof(translation) !== 'string') return;
 | 
			
		||||
 | 
			
		||||
	const callExpression = findCallExpression(node);
 | 
			
		||||
	const vueExpression = findVueExpression(node);
 | 
			
		||||
 | 
			
		||||
	// some more checks on how the translation is called
 | 
			
		||||
			if (method == 'ts') {
 | 
			
		||||
				if (translation.match(/\{/)) {
 | 
			
		||||
	if (method === 'ts') {
 | 
			
		||||
		// the `<I18n> component gets parametric translations via
 | 
			
		||||
		// `i18n.ts.*`, but we error out elsewhere
 | 
			
		||||
		if (translation.match(/\{/) && !vueExpression) {
 | 
			
		||||
			context.report({
 | 
			
		||||
				node,
 | 
			
		||||
				message: `translation for ${pathStr} is parametric, but called via 'ts'`,
 | 
			
		||||
| 
						 | 
				
			
			@ -127,7 +139,7 @@ function theRule(context) {
 | 
			
		|||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
				if (findCallExpression(node)) {
 | 
			
		||||
		if (callExpression) {
 | 
			
		||||
			context.report({
 | 
			
		||||
				node,
 | 
			
		||||
				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(/\{/)) {
 | 
			
		||||
			context.report({
 | 
			
		||||
				node,
 | 
			
		||||
| 
						 | 
				
			
			@ -144,8 +156,7 @@ function theRule(context) {
 | 
			
		|||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
				const callExpression = findCallExpression(node);
 | 
			
		||||
				if (!callExpression) {
 | 
			
		||||
		if (!callExpression && !vueExpression) {
 | 
			
		||||
			context.report({
 | 
			
		||||
				node,
 | 
			
		||||
				message: `translation for ${pathStr} is parametric, but not called as a function`,
 | 
			
		||||
| 
						 | 
				
			
			@ -153,6 +164,11 @@ function theRule(context) {
 | 
			
		|||
			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)) {
 | 
			
		||||
			context.report({
 | 
			
		||||
				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 = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,31 +3,46 @@ const localeRule = require("./locale");
 | 
			
		|||
 | 
			
		||||
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(
 | 
			
		||||
  'sharkey-locale',
 | 
			
		||||
  localeRule,
 | 
			
		||||
  {
 | 
			
		||||
    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
 | 
			
		||||
      {code: 'i18n.ts.foo["something"]', options: [locale] },
 | 
			
		||||
      {code: 'i18n.ts.top', options: [locale] },
 | 
			
		||||
      {code: 'i18n.tsx.foo.baz({x:1})', options: [locale] },
 | 
			
		||||
      {code: 'whatever.i18n.ts.blah.blah', options: [locale] },
 | 
			
		||||
      {code: 'whatever.i18n.tsx.does.not.matter', options: [locale] },
 | 
			
		||||
      {code: 'whatever(i18n.ts.foo.bar)', options: [locale] },
 | 
			
		||||
      testCase('i18n.ts.foo["something"]'),
 | 
			
		||||
      testCase('i18n.ts.top'),
 | 
			
		||||
      testCase('i18n.tsx.foo.baz({x:1})'),
 | 
			
		||||
      testCase('whatever.i18n.ts.blah.blah'),
 | 
			
		||||
      testCase('whatever.i18n.tsx.does.not.matter'),
 | 
			
		||||
      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: [
 | 
			
		||||
      {code: 'i18n.ts.not', options: [locale], errors: 1 },
 | 
			
		||||
      {code: 'i18n.tsx.deep.not', options: [locale], errors: 1 },
 | 
			
		||||
      {code: 'i18n.tsx.deep.not({x:12})', options: [locale], errors: 1 },
 | 
			
		||||
      {code: 'i18n.tsx.top({x:1})', options: [locale], errors: 1 },
 | 
			
		||||
      {code: 'i18n.ts.foo.baz', options: [locale], errors: 1 },
 | 
			
		||||
      {code: 'i18n.tsx.foo.baz', options: [locale], errors: 1 },
 | 
			
		||||
      {code: 'i18n.tsx.foo.baz({y:2})', options: [locale], errors: 2 },
 | 
			
		||||
      testCase('i18n.ts.not', 1),
 | 
			
		||||
      testCase('i18n.tsx.deep.not', 1),
 | 
			
		||||
      testCase('i18n.tsx.deep.not({x:12})', 1),
 | 
			
		||||
      testCase('i18n.tsx.top({x:1})', 1),
 | 
			
		||||
      testCase('i18n.ts.foo.baz', 1),
 | 
			
		||||
      testCase('i18n.tsx.foo.baz', 1),
 | 
			
		||||
      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…
	
	Add table
		
		Reference in a new issue