refactor: clear the 85 minutes of technical debt (#81)
Thirteen of SonarQube's fourteen code smells, with the fourteenth argued as a false positive rather than coded around. Four of these were not stylistic. The Remove button in Cart's List.Item actions array carried no key, so React could not match it across renders and rebuilt it on every cart render. The cart, customer-auth and favorites providers each passed a freshly allocated object as their context value, which re-renders every consumer whenever the provider renders, cart or session unchanged — and these three wrap the entire storefront, so the fan-out was the whole tree. Two of them also rebuilt a Set on every render for good measure. All four now memoized on the values they actually derive from. The four cognitive-complexity findings wanted real restructuring rather than suppression. parseItemFilters splits into one helper per field, leaving the function with the order those helpers run in and the single rule that spans two fields; the order is preserved exactly, because a query wrong in two ways reports the first field and rearranging the calls would change which error a caller sees. adminCategories' PUT extracts the parent resolution — the existence check and the cycle check — into a resolver that returns the refusal rather than sending it. App's four-way render chain becomes a Catalogue component, which also removes two of the nested ternaries. Customers' confirm dialog moves to module level, since a function's cognitive complexity counts everything declared inside it and that dialog branches on `disabling` five times. The rest were mechanical: two more nested ternaries — a status-colour lookup and a pluralisation helper — and one type assertion that asserted the type the expression already had. Left alone: S6478 on CategoryTreeSelect's popupRender. That is antd's render prop, called as a function and spliced in, never mounted as a component type, so the destroy-the-subtree failure the rule describes cannot happen. Marked false positive in SonarQube with that reasoning rather than contorting the component around a rule that misread it. Verified rather than assumed. Backend unit 78 pass, integration 134 pass, end-to-end 83 pass, both workspaces build clean. ESLint warnings drop from 35 to 31 with no new file warning — the same React and SonarJS rules #60 turned on are what surfaced this backlog in the first place. Refs #81 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+59
-34
@@ -81,26 +81,67 @@ function parsePrice(value: unknown, name: string): number | null {
|
||||
return parseNonNegativeInteger(raw, name);
|
||||
}
|
||||
|
||||
export function parseItemFilters(query: Record<string, unknown>): ItemFilters {
|
||||
const categoryRaw = singleValue(query.category, 'category');
|
||||
const categoryId = categoryRaw === null || categoryRaw === '' ? null : parseId(categoryRaw, 'category');
|
||||
function parseCategoryId(value: unknown): number | null {
|
||||
const raw = singleValue(value, 'category');
|
||||
if (raw === null || raw === '') {
|
||||
return null;
|
||||
}
|
||||
return parseId(raw, 'category');
|
||||
}
|
||||
|
||||
const tagsRaw = singleValue(query.tags, 'tags');
|
||||
function parseTagIds(value: unknown): number[] {
|
||||
const raw = singleValue(value, 'tags');
|
||||
const tagIds: number[] = [];
|
||||
if (tagsRaw) {
|
||||
for (const part of tagsRaw.split(',')) {
|
||||
const trimmed = part.trim();
|
||||
if (trimmed === '') {
|
||||
continue;
|
||||
}
|
||||
const id = parseId(trimmed, 'tags');
|
||||
// Duplicates would inflate the required-match count below and make the
|
||||
// filter match nothing at all.
|
||||
if (!tagIds.includes(id)) {
|
||||
tagIds.push(id);
|
||||
}
|
||||
if (!raw) {
|
||||
return tagIds;
|
||||
}
|
||||
for (const part of raw.split(',')) {
|
||||
const trimmed = part.trim();
|
||||
if (trimmed === '') {
|
||||
continue;
|
||||
}
|
||||
const id = parseId(trimmed, 'tags');
|
||||
// Duplicates would inflate the required-match count in buildItemFilterSql
|
||||
// and make the filter match nothing at all.
|
||||
if (!tagIds.includes(id)) {
|
||||
tagIds.push(id);
|
||||
}
|
||||
}
|
||||
return tagIds;
|
||||
}
|
||||
|
||||
function parseStatus(value: unknown): ItemStatus | null {
|
||||
const raw = singleValue(value, 'status');
|
||||
if (raw === null || raw === '') {
|
||||
return null;
|
||||
}
|
||||
if (!ITEM_STATUSES.includes(raw)) {
|
||||
throw new FilterError('invalid status');
|
||||
}
|
||||
return raw as ItemStatus;
|
||||
}
|
||||
|
||||
function parseFavoritesOnly(value: unknown): boolean {
|
||||
const raw = singleValue(value, 'favorites');
|
||||
if (raw === null || raw === '') {
|
||||
return false;
|
||||
}
|
||||
if (TRUE_VALUES.includes(raw)) {
|
||||
return true;
|
||||
}
|
||||
if (FALSE_VALUES.includes(raw)) {
|
||||
return false;
|
||||
}
|
||||
throw new FilterError('invalid favorites');
|
||||
}
|
||||
|
||||
// The per-field parsing lives in the helpers above; what stays here is the
|
||||
// order they run in and the one rule that spans two fields. Order is
|
||||
// deliberate and observable: a query wrong in two ways reports the first
|
||||
// field, so moving these lines around changes which error a caller sees.
|
||||
export function parseItemFilters(query: Record<string, unknown>): ItemFilters {
|
||||
const categoryId = parseCategoryId(query.category);
|
||||
const tagIds = parseTagIds(query.tags);
|
||||
|
||||
const minPriceCents = parsePrice(query.min_price, 'min_price');
|
||||
const maxPriceCents = parsePrice(query.max_price, 'max_price');
|
||||
@@ -108,24 +149,8 @@ export function parseItemFilters(query: Record<string, unknown>): ItemFilters {
|
||||
throw new FilterError('min_price may not exceed max_price');
|
||||
}
|
||||
|
||||
const statusRaw = singleValue(query.status, 'status');
|
||||
let status: ItemStatus | null = null;
|
||||
if (statusRaw !== null && statusRaw !== '') {
|
||||
if (!ITEM_STATUSES.includes(statusRaw)) {
|
||||
throw new FilterError('invalid status');
|
||||
}
|
||||
status = statusRaw as ItemStatus;
|
||||
}
|
||||
|
||||
const favoritesRaw = singleValue(query.favorites, 'favorites');
|
||||
let favoritesOnly = false;
|
||||
if (favoritesRaw !== null && favoritesRaw !== '') {
|
||||
if (TRUE_VALUES.includes(favoritesRaw)) {
|
||||
favoritesOnly = true;
|
||||
} else if (!FALSE_VALUES.includes(favoritesRaw)) {
|
||||
throw new FilterError('invalid favorites');
|
||||
}
|
||||
}
|
||||
const status = parseStatus(query.status);
|
||||
const favoritesOnly = parseFavoritesOnly(query.favorites);
|
||||
|
||||
return { categoryId, tagIds, minPriceCents, maxPriceCents, status, favoritesOnly };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user