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 8, 2022
1 parent 627583f commit a5f1640
Show file tree
Hide file tree
Showing 17 changed files with 958 additions and 1,208 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 a5f1640

Please sign in to comment.