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,31 @@
const {getProp} = require('jsx-ast-utils')
module.exports = {
meta: {
docs: {
description: '[aria-label] text should be formatted as you would visual text.',
url: require('../url')(module),
},
schema: [],
},
create(context) {
return {
JSXOpeningElement: node => {
const prop = getProp(node.attributes, 'aria-label')
if (!prop) return
const propValue = prop.value
if (propValue.type !== 'Literal') return
const ariaLabel = propValue.value
if (ariaLabel.match(/^[a-z]+.*$/)) {
context.report({
node,
message: '[aria-label] text should be formatted the same as you would visual text. Use sentence case.',
})
}
},
}
},
}

View file

@ -0,0 +1,73 @@
const {getProp, getPropValue} = require('jsx-ast-utils')
const {getElementType} = require('../utils/get-element-type')
const bannedLinkText = ['read more', 'here', 'click here', 'learn more', 'more']
/* Downcase and strip extra whitespaces and punctuation */
const stripAndDowncaseText = text => {
return text
.toLowerCase()
.replace(/[.,/#!$%^&*;:{}=\-_`~()]/g, '')
.replace(/\s{2,}/g, ' ')
.trim()
}
module.exports = {
meta: {
docs: {
description: 'disallow generic link text',
url: require('../url')(module),
},
deprecated: true,
replacedBy: ['jsx-a11y/anchor-ambiguous-text'],
schema: [],
},
create(context) {
return {
JSXOpeningElement: node => {
const elementType = getElementType(context, node)
if (elementType !== 'a') return
if (getProp(node.attributes, 'aria-labelledby')) return
let cleanTextContent // text content we can reliably fetch
const parent = node.parent
let jsxTextNode
if (parent.children && parent.children.length > 0 && parent.children[0].type === 'JSXText') {
jsxTextNode = parent.children[0]
cleanTextContent = stripAndDowncaseText(parent.children[0].value)
}
const ariaLabel = getPropValue(getProp(node.attributes, 'aria-label'))
const cleanAriaLabelValue = ariaLabel && stripAndDowncaseText(ariaLabel)
if (ariaLabel) {
if (bannedLinkText.includes(cleanAriaLabelValue)) {
context.report({
node,
message:
'Avoid setting generic link text like `Here`, `Click here`, `Read more`. Make sure that your link text is both descriptive and concise.',
})
}
if (cleanTextContent && !cleanAriaLabelValue.includes(cleanTextContent)) {
context.report({
node,
message: 'When using ARIA to set a more descriptive text, it must fully contain the visible label.',
})
}
} else {
if (cleanTextContent) {
if (!bannedLinkText.includes(cleanTextContent)) return
context.report({
node: jsxTextNode,
message:
'Avoid setting generic link text like `Here`, `Click here`, `Read more`. Make sure that your link text is both descriptive and concise.',
})
}
}
},
}
},
}

View file

@ -0,0 +1,66 @@
const {getProp, getPropValue} = require('jsx-ast-utils')
const {getElementType} = require('../utils/get-element-type')
const SEMANTIC_ELEMENTS = [
'a',
'button',
'summary',
'select',
'option',
'textarea',
'input',
'span',
'div',
'p',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'details',
'summary',
'dialog',
'tr',
'th',
'td',
'label',
]
const ifSemanticElement = (context, node) => {
const elementType = getElementType(context, node.openingElement, true)
for (const semanticElement of SEMANTIC_ELEMENTS) {
if (elementType === semanticElement) {
return true
}
}
return false
}
module.exports = {
meta: {
docs: {
description: 'Guards against developers using the title attribute',
url: require('../url')(module),
},
schema: [],
},
create(context) {
return {
JSXElement: node => {
const elementType = getElementType(context, node.openingElement, true)
if (elementType !== `iframe` && ifSemanticElement(context, node)) {
const titleProp = getPropValue(getProp(node.openingElement.attributes, `title`))
if (titleProp) {
context.report({
node,
message: 'The title attribute is not accessible and should never be used unless for an `<iframe>`.',
})
}
}
},
}
},
}

View file

@ -0,0 +1,85 @@
const {getProp, getLiteralPropValue} = require('jsx-ast-utils')
const {getElementType} = require('../utils/get-element-type')
const {generateObjSchema} = require('eslint-plugin-jsx-a11y/lib/util/schemas')
const defaultClassName = 'sr-only'
const defaultcomponentName = 'VisuallyHidden'
const schema = generateObjSchema({
className: {type: 'string'},
componentName: {type: 'string'},
})
/** Note: we are not including input elements at this time
* because a visually hidden input field might cause a false positive.
* (e.g. fileUpload https://github.com/primer/react/pull/3492)
*/
const INTERACTIVE_ELEMENTS = ['a', 'button', 'summary', 'select', 'option', 'textarea']
const checkIfInteractiveElement = (context, node) => {
const elementType = getElementType(context, node.openingElement)
for (const interactiveElement of INTERACTIVE_ELEMENTS) {
if (elementType === interactiveElement) {
return true
}
}
return false
}
// if the node is visually hidden recursively check if it has interactive children
const checkIfVisuallyHiddenAndInteractive = (context, options, node, isParentVisuallyHidden) => {
const {className, componentName} = options
if (node.type === 'JSXElement') {
const classes = getLiteralPropValue(getProp(node.openingElement.attributes, 'className'))
const isVisuallyHiddenElement = node.openingElement.name.name === componentName
let hasSROnlyClass = false
if (classes != null) {
hasSROnlyClass = classes.includes(className)
}
let isHidden = false
if (hasSROnlyClass || isVisuallyHiddenElement || !!isParentVisuallyHidden) {
if (checkIfInteractiveElement(context, node)) {
return true
}
isHidden = true
}
if (node.children && node.children.length > 0) {
return (
typeof node.children?.find(child =>
checkIfVisuallyHiddenAndInteractive(context, options, child, !!isParentVisuallyHidden || isHidden),
) !== 'undefined'
)
}
}
return false
}
module.exports = {
meta: {
docs: {
description: 'Ensures that interactive elements are not visually hidden',
url: require('../url')(module),
},
schema: [schema],
},
create(context) {
const {options} = context
const config = options[0] || {}
const className = config.className || defaultClassName
const componentName = config.componentName || defaultcomponentName
return {
JSXElement: node => {
if (checkIfVisuallyHiddenAndInteractive(context, {className, componentName}, node, false)) {
context.report({
node,
message:
'Avoid visually hidding interactive elements. Visually hiding interactive elements can be confusing to sighted keyboard users as it appears their focus has been lost when they navigate to the hidden element.',
})
}
},
}
},
}

View file

@ -0,0 +1,61 @@
// @ts-check
const {aria, roles} = require('aria-query')
const {getPropValue, propName} = require('jsx-ast-utils')
const {getRole} = require('../utils/get-role')
module.exports = {
meta: {
docs: {
description:
'Enforce that elements with explicit or implicit roles defined contain only `aria-*` properties supported by that `role`.',
url: require('../url')(module),
},
schema: [],
},
create(context) {
return {
JSXOpeningElement(node) {
// Get the elements explicit or implicit role
const role = getRole(context, node)
// Return early if role could not be determined
if (!role) return
// Get allowed ARIA attributes:
// - From the role itself
let allowedProps = Object.keys(roles.get(role)?.props || {})
// - From parent roles
for (const parentRole of roles.get(role)?.superClass.flat() ?? []) {
allowedProps = allowedProps.concat(Object.keys(roles.get(parentRole)?.props || {}))
}
// Dedupe, for performance
allowedProps = Array.from(new Set(allowedProps))
// Get prohibited ARIA attributes:
// - From the role itself
let prohibitedProps = roles.get(role)?.prohibitedProps || []
// - From parent roles
for (const parentRole of roles.get(role)?.superClass.flat() ?? []) {
prohibitedProps = prohibitedProps.concat(roles.get(parentRole)?.prohibitedProps || [])
}
// - From comparing allowed vs all ARIA attributes
prohibitedProps = prohibitedProps.concat(aria.keys().filter(x => !allowedProps.includes(x)))
// Dedupe, for performance
prohibitedProps = Array.from(new Set(prohibitedProps))
for (const prop of node.attributes) {
// Return early if prohibited ARIA attribute is set to an ignorable value
if (getPropValue(prop) == null || prop.type === 'JSXSpreadAttribute') return
if (prohibitedProps?.includes(propName(prop))) {
context.report({
node,
message: `The attribute ${propName(prop)} is not supported by the role ${role}.`,
})
}
}
},
}
},
}

View file

@ -0,0 +1,45 @@
const {hasProp} = require('jsx-ast-utils')
const {getElementType} = require('../utils/get-element-type')
module.exports = {
meta: {
docs: {
description: 'SVGs must have an accessible name',
url: require('../url')(module),
},
schema: [],
},
create(context) {
return {
JSXOpeningElement: node => {
const elementType = getElementType(context, node)
if (elementType !== 'svg') return
// Check if there is a nested title element that is the first non-whitespace child of the `<svg>`
const childrenWithoutWhitespace = node.parent.children?.filter(({type, value}) =>
type === 'JSXText' ? value.trim() !== '' : type !== 'JSXText',
)
const hasNestedTitleAsFirstChild =
childrenWithoutWhitespace?.[0]?.type === 'JSXElement' &&
childrenWithoutWhitespace?.[0]?.openingElement?.name?.name === 'title'
// Check if `aria-label` or `aria-labelledby` is set
const hasAccessibleName = hasProp(node.attributes, 'aria-label') || hasProp(node.attributes, 'aria-labelledby')
// Check if SVG is decorative
const isDecorative =
hasProp(node.attributes, 'role', 'presentation') || hasProp(node.attributes, 'aria-hidden', 'true')
if (elementType === 'svg' && !hasAccessibleName && !isDecorative && !hasNestedTitleAsFirstChild) {
context.report({
node,
message:
'`<svg>` must have an accessible name. Set `aria-label` or `aria-labelledby`, or nest a `<title>` element. However, if the `<svg>` is purely decorative, hide it with `aria-hidden="true"` or `role="presentation"`.',
})
}
},
}
},
}

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,52 @@
// This is adapted from https://github.com/selaux/eslint-plugin-filenames since it's no longer actively maintained
// and needed a fix for eslint v9
const path = require('path')
const parseFilename = require('../utils/parse-filename')
const getExportedName = require('../utils/get-exported-name')
const isIgnoredFilename = require('../utils/is-ignored-filename')
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'ensure filenames match a regex naming convention',
url: require('../url')(module),
},
schema: {
type: 'array',
minItems: 0,
maxItems: 1,
items: [
{
type: 'string',
},
],
},
},
create(context) {
// GitHub's default is kebab case or one hump camel case
const defaultRegexp = /^[a-z0-9-]+(.[a-z0-9-]+)?$/
const conventionRegexp = context.options[0] ? new RegExp(context.options[0]) : defaultRegexp
const ignoreExporting = context.options[1] ? context.options[1] : false
return {
Program(node) {
const filename = context.getFilename()
const absoluteFilename = path.resolve(filename)
const parsed = parseFilename(absoluteFilename)
const shouldIgnore = isIgnoredFilename(filename)
const isExporting = Boolean(getExportedName(node))
const matchesRegex = conventionRegexp.test(parsed.name)
if (shouldIgnore) return
if (ignoreExporting && isExporting) return
if (!matchesRegex) {
context.report(node, "Filename '{{name}}' does not match the regex naming convention.", {
name: parsed.base,
})
}
},
}
},
}

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,29 @@
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow creating dynamic script tags',
url: require('../url')(module),
},
schema: [],
},
create(context) {
return {
'CallExpression[callee.property.name="createElement"][arguments.length > 0]': function (node) {
if (node.arguments[0].value !== 'script') return
context.report({
node: node.arguments[0],
message: "Don't create dynamic script tags, add them in the server template instead.",
})
},
'AssignmentExpression[left.property.name="type"][right.value="text/javascript"]': function (node) {
context.report({
node: node.right,
message: "Don't create dynamic script tags, add them in the server template instead.",
})
},
}
},
}

View file

@ -0,0 +1,36 @@
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow implicit global variables',
url: require('../url')(module),
},
schema: [],
},
create(context) {
const sourceCode = context.sourceCode ?? context.getSourceCode()
return {
Program(node) {
const scope = sourceCode.getScope(node) ? sourceCode.getScope(node) : 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,21 @@
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow `Element.prototype.innerHTML` in favor of `Element.prototype.textContent`',
url: require('../url')(module),
},
schema: [],
},
create(context) {
return {
'MemberExpression[property.name=innerHTML]': function (node) {
context.report({
node: node.property,
message: 'Using innerHTML poses a potential security risk and should not be used. Prefer using textContent.',
})
},
}
},
}

View file

@ -0,0 +1,34 @@
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 the member expression is part of a call expression like `.innerText()` then it is not the same
// as the `Element.innerText` property, and should not trigger a warning
if (node.parent.type === 'CallExpression') return
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,59 @@
const passiveEventListenerNames = new Set([
'touchstart',
'touchmove',
'touchenter',
'touchend',
'touchleave',
'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,35 @@
const passiveEventListenerNames = new Set([
'touchstart',
'touchmove',
'touchenter',
'touchend',
'touchleave',
'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 unescaped 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,
})
}
},
}
},
}