initial commit of actions

This commit is contained in:
Dominik Polakovics Polakovics 2026-01-31 18:56:04 +01:00
commit 949ece5785
44660 changed files with 12034344 additions and 0 deletions

View file

@ -0,0 +1,20 @@
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce `for..of` loops over `Array.forEach`',
url: require('../url')(module)
},
schema: []
},
create(context) {
return {
CallExpression(node) {
if (node.callee.property && node.callee.property.name === 'forEach') {
context.report({node, message: 'Prefer for...of instead of Array.forEach'})
}
}
}
}
}

View file

@ -0,0 +1,28 @@
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow `event.currentTarget` calls inside of async functions',
url: require('../url')(module)
},
schema: []
},
create(context) {
const scopeDidWait = new WeakSet()
return {
AwaitExpression() {
scopeDidWait.add(context.getScope(), true)
},
MemberExpression(node) {
if (node.property && node.property.name === 'currentTarget') {
const scope = context.getScope()
if (scope.block.async && scopeDidWait.has(scope)) {
context.report({node, message: 'event.currentTarget inside an async function is error prone'})
}
}
}
}
}
}

View file

@ -0,0 +1,28 @@
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow `event.preventDefault` calls inside of async functions',
url: require('../url')(module)
},
schema: []
},
create(context) {
const scopeDidWait = new WeakSet()
return {
AwaitExpression() {
scopeDidWait.add(context.getScope(), true)
},
CallExpression(node) {
if (node.callee.property && node.callee.property.name === 'preventDefault') {
const scope = context.getScope()
if (scope.block.async && scopeDidWait.has(scope)) {
context.report({node, message: 'event.preventDefault() inside an async function is error prone'})
}
}
}
}
}
}

View file

@ -0,0 +1,30 @@
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow usage of CSRF tokens in JavaScript',
url: require('../url')(module)
},
schema: []
},
create(context) {
function checkAuthenticityTokenUsage(node, str) {
if (str.includes('authenticity_token')) {
context.report({
node,
message:
'Form CSRF tokens (authenticity tokens) should not be created in JavaScript and their values should not be used directly for XHR requests.'
})
}
}
return {
Literal(node) {
if (typeof node.value === 'string') {
checkAuthenticityTokenUsage(node, node.value)
}
}
}
}
}

View file

@ -0,0 +1,53 @@
const svgElementAttributes = require('svg-element-attributes')
const attributeCalls = /^(get|has|set|remove)Attribute$/
const validAttributeName = /^[a-z][a-z0-9-]*$/
// these are common SVG attributes that *must* have the correct case to work
const camelCaseAttributes = Object.values(svgElementAttributes)
.reduce((all, elementAttrs) => all.concat(elementAttrs), [])
.filter(name => !validAttributeName.test(name))
const validSVGAttributeSet = new Set(camelCaseAttributes)
// lowercase variants of camelCase SVG attributes are probably an error
const invalidSVGAttributeSet = new Set(camelCaseAttributes.map(name => name.toLowerCase()))
function isValidAttribute(name) {
return validSVGAttributeSet.has(name) || (validAttributeName.test(name) && !invalidSVGAttributeSet.has(name))
}
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow wrong usage of attribute names',
url: require('../url')(module)
},
fixable: 'code',
schema: []
},
create(context) {
return {
CallExpression(node) {
if (!node.callee.property) return
const calleeName = node.callee.property.name
if (!attributeCalls.test(calleeName)) return
const attributeNameNode = node.arguments[0]
if (!attributeNameNode) return
if (!isValidAttribute(attributeNameNode.value)) {
context.report({
node: attributeNameNode,
message: 'Attributes should be lowercase and hyphen separated, or part of the SVG whitelist.',
fix(fixer) {
return fixer.replaceText(attributeNameNode, `'${attributeNameNode.value.toLowerCase()}'`)
}
})
}
}
}
}
}

View file

