Skip to content

feat(lint): add Flow.Node render rule#618

Open
nandanmen wants to merge 1 commit into
cloudflare:mainfrom
nandanmen:nanda/DESENG-972
Open

feat(lint): add Flow.Node render rule#618
nandanmen wants to merge 1 commit into
cloudflare:mainfrom
nandanmen:nanda/DESENG-972

Conversation

@nandanmen

Copy link
Copy Markdown
Contributor

Summary

  • Add kumo/no-flow-node-custom-render to enforce Flow.Node render={<CustomComponent />} passthrough requirements.
  • Verify local custom render components forward refs and spread received props.
  • Enable the rule in root, package, and docs oxlint configs.

Tests

  • pnpm --filter @cloudflare/kumo exec vitest run --project=unit tests/lint/no-flow-node-custom-render.test.ts

  • pnpm exec tsc --noEmit --target ES2022 --module ES2022 --moduleResolution bundler --jsx react-jsx --strict --skipLibCheck --types vitest --allowJs --checkJs false tests/lint/no-flow-node-custom-render.test.ts lint/no-flow-node-custom-render.js

  • pnpm exec oxlint --config /var/folders/bj/9s0glzp52jvf5230xw5lk1rc0000gn/T/opencode/oxlint-touched.json lint/no-flow-node-custom-render.js lint/kumo-plugin.js packages/kumo/lint/no-flow-node-custom-render.js packages/kumo/lint/kumo-plugin.js packages/kumo/tests/lint/no-flow-node-custom-render.test.ts

  • pnpm exec prettier --check .oxlintrc.json .changeset/flow-node-render-lint.md lint/kumo-plugin.js lint/no-flow-node-custom-render.js packages/kumo/.oxlintrc.json packages/kumo/lint/kumo-plugin.js packages/kumo/lint/no-flow-node-custom-render.js packages/kumo/tests/lint/no-flow-node-custom-render.test.ts packages/kumo-docs-astro/.oxlintrc.json

  • Reviews

  • bonk has reviewed the change

  • automated review not possible because: bonk has not reviewed this change yet

  • Tests

  • Tests included/updated

  • Automated tests not possible - manual testing has been completed as follows: n/a

  • Additional testing not necessary because: n/a

@pkg-pr-new

pkg-pr-new Bot commented Jul 7, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/@cloudflare/kumo@618

commit: 362db03

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Docs Preview

View docs preview

Commit: 362db03

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor
Visual Regression Report — 26 changed, 15 unchanged

26 screenshot(s) with visual changes:

Button / Variant: Primary

473 px (0.47%) changed

Before After Diff
Before After Diff

Button / Variant: Destructive

1,030 px (1.01%) changed

Before After Diff
Before After Diff

Button / Variant: Outline

269 px (0.26%) changed

Before After Diff
Before After Diff

Button / Variant: Secondary Destructive

1,122 px (1.11%) changed

Before After Diff
Before After Diff

Button / Sizes

1,507 px (1.48%) changed

Before After Diff
Before After Diff

Button / Loading State

518 px (0.51%) changed

Before After Diff
Before After Diff

Button / Disabled State

117 px (0.12%) changed

Before After Diff
Before After Diff

Button / Link as Button

820 px (0.81%) changed

Before After Diff
Before After Diff

Dialog / Dialog With Actions

298 px (0.29%) changed

Before After Diff
Before After Diff

Dialog / Dialog Alert

1,635 px (1.61%) changed

Before After Diff
Before After Diff

Dialog / Dialog Confirmation

1,226 px (1.21%) changed

Before After Diff
Before After Diff

Dialog / Custom Max Width

875 px (0.86%) changed

Before After Diff
Before After Diff

Dialog / Dialog With Combobox

480 px (0.47%) changed

Before After Diff
Before After Diff

Dialog / Dialog With Dropdown

383 px (0.38%) changed

Before After Diff
Before After Diff

Select / Select Basic

1,097 px (1.08%) changed

Before After Diff
Before After Diff

Select / Select Without Label

723 px (0.71%) changed

Before After Diff
Before After Diff

Select / Select With Description

2,199 px (1.86%) changed

Before After Diff
Before After Diff

Select / Select With Error

1,927 px (1.63%) changed

Before After Diff
Before After Diff

Select / Select Placeholder

1,308 px (1.29%) changed

Before After Diff
Before After Diff

Select / Select With Tooltip

1,062 px (1.05%) changed

Before After Diff
Before After Diff

Select / Select Loading

2,375 px (1.18%) changed

Before After Diff
Before After Diff

Select / Select Complex

1,279 px (1.08%) changed

Before After Diff
Before After Diff

Select / Select Disabled Options

1,217 px (1.2%) changed

Before After Diff
Before After Diff

Select / Select Disabled Items

793 px (0.78%) changed

