-
Notifications
You must be signed in to change notification settings - Fork 694
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Updates all linting packages. Removes HTMLHint and replaces with ESli…
…nt rules for vue components.
- Loading branch information
Showing
17 changed files
with
958 additions
and
1,208 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); |
135 changes: 135 additions & 0 deletions
135
packages/eslint-plugin-kolibri/lib/rules/vue-component-class-name-casing.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
}, | ||
}); | ||
}, | ||
}; |
50 changes: 50 additions & 0 deletions
50
packages/eslint-plugin-kolibri/lib/rules/vue-component-no-duplicate-id.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.", | ||
}); | ||
}); | ||
} | ||
}); | ||
}, | ||
}); | ||
}, | ||
}; |
52 changes: 52 additions & 0 deletions
52
packages/eslint-plugin-kolibri/lib/rules/vue-component-require-img-src.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}, | ||
}); | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
96 changes: 96 additions & 0 deletions
96
packages/eslint-plugin-kolibri/tests/lib/rules/vue-component-class-name-casing.spec.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}, | ||
], | ||
}, | ||
], | ||
}); |
37 changes: 37 additions & 0 deletions
37
packages/eslint-plugin-kolibri/tests/lib/rules/vue-component-no-duplicate-id.spec.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}, | ||
], | ||
}, | ||
], | ||
}); |
Oops, something went wrong.