Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
5b84916
format: run lint:fix
shervElmi Feb 25, 2026
7d48853
Add ESLint rule to disallow unnecessary optional chaining on querySel…
shervElmi Feb 25, 2026
43e2591
Add ESLint rule to detect and warn about repeated DOM queries with th…
shervElmi Feb 25, 2026
dbc1f29
Add ESLint rule to detect appendChild calls in loops and suggest usin…
shervElmi Feb 25, 2026
04d0653
Add ESLint rules for DOM optimization and enforce JavaScript coding s…
shervElmi Feb 25, 2026
cdc6685
Update ESLint rules documentation with new DOM optimization rules and…
shervElmi Feb 25, 2026
befa405
Refactor: convert var declarations to const/let following ESLint pref…
shervElmi Feb 25, 2026
fd693fc
Add id prop to BaseControl in RadioControl component
shervElmi Feb 25, 2026
d0e07b5
Add eslint-disable-line comment for no-unused-expressions in Cypress …
shervElmi Feb 25, 2026
9642772
Remove unused sourceCode variable from no-repeated-selector ESLint rule
shervElmi Feb 25, 2026
cf8960a
Initialize variables at declaration to prevent undefined values and i…
shervElmi Feb 25, 2026
4de4244
Refactor addon-state.js to use array destructuring for cleaner elemen…
shervElmi Mar 3, 2026
d5257f8
Refactor code to use destructuring, optional chaining, and improve sw…
shervElmi Mar 3, 2026
bc28623
Disable ESLint rules to suppress DeepSource false positives for legac…
shervElmi Mar 3, 2026
a90c155
Replace double negation operator with explicit Boolean() conversion f…
shervElmi Mar 3, 2026
2c7c958
Remove unused global comments and fix escaped forward slash in URL path
shervElmi Mar 3, 2026
ebc5d89
Remove unused global comment and consolidate multi-line template lite…
shervElmi Mar 3, 2026
f56c108
revert: undo manual JS-0119 init-declarations fixes
shervElmi Mar 3, 2026
f089f93
fix: suppress DeepSource JS-0085 and webpack resolver errors
shervElmi Mar 3, 2026
8caed08
Add rule to prevent automatic commits in code change principles
shervElmi Mar 3, 2026
6071ded
Suppress ESLint prefer-const warning for intentional let declaration …
shervElmi Mar 3, 2026
85b3274
Add hasOwnProperty checks to for-in loops and optional chaining for o…
shervElmi Mar 3, 2026
56ed772
Move fieldID declaration to top of checkValidity function and use des…
shervElmi Mar 3, 2026
68994cc
Disable DeepSource short variable name rules to allow project convent…
shervElmi Mar 3, 2026
4f731f5
Remove DeepSource short variable name rules and webpack resolver, add…
shervElmi Mar 3, 2026
9ecd0e5
Remove eslint-import-resolver-webpack dependency and add trailing new…
shervElmi Mar 3, 2026
4e2902c
Merge remote-tracking branch 'origin/HEAD' into feature/add-new-ff-es…
shervElmi Mar 3, 2026
fc73039
Replace Object.prototype.hasOwnProperty.call with Object.hasOwn and i…
shervElmi Mar 3, 2026
2d78a29
Improve variable naming in scroll position calculation by replacing s…
shervElmi Mar 3, 2026
af49f23
npm run build
shervElmi Mar 3, 2026
00cd712
Improve code formatting by adding const to imageSize declaration and …
shervElmi Mar 3, 2026
f2bca79
Move variable declarations to point of use and convert var to const/l…
shervElmi Mar 3, 2026
8e98b19
Move variable declarations to point of use and convert var to const/l…
shervElmi Mar 3, 2026
e5f860c
Merge branch 'master' into feature/add-new-ff-eslint-rules
Crabcyborg Mar 4, 2026
b512e0a
Remove prefer template oxlint rule exception
Crabcyborg Mar 4, 2026
4d12b4e
Prefer const
Crabcyborg Mar 4, 2026
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
97 changes: 91 additions & 6 deletions .deepsource.toml
Original file line number Diff line number Diff line change
@@ -1,28 +1,113 @@
version = 1

# 1. Global Exclude Patterns
# Tell DeepSource which files to completely ignore (e.g., external libraries).
exclude_patterns = [
"**/node_modules/**",
"**/vendor/**",
"**/venv/**",
"**/dist/**",
"**/build/**"
"**/build/**",
"**/coverage/**",
"**/*.min.js",
"**/*.js.map",
"js/formidable_blocks.js",
"js/formidable_overlay.js",
"js/form-templates.js",
"js/formidable_dashboard.js",
"js/onboarding-wizard.js",
"js/addons-page.js",
"js/formidable_styles.js",
"js/formidable_admin.js",
"js/bootstrap-multiselect.js",
"js/formidable-settings-components.js",
"js/formidable-web-components.js",
"js/frm_testing_mode.js",
"js/welcome-tour.js",
"eslint-rules/**"
]

# 2. Test Patterns
# Tell DeepSource which files are tests. This prevents false positives
# (e.g., allowing "assert" statements in test files).
test_patterns = [
"**/tests/**"
"**/tests/**",
"**/phpunit/**",
"**/cypress/**"
]

# 3. Analyzers
# Add a block like this for every language you use.
[[analyzers]]
name = "php"
enabled = true

[[analyzers]]
name = "javascript"
enabled = true

[analyzers.meta]
plugins = ["react"]
dependency_file_paths = ["package.json"]
environment = [
"browser",
"jquery",
"cypress",
"mocha"
]
module_system = "es-modules"
globals = [
# WordPress core globals
"ajaxurl",
"wp",
"wpApiSettings",
"tinyMCE",
"tb_remove",
"adminpage",
"pagenow",
# Google reCAPTCHA / hCaptcha / Turnstile
"grecaptcha",
"hcaptcha",
"turnstile",
# Formidable globals (via wp_localize_script)
"frmGlobal",
"frm_js",
"frm_admin_js",
"frmDom",
"frmFormTemplatesVars",
"FrmDeactivationFeedbackI18n",
"formidable_form_selector",
"frmSettings",
"s11FloatingLinksData",
"frmPlugSearch",
"frmAddonsVars",
"frmOnboardingWizardVars",
"frmApplicationsVars",
"frmWelcomeTourVars",
"frm_stripe_vars",
"frm_trans_vars",
"frmSquareVars",
# Formidable window globals
"frmFrontForm",
"frmAdminBuild",
"frmAdminBuildJS",
"FrmFormsConnect",
"frmAdminPopup",
"frmOverlay",
"frmDropdownComponent",
"frmStylerFunctions",
"frmProForm",
"frmThemeOverride_jsErrors",
"frmThemeOverride_frmPlaceError",
"frmThemeOverride_frmAfterSubmit",
"frm_add_logic_row",
"frm_remove_tag",
"frm_show_div",
"frmCheckAll",
"frmCheckAllLevel",
"frmGetFieldValues",
"frmImportCsv",
"FormidablePSH",
"__FRMURLVARS",
"frm_password_checks",
"formidable_block_calculator",
"formidable_view_selector",
"frmdates_admin_js",
"frm_abdn"
]
1 change: 0 additions & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
"eslint/sort-keys": "off",
"eslint/no-magic-numbers": "off",
"eslint/radix": "off",
"eslint/prefer-template": "off",
"eslint/prefer-const": "off",
"eslint/prefer-object-spread": "off",
"eslint/prefer-destructuring": "off",
Expand Down
1 change: 1 addition & 0 deletions .windsurf/rules/enterprise/code-change-principles.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Critical principles for enterprise-level plugin development.
4. **No custom solutions**: Never invent new patterns. Use existing ones or search the web to review best practices, then follow the official WordPress standards or VIP guidelines.
5. **User changes are final**: If user makes manual changes, treat as authoritative
6. **Multi-issue fixes**: When fixing multiple issues in one request, run all 6 phases independently for each issue. Findings from one issue's phases may inform the next, but every phase is mandatory for every issue, even when issues share a root cause.
7. **Don't commit changes**: Never commit changes locally or remotely. I’ll handle that manually.

---

Expand Down
51 changes: 41 additions & 10 deletions .windsurf/skills/eslint-rules/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,25 @@ Workflow for creating and maintaining custom ESLint rules in the Formidable Form

Custom ESLint rules live in `/eslint-rules/` at the project root.

```
```text
eslint-rules/
├── index.js # Plugin entry point, exports all rules
└── rules/ # Individual rule files
├── prefer-strict-comparison.js
├── no-redundant-undefined-check.js
├── prefer-includes.js
└── no-typeof-undefined.js
├── no-typeof-undefined.js
├── no-optional-chaining-queryselectorall.js
├── no-repeated-selector.js
└── prefer-document-fragment.js
```

The plugin is imported in `eslint.config.mjs` as `formidable` and rules are referenced as `formidable/<rule-name>`.

### Release Exclusions

The `/eslint-rules/` directory is excluded from releases via:

- `.gitattributes`: `export-ignore`
- `bin/zip-plugin.sh`: `-x "*/eslint-rules/*"`

Expand Down Expand Up @@ -65,20 +69,47 @@ Detects `.indexOf()` comparisons with `-1` and suggests `.includes()` instead. C

- **Fixable:** Yes
- **Patterns caught:**
- `arr.indexOf(x) !== -1` and yoda `-1 !== arr.indexOf(x)`
- `arr.indexOf(x) === -1` and yoda `-1 === arr.indexOf(x)`
- `arr.indexOf(x) > -1` and yoda `-1 < arr.indexOf(x)`
- `arr.indexOf(x) >= 0`
- `arr.indexOf(x) !== -1` and yoda `-1 !== arr.indexOf(x)`
- `arr.indexOf(x) === -1` and yoda `-1 === arr.indexOf(x)`
- `arr.indexOf(x) > -1` and yoda `-1 < arr.indexOf(x)`
- `arr.indexOf(x) >= 0`

### formidable/no-typeof-undefined

Detects `typeof x === 'undefined'` and yoda `'undefined' === typeof x` patterns. Replaces with direct `x === undefined` comparison. Catches yoda-style patterns that `unicorn/no-typeof-undefined` misses.

- **Fixable:** Yes
- **Patterns caught:**
- `typeof x === 'undefined'` / `typeof x == 'undefined'`
- `'undefined' === typeof x` / `'undefined' == typeof x` (yoda)
- Both `===`/`!==` and `==`/`!=` variants
- `typeof x === 'undefined'` / `typeof x == 'undefined'`
- `'undefined' === typeof x` / `'undefined' == typeof x` (yoda)
- Both `===`/`!==` and `==`/`!=` variants

### formidable/no-optional-chaining-queryselectorall

Prevents unnecessary optional chaining (`?.`) on DOM methods that always return a value (never null/undefined). Methods like `querySelectorAll`, `getElementsByClassName`, and `children` always return a collection, so optional chaining is redundant.

- **Fixable:** Yes
- **Severity:** Error
- **Patterns caught:**
- `document.querySelectorAll(...)?.forEach`
- `element.getElementsByClassName(...)?.[0]`
- `element.children?.length`

### formidable/no-repeated-selector

Detects repeated calls to `querySelector` or `querySelectorAll` with the same selector string in the same function scope. Suggests caching the result in a variable.

- **Fixable:** No (requires developer judgment)
- **Severity:** Warning
- **Example:** Multiple `document.querySelector('.item')` calls should be cached

### formidable/prefer-document-fragment

Detects `appendChild`, `append`, or `prepend` calls inside loops. Suggests using `DocumentFragment` to batch DOM operations and prevent multiple reflows.

- **Fixable:** No (requires restructuring code)
- **Severity:** Warning
- **Loop types detected:** `for`, `for...of`, `for...in`, `while`, `forEach`, `map`

---

Expand Down Expand Up @@ -180,7 +211,7 @@ npm run lint:fix

## AST Explorer

Use https://astexplorer.net/ with the `espree` parser to inspect AST node types when developing rules. This helps identify the correct node visitors and property names.
Use <https://astexplorer.net/> with the `espree` parser to inspect AST node types when developing rules. This helps identify the correct node visitors and property names.

## Invocation

Expand Down
6 changes: 6 additions & 0 deletions eslint-rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@ const preferStrictComparison = require( './rules/prefer-strict-comparison' );
const noRedundantUndefinedCheck = require( './rules/no-redundant-undefined-check' );
const preferIncludes = require( './rules/prefer-includes' );
const noTypeofUndefined = require( './rules/no-typeof-undefined' );
const noOptionalChainingQueryselectorall = require( './rules/no-optional-chaining-queryselectorall' );
const noRepeatedSelector = require( './rules/no-repeated-selector' );
const preferDocumentFragment = require( './rules/prefer-document-fragment' );

module.exports = {
rules: {
'prefer-strict-comparison': preferStrictComparison,
'no-redundant-undefined-check': noRedundantUndefinedCheck,
'prefer-includes': preferIncludes,
'no-typeof-undefined': noTypeofUndefined,
'no-optional-chaining-queryselectorall': noOptionalChainingQueryselectorall,
'no-repeated-selector': noRepeatedSelector,
'prefer-document-fragment': preferDocumentFragment,
},
};
115 changes: 115 additions & 0 deletions eslint-rules/rules/no-optional-chaining-queryselectorall.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
'use strict';

/**
* Detects unnecessary optional chaining on querySelectorAll results.
* Since querySelectorAll always returns a NodeList (never null/undefined),
* and NodeList.forEach handles empty lists safely, the ?. is redundant.
*/
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'Disallow unnecessary optional chaining on querySelectorAll and similar DOM methods that never return null.',
},
fixable: 'code',
schema: [],
messages: {
unnecessaryChaining: 'Unnecessary optional chaining on {{method}}. {{reason}}',
},
},

create( context ) {
const sourceCode = context.sourceCode;

// Methods that always return a value (never null/undefined)
const alwaysReturns = new Set([
'querySelectorAll',
'getElementsByClassName',
'getElementsByTagName',
'getElementsByName',
'children',
]);

return {
ChainExpression( node ) {
const { expression } = node;

// Check if this is a call expression with optional chaining
if ( expression.type !== 'CallExpression' || ! expression.optional ) {
return;
}

const { callee } = expression;

// Check if callee is a MemberExpression (e.g., document.querySelectorAll)
if ( callee.type !== 'MemberExpression' ) {
return;
}

const methodName = callee.property.name;

if ( ! alwaysReturns.has( methodName ) ) {
return;
}

let reason;
if ( methodName === 'querySelectorAll' ) {
reason = 'querySelectorAll always returns a NodeList. Use .querySelectorAll() without ?.';
} else if ( methodName === 'children' ) {
reason = 'children always returns an HTMLCollection. Use .children without ?.';
} else {
reason = `${ methodName } always returns a collection. Remove the ?.`;
}

context.report({
node,
messageId: 'unnecessaryChaining',
data: {
method: methodName,
reason,
},
fix( fixer ) {
// Remove the ?. by replacing the CallExpression with a non-optional version
const callText = sourceCode.getText( expression );
const fixedText = callText.replace( /\?\.([([])/g, '$1' );

return fixer.replaceText( node, fixedText );
},
});
},

// Also catch cases like: elements?.forEach where elements is from querySelectorAll
MemberExpression( node ) {
if ( ! node.optional ) {
return;
}

const { object } = node;

// Check if object is a direct call to querySelectorAll or similar
if ( object.type === 'CallExpression' && object.callee.type === 'MemberExpression' ) {
const methodName = object.callee.property.name;

if ( alwaysReturns.has( methodName ) ) {
context.report({
node,
messageId: 'unnecessaryChaining',
data: {
method: methodName,
reason: `${ methodName } always returns a collection, so ${ node.property.name } will not be null.`,
},
fix( fixer ) {
// Replace ?. with .
const objectText = sourceCode.getText( object );
const propertyText = sourceCode.getText( node.property );
const fixedText = `${ objectText }.${ propertyText }`;

return fixer.replaceText( node, fixedText );
},
Comment on lines +101 to +108
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.

⚠️ Potential issue | 🟡 Minor

Fix may produce invalid code for computed properties.

If node.computed is true (e.g., result?.[0]), the fix outputs result.0 instead of result[0]. Consider preserving bracket notation for computed accesses.

🐛 Proposed fix for computed property handling
 							fix( fixer ) {
 								// Replace ?. with .
 								const objectText = sourceCode.getText( object );
 								const propertyText = sourceCode.getText( node.property );
-								const fixedText = `${ objectText }.${ propertyText }`;
+								const fixedText = node.computed
+									? `${ objectText }[${ propertyText }]`
+									: `${ objectText }.${ propertyText }`;
 
 								return fixer.replaceText( node, fixedText );
 							},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fix( fixer ) {
// Replace ?. with .
const objectText = sourceCode.getText( object );
const propertyText = sourceCode.getText( node.property );
const fixedText = `${ objectText }.${ propertyText }`;
return fixer.replaceText( node, fixedText );
},
fix( fixer ) {
// Replace ?. with .
const objectText = sourceCode.getText( object );
const propertyText = sourceCode.getText( node.property );
const fixedText = node.computed
? `${ objectText }[${ propertyText }]`
: `${ objectText }.${ propertyText }`;
return fixer.replaceText( node, fixedText );
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eslint-rules/rules/no-optional-chaining-queryselectorall.js` around lines 101
- 108, The current fixer in fix(fixer) uses sourceCode.getText(object) and
sourceCode.getText(node.property) and always emits
`${objectText}.${propertyText}`, which breaks computed accesses (node.computed
true) by turning result?.[0] into result.0; update the fixer to check
node.computed and, when true, emit bracket notation preserving the property
expression (e.g., `${objectText}[${propertyText}]`), otherwise keep the dot
form; continue to use sourceCode.getText(object) and
sourceCode.getText(node.property) and call fixer.replaceText(node, fixedText) as
before.

});
}
}
},
};
},
};
Loading