Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/core/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@
- [getJsxConfigFromContext](functions/getJsxConfigFromContext.md)
- [getJsxElementType](functions/getJsxElementType.md)
- [getPhaseKindOfFunction](functions/getPhaseKindOfFunction.md)
- [hasJsxAttribute](functions/hasJsxAttribute.md)
- [hasNoneOrLooseComponentName](functions/hasNoneOrLooseComponentName.md)
- [isAssignmentToThisState](functions/isAssignmentToThisState.md)
- [isChildrenOfCreateElement](functions/isChildrenOfCreateElement.md)
Expand Down
20 changes: 15 additions & 5 deletions packages/core/docs/functions/getJsxAttribute.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,35 @@

# Function: getJsxAttribute()

> **getJsxAttribute**(`context`, `attributes`, `initialScope?`): (`name`) => `undefined` \| `TSESTreeJSXAttributeLike`
> **getJsxAttribute**(`context`, `node`, `initialScope?`): (`name`) => `undefined` \| `JSXAttribute` \| `JSXSpreadAttribute`

Get a function to find JSX attributes by name, considering direct attributes and spread attributes.

## Parameters

### context

`RuleContext`

### attributes
The ESLint rule context

### node

`JSXElement`

`TSESTreeJSXAttributeLike`[]
The JSX element node

### initialScope?

`Scope`

Optional initial scope for variable resolution

## Returns

> (`name`): `undefined` \| `TSESTreeJSXAttributeLike`
A function that takes an attribute name and returns the corresponding JSX attribute node or undefined

> (`name`): `undefined` \| `JSXAttribute` \| `JSXSpreadAttribute`

### Parameters

Expand All @@ -34,4 +44,4 @@

### Returns

`undefined` \| `TSESTreeJSXAttributeLike`
`undefined` \| `JSXAttribute` \| `JSXSpreadAttribute`
43 changes: 0 additions & 43 deletions packages/core/docs/functions/hasJsxAttribute.md

This file was deleted.

49 changes: 19 additions & 30 deletions packages/core/src/jsx/jsx-attribute.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,47 @@
import type * as AST from "@eslint-react/ast";
import type { RuleContext } from "@eslint-react/kit";
import { findProperty, findVariable, getVariableDefinitionNode } from "@eslint-react/var";
import type { Scope } from "@typescript-eslint/scope-manager";
import type { TSESTree } from "@typescript-eslint/types";
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";

import type { Scope } from "@typescript-eslint/scope-manager";
import { getJsxAttributeName } from "./jsx-attribute-name";

