/** * @file no-hardcoded-search-params.js * Enforces use of SearchParam system instead of manual URLSearchParams manipulation */ module.exports = { meta: { type: 'problem', docs: { description: 'Enforce use of SearchParamBuilder/SearchParamParser instead of manual URLSearchParams manipulation', category: 'Best Practices', recommended: true, }, fixable: 'code', schema: [], messages: { manualSearchParams: 'Manual URLSearchParams construction. Use SearchParamBuilder instead: import { SearchParamBuilder } from "@/lib/routing/search-params"', manualGetParam: 'Manual search param access with get(). Use SearchParamParser instead: import { SearchParamParser } from "@/lib/routing/search-params"', manualSetParam: 'Manual search param setting with set(). Use SearchParamBuilder instead', }, }, create(context) { return { // Detect: new URLSearchParams() NewExpression(node) { if (node.callee.type === 'Identifier' && node.callee.name === 'URLSearchParams') { // Check if it's being used for construction (not just parsing) const parent = node.parent; // If it's in a call expression like new URLSearchParams().toString() // or new URLSearchParams().get() if (parent.type === 'MemberExpression') { const property = parent.property.name; if (property === 'toString' || property === 'set' || property === 'append') { context.report({ node, messageId: 'manualSearchParams', }); } } // If it's just new URLSearchParams() without further use else if (parent.type === 'VariableDeclarator' || parent.type === 'AssignmentExpression') { context.report({ node, messageId: 'manualSearchParams', }); } } }, // Detect: params.get() or params.set() CallExpression(node) { if (node.callee.type === 'MemberExpression') { const object = node.callee.object; const property = node.callee.property.name; // Check if it's a URLSearchParams instance if (object.type === 'Identifier' && (property === 'get' || property === 'set' || property === 'append' || property === 'delete')) { // Try to trace back to see if it's URLSearchParams let current = object; let isUrlSearchParams = false; // Check if it's from URLSearchParams constructor const scope = context.getScope(); const variable = scope.variables.find(v => v.name === current.name); if (variable && variable.defs.length > 0) { const def = variable.defs[0]; if (def.node.init && def.node.init.type === 'NewExpression' && def.node.init.callee.name === 'URLSearchParams') { isUrlSearchParams = true; } } if (isUrlSearchParams) { if (property === 'get') { context.report({ node, messageId: 'manualGetParam', }); } else if (property === 'set' || property === 'append') { context.report({ node, messageId: 'manualSetParam', }); } } } } }, // Detect: searchParams.get() directly (from function parameter) MemberExpression(node) { if (node.property.type === 'Identifier' && (node.property.name === 'get' || node.property.name === 'set')) { // Check if object is named "searchParams" or "params" if (node.object.type === 'Identifier' && (node.object.name === 'searchParams' || node.object.name === 'params')) { // Check parent is a call expression if (node.parent.type === 'CallExpression' && node.parent.callee === node) { if (node.property.name === 'get') { context.report({ node, messageId: 'manualGetParam', }); } else if (node.property.name === 'set') { context.report({ node, messageId: 'manualSetParam', }); } } } } }, }; }, };