@ -0,0 +1,57 @@
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce a naming convention for js- prefixed classes',
url: require('../url')(module)
},
schema: []
},
create(context) {
const allJsClassNameRegexp = /\bjs-[_a-zA-Z0-9-]*/g
const validJsClassNameRegexp = /^js(-[a-z0-9]+)+$/g
const endWithJsClassNameRegexp = /\bjs-[_a-zA-Z0-9-]*$/g
function checkStringFormat(node, str) {
const matches = str.match(allJsClassNameRegexp) || []
for (const match of matches) {
if (!match.match(validJsClassNameRegexp)) {
context.report({node, message: 'js- class names should be lowercase and only contain dashes.'})
}
}
}
function checkStringEndsWithJSClassName(node, str) {
if (str.match(endWithJsClassNameRegexp)) {
context.report({node, message: 'js- class names should be statically defined.'})
}
}
return {
Literal(node) {
if (typeof node.value === 'string') {
checkStringFormat(node, node.value)
if (
node.parent &&
node.parent.type === 'BinaryExpression' &&
node.parent.operator === '+' &&
node.parent.left.value
) {
checkStringEndsWithJSClassName(node.parent.left, node.parent.left.value)
}
}
},
TemplateLiteral(node) {
for (const quasi of node.quasis) {
checkStringFormat(quasi, quasi.value.raw)
if (quasi.tail === false) {
checkStringEndsWithJSClassName(quasi, quasi.value.raw)
}
}
}
}
}
}

View file

@ -0,0 +1,19 @@
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow usage of `Element.prototype.blur()`',
url: require('../url')(module)
},
schema: []
},
create(context) {
return {
CallExpression(node) {
if (node.callee.property && node.callee.property.name === 'blur') {
context.report({node, message: 'Do not use element.blur(), instead restore the focus of a previous element.'})
}
}
}
}
}

View file

@ -0,0 +1,31 @@
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow usage the `d-none` CSS class',
url: require('../url')(module)
},
schema: []
},
create(context) {
return {
CallExpression(node) {
if (
node.callee.type === 'MemberExpression' &&
node.callee.object.property &&
node.callee.object.property.name === 'classList'
) {
const invalidArgument = node.arguments.some(arg => {
return arg.type === 'Literal' && arg.value === 'd-none'
})
if (invalidArgument) {
context.report({
node,
message: 'Prefer hidden property to d-none class'
})
}
}
}
}
}
}

View file

@ -0,0 +1,20 @@
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce usage of `Element.prototype.getAttribute` instead of `Element.prototype.datalist`',
url: require('../url')(module)
},
schema: []
},
create(context) {
return {
MemberExpression(node) {
if (node.property && node.property.name === 'dataset') {
context.report({node, message: "Use getAttribute('data-your-attribute') instead of dataset."})
}
}
}
}
}

View file

@ -0,0 +1,35 @@
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow implicit global variables',
url: require('../url')(module)
},
schema: []
},
create(context) {
return {
Program() {
const scope = context.getScope()
for (const variable of scope.variables) {
if (variable.writeable) {
return
}
for (const def of variable.defs) {
if (
def.type === 'FunctionName' ||
def.type === 'ClassName' ||
(def.type === 'Variable' && def.parent.kind === 'const') ||
(def.type === 'Variable' && def.parent.kind === 'let')
) {
context.report({node: def.node, message: 'Implicit global variable, assign as global property instead.'})
}
}
}
}
}
}
}

View file

@ -0,0 +1,30 @@
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow `Element.prototype.innerText` in favor of `Element.prototype.textContent`',
url: require('../url')(module)
},
fixable: 'code',
schema: []
},
create(context) {
return {
MemberExpression(node) {
if (node.property && node.property.name === 'innerText') {
context.report({
meta: {
fixable: 'code'
},
node: node.property,
message: 'Prefer textContent to innerText',
fix(fixer) {
return fixer.replaceText(node.property, 'textContent')
}
})
}
}
}
}
}

View file