export function getJsxAttribute(
context: RuleContext,
attributes: AST.TSESTreeJSXAttributeLike[],
initialScope?: Scope,
) {
/**
* Get a function to find JSX attributes by name, considering direct attributes and spread attributes.
* @param context The ESLint rule context
* @param node The JSX element node
* @param initialScope Optional initial scope for variable resolution
* @returns A function that takes an attribute name and returns the corresponding JSX attribute node or undefined
*/
export function getJsxAttribute(context: RuleContext, node: TSESTree.JSXElement, initialScope?: Scope) {
const scope = initialScope ?? context.sourceCode.getScope(node);
const attributes = node.openingElement.attributes;
/**
* Find a JSX attribute by name, considering both direct attributes and spread attributes.
* @param name The name of the attribute to find
* @returns The JSX attribute node if found, otherwise undefined
*/
return (name: string) => {
return attributes.findLast((attr) => {
// Case 1: Direct JSX attribute (e.g., className="value")
if (attr.type === T.JSXAttribute) {
return getJsxAttributeName(context, attr) === name;
}
// For spread attributes, we need a scope to resolve variables
if (initialScope == null) return false;
switch (attr.argument.type) {
// Case 2: Spread from variable (e.g., {...props})
case T.Identifier: {
const variable = findVariable(attr.argument.name, initialScope);
const variable = findVariable(attr.argument.name, scope);
const variableNode = getVariableDefinitionNode(variable, 0);
if (variableNode?.type === T.ObjectExpression) {
return findProperty(name, variableNode.properties, initialScope) != null;
return findProperty(name, variableNode.properties, scope) != null;
}
return false;
}
// Case 3: Spread from object literal (e.g., {{...{prop: value}}})
case T.ObjectExpression:
return findProperty(name, attr.argument.properties, initialScope) != null;
return findProperty(name, attr.argument.properties, scope) != null;
}
return false;
});
};
}

/**
* Checks if a JSX element has a specific attribute
*
* @param context - ESLint rule context
* @param name - Name of the attribute to check for
* @param attributes - List of JSX attributes from opening element
* @param initialScope - Optional scope for resolving variables in spread attributes
* @returns boolean indicating whether the attribute exists
*/
export function hasJsxAttribute(
context: RuleContext,
name: string,
attributes: TSESTree.JSXOpeningElement["attributes"],
initialScope?: Scope,
) {
return getJsxAttribute(context, attributes, initialScope)(name) != null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,13 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {

return {
JSXElement(node) {
const findJsxAttribute = getJsxAttribute(
context,
node.openingElement.attributes,
context.sourceCode.getScope(node),
);
const findJsxAttribute = getJsxAttribute(context, node);
if (findJsxAttribute(DSIH) == null) return;
const children = findJsxAttribute("children") ?? node.children.find(isSignificantChildren);
if (children == null) return;
const childrenPropOrNode = findJsxAttribute("children") ?? node.children.find(isSignificantChildren);
if (childrenPropOrNode == null) return;
context.report({
messageId: "noDangerouslySetInnerhtmlWithChildren",
node: children,
node: childrenPropOrNode,
});
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,11 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
if (!context.sourceCode.text.includes(DSIH)) return {};
return {
JSXElement(node) {
const findJsxAttribute = getJsxAttribute(
context,
node.openingElement.attributes,
context.sourceCode.getScope(node),
);
const attr = findJsxAttribute(DSIH);
if (attr == null) return;
const dsihProp = getJsxAttribute(context, node)(DSIH);
if (dsihProp == null) return;
context.report({
messageId: "noDangerouslySetInnerhtml",
node: attr,
node: dsihProp,
});
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,7 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
return;
}

const findJsxAttribute = getJsxAttribute(
context,
node.openingElement.attributes,
context.sourceCode.getScope(node),
);

if (findJsxAttribute("type") != null) {
if (getJsxAttribute(context, node)("type") != null) {
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ export default createRule<[], MessageID>({
meta: {
type: "problem",
docs: {
description: "Enforces explicit `sandbox` attribute for `iframe` elements.",
description: "Enforces explicit `sandbox` prop for `iframe` elements.",
[Symbol.for("rule_features")]: RULE_FEATURES,
},
fixable: "code",
hasSuggestions: true,
messages: {
addIframeSandbox: "Add 'sandbox' attribute with value '{{value}}'.",
noMissingIframeSandbox: "Add missing 'sandbox' attribute on 'iframe' component.",
addIframeSandbox: "Add 'sandbox' prop with value '{{value}}'.",
noMissingIframeSandbox: "Add missing 'sandbox' prop on 'iframe' component.",
},
schema: [],
},
Expand All @@ -44,17 +44,11 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
// If the element is not an iframe, we don't need to do anything.
if (domElementType !== "iframe") return;

const findJsxAttribute = getJsxAttribute(
context,
node.openingElement.attributes,
context.sourceCode.getScope(node),
);
// Find the 'sandbox' prop on the iframe element.
const sandboxProp = getJsxAttribute(context, node)("sandbox");

// Find the 'sandbox' attribute on the iframe element.
const sandboxAttr = findJsxAttribute("sandbox");

// If the 'sandbox' attribute is missing, report an error.
if (sandboxAttr == null) {
// If the 'sandbox' prop is missing, report an error.
if (sandboxProp == null) {
context.report({
messageId: "noMissingIframeSandbox",
node: node.openingElement,
Expand All @@ -71,23 +65,23 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
}

// Resolve the value of the 'sandbox' attribute.
const sandboxValue = resolveJsxAttributeValue(context, sandboxAttr);
// If the value is a static string, the attribute is correctly used.
const sandboxValue = resolveJsxAttributeValue(context, sandboxProp);
// If the value is a static string, the prop is correctly used.
if (typeof sandboxValue.toStatic("sandbox") === "string") return;

// If the value is not a static string (e.g., a variable), report an error.
context.report({
messageId: "noMissingIframeSandbox",
node: sandboxValue.node ?? sandboxAttr,
node: sandboxValue.node ?? sandboxProp,
suggest: [
{
messageId: "addIframeSandbox",
data: { value: "" },
fix(fixer) {
// Do not try to fix spread attributes.
if (sandboxValue.kind.startsWith("spread")) return null;
// Suggest replacing the attribute with a valid one.
return fixer.replaceText(sandboxAttr, `sandbox=""`);
// Suggest replacing the prop with a valid one.
return fixer.replaceText(sandboxProp, `sandbox=""`);
},
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,28 +36,22 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
return;
}

const findJsxAttribute = getJsxAttribute(
context,
node.openingElement.attributes,
context.sourceCode.getScope(node),
);

// Find the 'style' attribute on the element.
const styleAttr = findJsxAttribute("style");
if (styleAttr == null) {
// Find the 'style' prop on the element.
const styleProp = getJsxAttribute(context, node)("style");
if (styleProp == null) {
return;
}

// Resolve the static value of the 'style' attribute.
const styleValue = resolveJsxAttributeValue(context, styleAttr);
// Resolve the static value of the 'style' prop.
const styleValue = resolveJsxAttributeValue(context, styleProp);
const staticValue = styleValue.toStatic();

// If the resolved value is a string, report an error.
// e.g., <div style="color: red;" />
if (typeof staticValue === "string") {
context.report({
messageId: "noStringStyleProp",
node: styleValue.node ?? styleAttr,
node: styleValue.node ?? styleProp,
});
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,23 +50,18 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
if (resolver.resolve(node).domElementType !== "iframe") {
return;
}
const findJsxAttribute = getJsxAttribute(
context,
node.openingElement.attributes,
context.sourceCode.getScope(node),
);
const sandboxAttr = findJsxAttribute("sandbox");
if (sandboxAttr == null) {
const sandboxProp = getJsxAttribute(context, node)("sandbox");
if (sandboxProp == null) {
return;
}

const sandboxValue = resolveJsxAttributeValue(context, sandboxAttr);
const sandboxValue = resolveJsxAttributeValue(context, sandboxProp);
const sandboxValueStatic = sandboxValue.toStatic("sandbox");

if (isUnsafeSandboxCombination(sandboxValueStatic)) {
context.report({
messageId: "noUnsafeIframeSandbox",
node: sandboxValue.node ?? sandboxAttr,
node: sandboxValue.node ?? sandboxProp,
});
}
},
Expand Down
Loading