PSA/eslint-plugin-custom-rules/check-required-props.js
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

125 lines
4.0 KiB
JavaScript

export default {
meta: {
type: 'problem',
docs: {
description: 'Ensure required props are provided to components',
category: 'Possible Errors',
recommended: true,
},
schema: [],
},
create(context) {
const sourceCode = context.getSourceCode();
// Collect local identifiers bound to our UI Button component
// We scope the rule primarily to Buttons imported from .../ui/Button
const buttonLocalNames = new Set();
// Updated regex to catch all Button import patterns including aliases
const buttonPathRegex = /(@\/components\/ui\/[Bb]utton|[\\/](ui[\\/])?[Bb]utton)(\.tsx?)?$/i;
for (const node of sourceCode.ast.body || []) {
if (node.type === 'ImportDeclaration') {
const src = node.source && node.source.value;
if (
typeof src === 'string' &&
(
buttonPathRegex.test(src) ||
src === 'server/src/components/ui/Button' ||
src === '../../components/ui/Button' ||
src === '../components/ui/Button' ||
src === '../ui/Button' ||
src === './ui/Button' ||
src === '@/components/ui/button' ||
src === '@/components/ui/Button'
)
) {
for (const spec of node.specifiers || []) {
if (spec.type === 'ImportSpecifier' && spec.imported && spec.imported.name === 'Button') {
buttonLocalNames.add(spec.local.name);
} else if (spec.type === 'ImportDefaultSpecifier') {
buttonLocalNames.add(spec.local.name);
}
}
}
}
}
function isTargetButton(openingEl) {
if (!openingEl || !openingEl.name) return false;
if (openingEl.name.type === 'JSXIdentifier') {
const name = openingEl.name.name;
// If we detected specific imports, prefer those. Otherwise fall back to literal "Button"
return buttonLocalNames.size > 0 ? buttonLocalNames.has(name) : name === 'Button';
}
return false;
}
function hasIdAttribute(openingEl) {
return (openingEl.attributes || []).some(
(attr) => attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'id'
);
}
function containsIdProperty(objExpr) {
if (!objExpr || objExpr.type !== 'ObjectExpression') return false;
return (objExpr.properties || []).some((p) => {
if (p.type !== 'Property') return false;
const key = p.key;
return (key.type === 'Identifier' && key.name === 'id') ||
(key.type === 'Literal' && key.value === 'id');
});
}
function hasIdViaSpread(openingEl) {
return (openingEl.attributes || []).some((attr) => {
if (attr.type !== 'JSXSpreadAttribute') return false;
const arg = attr.argument;
if (!arg) return false;
// Accept {...withDataAutomationId(...)} as satisfying the requirement,
// since that utility injects an 'id' (or derives one) consistently.
if (
arg.type === 'CallExpression' &&
arg.callee &&
arg.callee.type === 'Identifier' &&
arg.callee.name === 'withDataAutomationId'
) {
return true;
}
// Also accept direct object spreads that contain an 'id' key: {...{ id: '...' }}
if (arg.type === 'ObjectExpression' && containsIdProperty(arg)) {
return true;
}
// Accept spreads from variables ending with 'Props' (e.g., filterButtonProps)
// These are typically from useAutomationIdAndRegister hook
if (
arg.type === 'Identifier' &&
(arg.name.endsWith('Props') || arg.name.endsWith('ButtonProps'))
) {
return true;
}
return false;
});
}
return {
JSXOpeningElement(node) {
if (!isTargetButton(node)) return;
const ok = hasIdAttribute(node) || hasIdViaSpread(node);
if (!ok) {
context.report({
node,
message: 'Button component requires an id prop',
});
}
},
};
},
};