Before After Diff
Before After Diff

Select / Select Grouped With Disabled

1,189 px (1.17%) changed

Before After Diff
Before After Diff

Select (Open)

2,509 px (0.01%) changed

Before After Diff
Before After Diff
15 screenshot(s) unchanged
  • Button / Basic
  • Button / Variant: Secondary
  • Button / Variant: Ghost
  • Button / With Icon
  • Button / Icon Only
  • Button / Title
  • Dialog / Dialog Basic
  • Dialog / Dialog Sizes
  • Dialog / Dialog With Select
  • Dialog (Open)
  • Select / Select Sizes
  • Select / Select Custom Rendering
  • Select / Select Multiple
  • Select / Select Grouped
  • Select / Select Long List

Generated by Kumo Visual Regression

@geoquant

geoquant commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

/bonk review

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good — a well-structured rule that directly maps to Flow.Node's cloneElement passthrough requirements.

A few notes:

  1. expressionContainsIdentifier false positive: it currently matches identifiers used as MemberExpression property names. If a developer names the ref param ref and writes ref={callbacks.ref}, the rule incorrectly counts that as forwarding the ref. Consider passing parent context through traverse so you can skip non-computed member properties.

  2. Missing tests for edge cases: default destructured params (({ ...rest } = {})), HOC-wrapped forwardRef (memo(forwardRef(...))), and aliased imports (import { forwardRef as fr }) are all supported by the implementation but untested.

  3. Duplication: The rule file is duplicated between lint/ and packages/kumo/lint/. I realize this is required because packages/kumo-docs-astro/.oxlintrc.json loads the root plugin while packages/kumo/.oxlintrc.json loads the package plugin, so it's consistent with the existing setup.

  4. Configs: Root, package, and docs configs all correctly enable the rule. The changeset is appropriately scoped to @cloudflare/kumo.

function expressionContainsIdentifier(node, names) {
let found = false;
traverse(node, {
Identifier(identifier) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expressionContainsIdentifier helper matches identifiers in any position, including MemberExpression property names. If a component names the ref param ref and writes ref={callbacks.ref}, the rule incorrectly counts this as forwarding the ref.

Consider passing parent context through traverse so you can skip non-computed member properties, or do a targeted walk instead of the generic one here.

Comment on lines +176 to +195
function getPropsParamNames(param) {
const names = new Set();
const unwrapped = unwrapExpression(param);

if (unwrapped?.type === "Identifier") {
names.add(unwrapped.name);
} else if (unwrapped?.type === "AssignmentPattern") {
for (const name of getPropsParamNames(unwrapped.left)) {
names.add(name);
}
} else if (unwrapped?.type === "ObjectPattern") {
for (const prop of unwrapped.properties ?? []) {
if (prop.type === "RestElement" && prop.argument?.type === "Identifier") {
names.add(prop.argument.name);
}
}
}

return names;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AssignmentPattern branch already handles default destructured params like ({ children, ...rest } = {}), but there is no test covering that path. Adding one would prevent regressions.

return null;
}

function findForwardRefRenderFunction(node, forwardRefNames) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

findForwardRefRenderFunction supports nested calls like memo(forwardRef(...)) through recursion. A test for this HOC-wrapped case would increase confidence that future changes don't break the recursion behavior.

return context.getReports();
}

describe("no-flow-node-custom-render", () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test suite doesn't exercise aliased forwardRef imports (e.g. import { forwardRef as fr } from 'react'). Adding a test would verify that the forwardRefNames tracking in ImportDeclaration works as intended.

@@ -0,0 +1,376 @@
import { defineRule } from "oxlint";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is duplicated in lint/no-flow-node-custom-render.js. I understand this is required because packages/kumo-docs-astro/.oxlintrc.json loads the root plugin while packages/kumo/.oxlintrc.json loads the package plugin, so the duplication is consistent with the existing architecture. No change needed.

@ask-bonk

ask-bonk Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

I've submitted a batch review on PR #618.

Summary of feedback:

  • Overall: The rule is well-structured and correctly enforces Flow.Node passthrough requirements.
  • Bug identified: expressionContainsIdentifier matches identifiers in any position, including MemberExpression property names. This means ref={callbacks.ref} would incorrectly count as forwarding the ref if the parameter is named ref.
  • Testing gaps: Suggested adding tests for:
    • Default destructured params (({ ...rest } = {}))
    • HOC-wrapped forwardRef (memo(forwardRef(...)))
    • Aliased forwardRef imports (import { forwardRef as fr })
  • File duplication: Noted that the rule is duplicated in both lint/ and packages/kumo/lint/, but this is required because two different oxlint configs load the two separate plugin files.
  • Config & changeset: Confirmed the rule is correctly enabled in root, package, and docs configs, and the changeset is appropriately scoped.

github run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants