Skip to content

Conversation

@jackw
Copy link
Collaborator

@jackw jackw commented Feb 4, 2026

What this PR does / why we need it:

Whilst going through the list of troublesome dependencies and checking their source code to confirm if they're react 18 & 19 compatible I discovered a lot of false positives mostly due to matching function names like render or findDOMNode.

We've also had a report of issues being raised by the react-detect cli for externalised dependencies which feels wrong as most plugins will use these externalised dependencies but none of the code is bundled.

Originally I'd planned to make this lib as naive as possible but having thought some more on it I think the solution I've got here to track imports from packages (e.g. react / react-dom) in the source files and then walk the AST checking if the Identifier name is listed in the imports should work. You can't import findDOMNode and then have a function with the same name in a js module, right?

For any deps that are externalised (either directly or indirectly due to their parent deps) I'm now filtering them out.

Which issue(s) this PR fixes:

Fixes #

Special notes for your reviewer:

📦 Published PR as canary version: Canary Versions

✨ Test out this PR locally via:

npm install @grafana/create-plugin@6.8.4-canary.2434.21668000529.0
npm install @grafana/plugin-docs-renderer@0.0.2-canary.2434.21668000529.0
npm install @grafana/react-detect@0.5.2-canary.2434.21668000529.0
# or 
yarn add @grafana/create-plugin@6.8.4-canary.2434.21668000529.0
yarn add @grafana/plugin-docs-renderer@0.0.2-canary.2434.21668000529.0
yarn add @grafana/react-detect@0.5.2-canary.2434.21668000529.0

@jackw jackw self-assigned this Feb 4, 2026
@jackw jackw requested a review from a team as a code owner February 4, 2026 10:15
@jackw jackw added the patch Increment the patch version when merged label Feb 4, 2026
@jackw jackw requested a review from a team as a code owner February 4, 2026 10:15
Copilot AI review requested due to automatic review settings February 4, 2026 10:15
@jackw jackw added the release Create a release when this pr is merged label Feb 4, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Feb 4, 2026

Hello! 👋 This repository uses Auto for releasing packages using PR labels.

✨ This PR can be merged and will trigger a new patch release.
NOTE: When merging a PR with the release label please avoid merging another PR. For further information see here.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to reduce false positives in react-detect by (1) tracking whether flagged identifiers are actually imported from react / react-dom, and (2) filtering out issues coming from Grafana-externalized dependencies.

Changes:

  • Added trackImportsFromPackage() AST utility to collect default/named imports (and CommonJS require) from a specific package.
  • Updated several pattern matchers to consult tracked imports when detecting findDOMNode, ReactDOM.render, unmountComponentAtNode, and createFactory, plus expanded matcher test coverage.
  • Filtered out dependency matches whose (root) dependency is Grafana-externalized, plus added results tests for the filtering behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/react-detect/src/utils/ast.ts Adds import/require tracking helper for a given package.
packages/react-detect/src/patterns/matcher.ts Uses import tracking to reduce false positives for several patterns.
packages/react-detect/src/patterns/matcher.test.ts Adds test cases for import-aware matching behavior.
packages/react-detect/src/results.ts Filters out externalized dependency matches before reporting.
packages/react-detect/src/results.test.ts Adds tests validating externalized dependency filtering.

Comment on lines +74 to +75
if (match.type === 'dependency' && match.rootDependency && isExternal(match.rootDependency)) {
return false;
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

Externalized dependency filtering currently only triggers when match.rootDependency is set. When dependency analysis is skipped or dependency loading fails (depContext null), rootDependency is typically undefined, so direct externalized deps (e.g. react, lodash, @grafana/*) will no longer be filtered and false positives will return. Consider falling back to match.packageName (or match.rootDependency ?? match.packageName) when checking isExternal, and add a test for the depContext/rootDependency-missing scenario.

Suggested change
if (match.type === 'dependency' && match.rootDependency && isExternal(match.rootDependency)) {
return false;
if (match.type === 'dependency') {
const depName = match.rootDependency ?? match.packageName;
if (depName && isExternal(depName)) {
return false;
}

Copilot uses AI. Check for mistakes.
node.callee.property.name === 'findDOMNode'
) {
const objectName = node.callee.object.name;
if (imports.defaultImports.has(objectName) || objectName === 'ReactDOM') {
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

These matchers still accept objectName === 'ReactDOM' even when there is no import/require from 'react-dom' in the file. That reintroduces the same class of false positives this PR is trying to eliminate (any local variable/object named ReactDOM with a .render/.findDOMNode property will match). Consider only matching MemberExpression calls when the object identifier is known to be imported/required from 'react-dom' (including default/namespace/require), rather than special-casing the literal name 'ReactDOM'.

Suggested change
if (imports.defaultImports.has(objectName) || objectName === 'ReactDOM') {
if (imports.defaultImports.has(objectName)) {

Copilot uses AI. Check for mistakes.
node.callee.property.name === 'createFactory' &&
hasValidArgs
) {
if (imports.defaultImports.has(node.callee.object.name) || node.callee.object.name === 'React') {
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

Similarly, allowing node.callee.object.name === 'React' without verifying an import/require from 'react' can match unrelated local identifiers named React and cause false positives. To keep the new import-tracking approach consistent, consider requiring the object identifier to be one of the tracked imports (default/namespace/require) before reporting createFactory usage.

Suggested change
if (imports.defaultImports.has(node.callee.object.name) || node.callee.object.name === 'React') {
const calleeObjectName = node.callee.object.name;
if (
imports.defaultImports.has(calleeObjectName) ||
imports.namespaceImports.has(calleeObjectName) ||
imports.requireImports.has(calleeObjectName)
) {

Copilot uses AI. Check for mistakes.
Comment on lines 145 to 149
export function findFindDOMNode(ast: TSESTree.Program, code: string): PatternMatch[] {
const matches: PatternMatch[] = [];
const imports = trackImportsFromPackage(ast, 'react-dom');
const findDOMNodeLocalNames = imports.namedImports.get('findDOMNode') || new Set();

Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

Each of these matchers calls trackImportsFromPackage (full AST walk) and then does another full walk to find call sites. Since findPatternMatches invokes many matchers per file, this adds multiple extra traversals per file. Consider computing import tracking once per file (e.g., in findPatternMatches) and passing it into the relevant matchers, or memoizing trackImportsFromPackage by (ast, packageName), to reduce analysis time on large bundles.

Copilot uses AI. Check for mistakes.
@grafana-plugins-platform-bot grafana-plugins-platform-bot bot moved this from 📬 Triage to 🔬 In review in Plugins Platform / Grafana Community Feb 4, 2026
Copy link
Contributor

@hugohaggmark hugohaggmark left a comment

Choose a reason for hiding this comment

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

Great with all the tests 🚀

Comment on lines +156 to +162
node.callee &&
node.callee.type === 'MemberExpression' &&
node.callee.object &&
node.callee.object.type === 'Identifier' &&
node.callee.property &&
node.callee.property.type === 'Identifier' &&
node.callee.property.name === 'findDOMNode'
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: maybe this could be simplified?

Suggested change
node.callee &&
node.callee.type === 'MemberExpression' &&
node.callee.object &&
node.callee.object.type === 'Identifier' &&
node.callee.property &&
node.callee.property.type === 'Identifier' &&
node.callee.property.name === 'findDOMNode'
node.callee?.type === 'MemberExpression' &&
node.callee.object?.type === 'Identifier' &&
node.callee.property?.type === 'Identifier' &&
node.callee.property?.name === 'findDOMNode'

Comment on lines +171 to +173
if (node.callee && node.callee.type === 'Identifier' && findDOMNodeLocalNames.has(node.callee.name)) {
matches.push(createPatternMatch(node, 'findDOMNode', code));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: maybe this could be simplified?

Suggested change
if (node.callee && node.callee.type === 'Identifier' && findDOMNodeLocalNames.has(node.callee.name)) {
matches.push(createPatternMatch(node, 'findDOMNode', code));
}
if (node.callee?.type === 'Identifier' && findDOMNodeLocalNames.has(node.callee.name)) {
matches.push(createPatternMatch(node, 'findDOMNode', code));
}

Comment on lines +192 to +198
node.callee &&
node.callee.type === 'MemberExpression' &&
node.callee.object &&
node.callee.object.type === 'Identifier' &&
node.callee.property &&
node.callee.property.type === 'Identifier' &&
node.callee.property.name === 'render' &&
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: same as previous comments

Comment on lines +229 to +235
node.callee &&
node.callee.type === 'MemberExpression' &&
node.callee.object &&
node.callee.object.type === 'Identifier' &&
node.callee.property &&
node.callee.property.type === 'Identifier' &&
node.callee.property.name === 'unmountComponentAtNode' &&
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: same as previous comments

Comment on lines +266 to +274
node.callee &&
node.callee.type === 'MemberExpression' &&
node.callee.object &&
node.callee.object.type === 'Identifier' &&
node.callee.property &&
node.callee.property.type === 'Identifier' &&
node.callee.property.name === 'createFactory' &&
hasValidArgs
) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: same as previous comments

* // For code: import { render as r } from 'react-dom'
* // Returns: { defaultImports: Set(), namedImports: Map([['render', Set(['r'])]]) }
*/
export function trackImportsFromPackage(ast: TSESTree.Program, packageName: string): ImportTracking {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: this function seems a bit overly complex and some new functions could potentially be extracted from it

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, it's not the nicest and I'm probably going too fast here. Would splitting it into functions for dealing with es6 import, cjs require and named imports be helpful? Maybe you had other ideas?

Copy link
Contributor

Choose a reason for hiding this comment

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

no I think that would do nicely, but these are just nits, no real need todo anything about it

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'll make the change as I think it fair we hold the door open for the next engineer that has to venture down this (dark) path. 🐉

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

Labels

patch Increment the patch version when merged release Create a release when this pr is merged

Projects

Status: 🔬 In review

Development

Successfully merging this pull request may close these issues.

2 participants