@ -0,0 +1,22 @@
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce using `async/await` syntax over Promises',
url: require('../url')(module)
},
schema: []
},
create(context) {
return {
MemberExpression(node) {
if (node.property && node.property.name === 'then') {
context.report({node: node.property, message: 'Prefer async/await to Promise.then()'})
} else if (node.property && node.property.name === 'catch') {
context.report({node: node.property, message: 'Prefer async/await to Promise.catch()'})
}
}
}
}
}

View file

@ -0,0 +1,51 @@
const passiveEventListenerNames = new Set(['touchstart', 'touchmove', 'wheel', 'mousewheel'])
const propIsPassiveTrue = prop => prop.key && prop.key.name === 'passive' && prop.value && prop.value.value === true
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow marking a event handler as passive when it has no effect',
url: require('../url')(module)
},
fixable: 'code',
schema: []
},
create(context) {
return {
['CallExpression[callee.property.name="addEventListener"]']: function (node) {
const [name, listener, options] = node.arguments
if (name.type !== 'Literal') return
if (passiveEventListenerNames.has(name.value)) return
if (options && options.type === 'ObjectExpression') {
const i = options.properties.findIndex(propIsPassiveTrue)
if (i === -1) return
const passiveProp = options.properties[i]
const l = options.properties.length
const source = context.getSourceCode()
context.report({
node: passiveProp,
message: `"${name.value}" event listener is not cancellable and so \`passive: true\` does nothing.`,
fix(fixer) {
const removals = []
if (l === 1) {
removals.push(options)
removals.push(...source.getTokensBetween(listener, options))
} else {
removals.push(passiveProp)
if (i > 0) {
removals.push(...source.getTokensBetween(options.properties[i - 1], passiveProp))
} else {
removals.push(...source.getTokensBetween(passiveProp, options.properties[i + 1]))
}
}
return removals.map(t => fixer.remove(t))
}
})
}
}
}
}
}

View file

@ -0,0 +1,28 @@
const observerMap = {
scroll: 'IntersectionObserver',
resize: 'ResizeObserver'
}
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow poorly performing event listeners',
url: require('../url')(module)
},
schema: []
},
create(context) {
return {
['CallExpression[callee.property.name="addEventListener"]']: function (node) {
const [name] = node.arguments
if (name.type !== 'Literal') return
if (!(name.value in observerMap)) return
context.report({
node,
message: `Avoid using "${name.value}" event listener. Consider using ${observerMap[name.value]} instead`
})
}
}
}
}

View file

@ -0,0 +1,27 @@
const passiveEventListenerNames = new Set(['touchstart', 'touchmove', 'wheel', 'mousewheel'])
const propIsPassiveTrue = prop => prop.key && prop.key.name === 'passive' && prop.value && prop.value.value === true
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce marking high frequency event handlers as passive',
url: require('../url')(module)
},
schema: []
},
create(context) {
return {
['CallExpression[callee.property.name="addEventListener"]']: function (node) {
const [name, listener, options] = node.arguments
if (!listener) return
if (name.type !== 'Literal') return
if (!passiveEventListenerNames.has(name.value)) return
if (options && options.type === 'ObjectExpression' && options.properties.some(propIsPassiveTrue)) return
context.report({node, message: `High Frequency Events like "${name.value}" should be \`passive: true\``})
}
}
}
}

View file

@ -0,0 +1,36 @@
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow unesaped HTML literals',
url: require('../url')(module)
},
schema: []
},
create(context) {
const htmlOpenTag = /^<[a-zA-Z]/
const message = 'Unescaped HTML literal. Use html`` tag template literal for secure escaping.'
return {
Literal(node) {
if (!htmlOpenTag.test(node.value)) return
context.report({
node,
message
})
},
TemplateLiteral(node) {
if (!htmlOpenTag.test(node.quasis[0].value.raw)) return
if (!node.parent.tag || node.parent.tag.name !== 'html') {
context.report({
node,
message
})
}
}
}
}
}