Skip to content

Commit

Permalink
Updates all linting packages. Removes HTMLHint and replaces with ESli…
Browse files Browse the repository at this point in the history
…nt rules for vue components.
  • Loading branch information
rtibbles committed Sep 27, 2022
1 parent 02b28b0 commit 5157eac
Show file tree
Hide file tree
Showing 17 changed files with 1,054 additions and 1,207 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
require("@rushstack/eslint-patch/modern-module-resolution");
module.exports = require('kolibri-tools/.eslintrc');
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

const utils = require('eslint-plugin-vue/lib//utils');
const casing = require('eslint-plugin-vue/lib//utils/casing');

// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------

/**
* Report a forbidden class casing
* @param {string} className
* @param {*} node
* @param {RuleContext} context
* @param {Set<string>} forbiddenClasses
*/
const reportForbiddenClassCasing = (className, node, context, caseType) => {
if (!casing.getChecker(caseType)(className)) {
const loc = node.value ? node.value.loc : node.loc;
context.report({
node,
loc,
message: 'Class name "{{class}}" is not {{caseType}}.',
data: {
class: className,
caseType,
},
});
}
};

/**
* @param {Expression} node
* @param {boolean} [textOnly]
* @returns {IterableIterator<{ className:string, reportNode: ESNode }>}
*/
function* extractClassNames(node, textOnly) {
if (node.type === 'Literal') {
yield* `${node.value}`.split(/\s+/).map(className => ({ className, reportNode: node }));
return;
}
if (node.type === 'TemplateLiteral') {
for (const templateElement of node.quasis) {
yield* templateElement.value.cooked
.split(/\s+/)
.map(className => ({ className, reportNode: templateElement }));
}
for (const expr of node.expressions) {
yield* extractClassNames(expr, true);
}
return;
}
if (node.type === 'BinaryExpression') {
if (node.operator !== '+') {
return;
}
yield* extractClassNames(node.left, true);
yield* extractClassNames(node.right, true);
return;
}
if (textOnly) {
return;
}
if (node.type === 'ObjectExpression') {
for (const prop of node.properties) {
if (prop.type !== 'Property') {
continue;
}
const classNames = utils.getStaticPropertyName(prop);
if (!classNames) {
continue;
}
yield* classNames.split(/\s+/).map(className => ({ className, reportNode: prop.key }));
}
return;
}
if (node.type === 'ArrayExpression') {
for (const element of node.elements) {
if (element == null) {
continue;
}
if (element.type === 'SpreadElement') {
continue;
}
yield* extractClassNames(element);
}
return;
}
}

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce specific casing for the class naming style in template',
categories: undefined,
},
fixable: null,
},
/** @param {RuleContext} context */
create(context) {
const caseType = 'kebab-case';
return utils.defineTemplateBodyVisitor(context, {
/**
* @param {VAttribute & { value: VLiteral } } node
*/
'VAttribute[directive=false][key.name="class"]'(node) {
node.value.value
.split(/\s+/)
.forEach(className => reportForbiddenClassCasing(className, node, context, caseType));
},

/** @param {VExpressionContainer} node */
"VAttribute[directive=true][key.name.name='bind'][key.argument.name='class'] > VExpressionContainer.value"(
node
) {
if (!node.expression) {
return;
}

for (const { className, reportNode } of extractClassNames(
/** @type {Expression} */ (node.expression)
)) {
reportForbiddenClassCasing(className, reportNode, context, caseType);
}
},
});
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

const utils = require('eslint-plugin-vue/lib//utils');

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'detect duplicate ids in Vue components',
categories: undefined,
},
fixable: null,
},
/** @param {RuleContext} context */
create(context) {
const IdAttrsMap = new Map();
return utils.defineTemplateBodyVisitor(context, {
/**
* @param {VAttribute & { value: VLiteral } } node
*/
'VAttribute[directive=false][key.name="id"]'(node) {
const idAttr = node.value;
if (!IdAttrsMap.has(idAttr.value)) {
IdAttrsMap.set(idAttr.value, []);
}
const nodes = IdAttrsMap.get(idAttr.value);
nodes.push(idAttr);
},
"VElement[parent.type!='VElement']:exit"() {
IdAttrsMap.forEach(attrs => {
if (Array.isArray(attrs) && attrs.length > 1) {
attrs.forEach(attr => {
context.report({
node: attr,
data: { id: attr.value },
message: "The id '{{id}}' is duplicated.",
});
});
}
});
},
});
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const utils = require('eslint-plugin-vue/lib/utils');

module.exports = {
meta: {
type: 'code',

docs: {
description: 'Require `src` attribute of `<img>` tag',
category: undefined,
},

fixable: null,
messages: {
missingSrcAttribute: 'Missing `src` attribute of `<img>` tag',
},
},

create(context) {
function report(node) {
context.report({
node,
messageId: 'missingSrcAttribute',
});
}

return utils.defineTemplateBodyVisitor(context, {
/**
* @param {VElement} node
*/
"VElement[rawName='img']"(node) {
const srcAttr = utils.getAttribute(node, 'src');
if (srcAttr) {
const value = srcAttr.value;
if (!value || !value.value) {
report(value || srcAttr);
}
return;
}
const srcDir = utils.getDirective(node, 'bind', 'src');
if (srcDir) {
const value = srcDir.value;
if (!value || !value.expression) {
report(value || srcDir);
}
return;
}

report(node.startTag);
},
});
},
};
2 changes: 1 addition & 1 deletion packages/eslint-plugin-kolibri/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"requireindex": "^1.1.0"
},
"devDependencies": {
"eslint": "^5.16.0"
"eslint": "^8.23.0"
},
"engines": {
"node": ">=0.10.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
'use strict';

const RuleTester = require('eslint').RuleTester;
const rule = require('../../../lib/rules/vue-component-class-name-casing');

const ruleTester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
parserOptions: { ecmaVersion: 2020, sourceType: 'module' },
});

ruleTester.run('class-name-casing', rule, {
valid: [
{ code: `<template><div class="is-allowed">Content</div></template>` },
{
code: `<template><div class="allowed" foo="barBar">Content</div></template>`,
},
{
code: `<template><div :class="{'is-allowed': true}">Content</div></template>`,
},
],

invalid: [
{
code: `<template><div class="forBidden is-allowed" /></template>`,
errors: [
{
message: 'Class name "forBidden" is not kebab-case.',
type: 'VAttribute',
},
],
},
{
code: `<template><div :class="'forBidden' + ' ' + 'is-allowed' + someVar" /></template>`,
errors: [
{
message: 'Class name "forBidden" is not kebab-case.',
type: 'Literal',
},
],
},
{
code: `<template><div :class="{'forBidden': someBool, 'some-var': true}" /></template>`,
errors: [
{
message: 'Class name "forBidden" is not kebab-case.',
type: 'Literal',
},
],
},
{
code: `<template><div :class="{forBidden: someBool}" /></template>`,
errors: [
{
message: 'Class name "forBidden" is not kebab-case.',
type: 'Identifier',
},
],
},
{
code: '<template><div :class="`forBidden ${someVar}`" /></template>',
errors: [
{
message: 'Class name "forBidden" is not kebab-case.',
type: 'TemplateElement',
},
],
},
{
code: `<template><div :class="'forBidden'" /></template>`,
errors: [
{
message: 'Class name "forBidden" is not kebab-case.',
type: 'Literal',
},
],
},
{
code: `<template><div :class="['forBidden', 'is-allowed']" /></template>`,
errors: [
{
message: 'Class name "forBidden" is not kebab-case.',
type: 'Literal',
},
],
},
{
code: `<template><div :class="['allowed forBidden', someString]" /></template>`,
errors: [
{
message: 'Class name "forBidden" is not kebab-case.',
type: 'Literal',
},
],
},
],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use strict';

const RuleTester = require('eslint').RuleTester;
const rule = require('../../../lib/rules/vue-component-no-duplicate-id');

const ruleTester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
parserOptions: { ecmaVersion: 2020, sourceType: 'module' },
});

ruleTester.run('no-duplicate-ids', rule, {
valid: [
{ code: `<template><div id="allowed">Content</div></template>` },
{
code: `<template><div id="allowed">Content</div><div class="allowed">Here</div></template>`,
},
{
code: `<template><div id="allowed">Content</div><div id="also">Here</div></template>`,
},
],

invalid: [
{
code: `<template><div id="allowed">Content</div><div id="allowed">Here</div></template>`,
errors: [
{
message: "The id 'allowed' is duplicated.",
type: 'VLiteral',
},
{
message: "The id 'allowed' is duplicated.",
type: 'VLiteral',
},
],
},
],
});
Loading

0 comments on commit 5157eac

Please sign in to comment.