Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
194 changes: 194 additions & 0 deletions scripts/codemod-any-to-unknown.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
#!/usr/bin/env node
/**
* Targeted codemod that promotes `any` to `unknown` in the safe patterns
* where the change is mechanical and the type narrowing it forces is a
* net positive.
*
* Patterns handled:
*
* 1. `[key: string]: any` → `[key: string]: unknown`
* (Index signatures: consumer must narrow before using the value.)
*
* 2. `useState<any>(` → `useState<unknown>(`
* (React state: forces narrowing on read; the setter still accepts
* anything via the unknown type.)
*
* 3. `as any` → `as unknown`
* (Casts: `as unknown` requires a second cast to use as a specific
* type — slightly stricter and surfaces the danger zone.)
*
* 4. `Record<string, any>` → `Record<string, unknown>`
* (Same rationale as #1.)
*
* 5. `Map<KEY, any>` → `Map<KEY, unknown>`
* (Same.)
*
* 6. `Set<any>` → `Set<unknown>`
* (Same.)
*
* 7. `(: any\[\])` → `: unknown[]`
* (Array of unknowns: members must be narrowed before use.)
*
* Patterns NOT handled (kept as `any`):
* - Bare `: any` parameter / return / variable annotations.
* These need contextual review; mechanical promotion to `unknown`
* cascades type errors through every consumer of the function.
* - Generic args on third-party RPC helpers (`invokeXxx<any>`) — these
* are deliberate "I'll narrow later" markers; bumping to unknown
* forces a parse change at every call site.
*
* Usage:
* node scripts/codemod-any-to-unknown.mjs # rewrite src/
* node scripts/codemod-any-to-unknown.mjs --check # exit 1 if any
* # of the above
* # patterns remain
* node scripts/codemod-any-to-unknown.mjs --dry-run
*/
import { readFile, writeFile, readdir } from 'node:fs/promises';
import path from 'node:path';

const args = new Set(process.argv.slice(2));
const CHECK_ONLY = args.has('--check');
const DRY_RUN = args.has('--dry-run');

const ROOT = 'src';

async function* walk(dir) {
for (const entry of await readdir(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) yield* walk(full);
else if (entry.isFile() && /\.(ts|tsx)$/.test(entry.name)) yield full;
}
}

const TRANSFORMS = [
// [name, regex, replacement]
['index-signature', /(\[\s*[A-Za-z0-9_$]+\s*:\s*string\s*\]\s*:\s*)any\b/g, '$1unknown'],
['useState-generic', /useState<\s*any\s*>/g, 'useState<unknown>'],
['as-any', /\bas\s+any\b(?!\s*\[)/g, 'as unknown'],
['record-string-any', /\bRecord<\s*string\s*,\s*any\s*>/g, 'Record<string, unknown>'],
['map-value-any', /\bMap<\s*([^,>]+),\s*any\s*>/g, 'Map<$1, unknown>'],
['set-any', /\bSet<\s*any\s*>/g, 'Set<unknown>'],
['any-array-annotation', /(\:\s*)any\[\]/g, '$1unknown[]'],
];

let filesScanned = 0;
let filesChanged = 0;
const counts = Object.fromEntries(TRANSFORMS.map(([name]) => [name, 0]));
const offenders = [];

for await (const file of walk(ROOT)) {
filesScanned += 1;
const original = await readFile(file, 'utf8');
if (!original.includes('any')) continue;

// Strip comments + strings to a parallel buffer where indices are
// preserved but those regions are filled with spaces. This way our
// top-level regex transforms don't accidentally touch documentation.
const stripped = stripCommentsAndStrings(original);

let next = original;
let mutated = false;

for (const [name, regex, replacement] of TRANSFORMS) {
// Find matches in the stripped buffer (so we ignore matches in
// strings/comments) but apply the replacement to the original text
// at the same offsets.
const matches = [...stripped.matchAll(regex)];
if (matches.length === 0) continue;
counts[name] += matches.length;
mutated = true;

if (CHECK_ONLY) continue;

// Walk right-to-left to keep offsets valid.
for (let i = matches.length - 1; i >= 0; i--) {
const m = matches[i];
const start = m.index;
const end = start + m[0].length;
// Confirm the slice in `next` still matches what stripped saw — if
// a previous transform shifted things, this is a no-op safety check.
const slice = next.slice(start, end);
if (slice !== m[0]) continue;
// Compute replacement using captured groups from the original match.
let replaced = replacement;
for (let g = 1; g < m.length; g++) {
replaced = replaced.replace(`$${g}`, m[g]);
}
next = next.slice(0, start) + replaced + next.slice(end);
}
}

if (mutated) {
if (CHECK_ONLY) {
offenders.push(file);
} else if (DRY_RUN) {
console.log(`would write ${file}`);
filesChanged += 1;
} else {
await writeFile(file, next, 'utf8');
filesChanged += 1;
}
}
}

if (CHECK_ONLY) {
if (offenders.length > 0) {
console.error(`✖ ${offenders.length} file(s) still match safe \`any\` patterns:`);
for (const f of offenders.slice(0, 20)) console.error(` - ${f}`);
if (offenders.length > 20) console.error(` ... and ${offenders.length - 20} more`);
console.error('Run: node scripts/codemod-any-to-unknown.mjs');
process.exit(1);
}
console.log(`✅ no safe \`any\` patterns remaining in ${filesScanned} files.`);
process.exit(0);
}

console.log('');
console.log(`Files scanned: ${filesScanned}; files changed: ${filesChanged}.`);
console.log(`Counts by pattern:`);
for (const [name, count] of Object.entries(counts)) {
console.log(` ${name.padEnd(24)} ${count}`);
}

/**
* Replace every char inside string literals and comments with a space
* (preserving newlines and lengths) so that regex matches against the
* returned buffer don't trigger inside strings/comments.
*/
function stripCommentsAndStrings(src) {
const out = src.split('');
const len = src.length;
let i = 0;
const blank = (start, end) => {
for (let k = start; k < end && k < len; k++) {
if (out[k] !== '\n') out[k] = ' ';
}
};
while (i < len) {
const c = src[i];
if (c === '/' && src[i + 1] === '/') {
const nl = src.indexOf('\n', i + 2);
const end = nl === -1 ? len : nl;
blank(i, end);
i = end;
} else if (c === '/' && src[i + 1] === '*') {
const e = src.indexOf('*/', i + 2);
const end = e === -1 ? len : e + 2;
blank(i, end);
i = end;
} else if (c === "'" || c === '"' || c === '`') {
let j = i + 1;
while (j < len) {
if (src[j] === '\\') { j += 2; continue; }
if (src[j] === c) { j++; break; }
j++;
}
blank(i, j);
i = j;
} else {
i++;
}
}
return out.join('');
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ interface GroupComponentCardProps {
locations: GroupLocation[];
techniques: Technique[] | undefined;
locationTechniques: GroupLocationTechnique[];
onUpdateComponent: (data: { id: string; [key: string]: any }) => void;
onUpdateComponent: (data: { id: string; [key: string]: unknown }) => void;
onDeleteComponent: (id: string) => void;
onAddLocation: (data: { group_component_id: string; location_code: string; location_name: string; max_width_cm?: number; max_height_cm?: number; max_area_cm2?: number }) => void;
addLocationPending: boolean;
onUpdateLocation: (data: { id: string; [key: string]: any }) => void;
onUpdateLocation: (data: { id: string; [key: string]: unknown }) => void;
onDeleteLocation: (id: string) => void;
onAddTechnique: (data: { group_location_id: string; technique_id: string; max_colors?: number }) => void;
addTechniquePending: boolean;
onUpdateTechnique: (data: { id: string; [key: string]: any }) => void;
onUpdateTechnique: (data: { id: string; [key: string]: unknown }) => void;
onDeleteTechnique: (id: string) => void;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ interface GroupLocationCardProps {
selectedGroup: string | null;
techniques: Technique[] | undefined;
locationTechniques: GroupLocationTechnique[];
onUpdateLocation: (data: { id: string; [key: string]: any }) => void;
onUpdateLocation: (data: { id: string; [key: string]: unknown }) => void;
onDeleteLocation: (id: string) => void;
onAddTechnique: (data: { group_location_id: string; technique_id: string; max_colors?: number }) => void;
addTechniquePending: boolean;
onUpdateTechnique: (data: { id: string; [key: string]: any }) => void;
onUpdateTechnique: (data: { id: string; [key: string]: unknown }) => void;
onDeleteTechnique: (id: string) => void;
}

Expand Down
6 changes: 3 additions & 3 deletions src/components/admin/hooks/useGroupPersonalization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export function useGroupPersonalization() {
});

const updateComponent = useMutation({
mutationFn: async ({ id, ...data }: { id: string; [key: string]: any }) => {
mutationFn: async ({ id, ...data }: { id: string; [key: string]: unknown }) => {
const { error } = await untypedFrom("product_group_components").update(data).eq("id", id);
if (error) throw error;
},
Expand Down Expand Up @@ -192,7 +192,7 @@ export function useGroupPersonalization() {
});

const updateLocation = useMutation({
mutationFn: async ({ id, ...data }: { id: string; [key: string]: any }) => {
mutationFn: async ({ id, ...data }: { id: string; [key: string]: unknown }) => {
const { error } = await untypedFrom("product_group_locations").update(data).eq("id", id);
if (error) throw error;
},
Expand Down Expand Up @@ -228,7 +228,7 @@ export function useGroupPersonalization() {
});

const updateTechnique = useMutation({
mutationFn: async ({ id, ...data }: { id: string; [key: string]: any }) => {
mutationFn: async ({ id, ...data }: { id: string; [key: string]: unknown }) => {
const { error } = await untypedFrom("product_group_location_techniques").update(data).eq("id", id);
if (error) throw error;
},
Expand Down
6 changes: 3 additions & 3 deletions src/components/admin/products/BulkImportDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ interface BulkImportDialogProps {

export function BulkImportDialog({ open, onOpenChange, onComplete }: BulkImportDialogProps) {
const [step, setStep] = useState<Step>('upload');
const [rawData, setRawData] = useState<Record<string, any>[]>([]);
const [rawData, setRawData] = useState<Record<string, unknown>[]>([]);
const [headers, setHeaders] = useState<string[]>([]);
const [fileName, setFileName] = useState('');
const [mapping, setMapping] = useState<ColumnMapping>({});
Expand All @@ -52,7 +52,7 @@ export function BulkImportDialog({ open, onOpenChange, onComplete }: BulkImportD
}, []);

// ── Upload complete handler ──
const handleFileProcessed = useCallback((h: string[], rows: Record<string, any>[], name: string, m: ColumnMapping) => {
const handleFileProcessed = useCallback((h: string[], rows: Record<string, unknown>[], name: string, m: ColumnMapping) => {
setHeaders(h);
setRawData(rows);
setFileName(name);
Expand All @@ -71,7 +71,7 @@ export function BulkImportDialog({ open, onOpenChange, onComplete }: BulkImportD
const row = rawData[i];
const errors: string[] = [];
const warnings: string[] = [];
const mapped: Record<string, any> = {};
const mapped: Record<string, unknown> = {};

for (const [sourceCol, targetField] of Object.entries(mapping)) {
if (targetField) mapped[targetField] = row[sourceCol];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ interface StepCompleteProps {
importMode: string;
invalidCount: number;
validationResults: ValidationResult[];
rawData: Record<string, any>[];
rawData: Record<string, unknown>[];
onReset: () => void;
onClose: () => void;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { TARGET_FIELDS, type ColumnMapping, type TargetFieldKey } from './types'

interface StepMappingProps {
headers: string[];
rawData: Record<string, any>[];
rawData: Record<string, unknown>[];
mapping: ColumnMapping;
setMapping: (fn: (prev: ColumnMapping) => ColumnMapping) => void;
requiredMapped: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { ValidationResult, ColumnMapping } from './types';

interface StepPreviewProps {
validationResults: ValidationResult[];
rawData: Record<string, any>[];
rawData: Record<string, unknown>[];
mapping: ColumnMapping;
importMode: ImportMode;
setImportMode: (mode: ImportMode) => void;
Expand Down
6 changes: 3 additions & 3 deletions src/components/admin/products/bulk-import/StepUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { toast } from 'sonner';
import { MAX_ROWS, TARGET_FIELDS, TEMPLATE_EXAMPLES, ALIAS_MAP, type ColumnMapping } from './types';

interface StepUploadProps {
onFileProcessed: (headers: string[], rows: Record<string, any>[], fileName: string, mapping: ColumnMapping) => void;
onFileProcessed: (headers: string[], rows: Record<string, unknown>[], fileName: string, mapping: ColumnMapping) => void;
}

const normalizeStr = (s: string) => s.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]/g, '');
Expand Down Expand Up @@ -56,7 +56,7 @@ export function StepUpload({ onFileProcessed }: StepUploadProps) {
const ext = file.name.split('.').pop()?.toLowerCase();
try {
let parsedHeaders: string[] = [];
let parsedRows: Record<string, any>[] = [];
let parsedRows: Record<string, unknown>[] = [];

if (ext === 'csv') {
const text = await file.text();
Expand All @@ -68,7 +68,7 @@ export function StepUpload({ onFileProcessed }: StepUploadProps) {
const buffer = await file.arrayBuffer();
const wb = XLSX.read(buffer, { type: 'array' });
const sheet = wb.Sheets[wb.SheetNames[0]];
const json = XLSX.utils.sheet_to_json<Record<string, any>>(sheet, { defval: '' });
const json = XLSX.utils.sheet_to_json<Record<string, unknown>>(sheet, { defval: '' });
if (json.length === 0) { toast.error('Planilha vazia'); return; }
parsedHeaders = Object.keys(json[0]);
parsedRows = json;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function useProductFormDraft(
keys.forEach((key) => {
const val = draft.formData[key];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (val !== undefined) setValue(key, val as any);
if (val !== undefined) setValue(key, val as unknown);
});
if (draft.images?.length) setImages(draft.images);
if (typeof draft.stepIndex === 'number') setStepIndex(draft.stepIndex);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { maskCep, ESTADOS_BR } from '@/utils/masks';
const fieldClass = "mt-1.5 h-9";

interface AddressTabProps {
form: Record<string, any>;
form: Record<string, unknown>;
}

export function AddressTab({ form }: AddressTabProps) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { maskCnpj, maskPhone, ESTADOS_BR } from '@/utils/masks';
const fieldClass = "mt-1.5 h-9";

interface BasicDataTabProps {
form: Record<string, any>;
form: Record<string, unknown>;
}

export function BasicDataTab({ form }: BasicDataTabProps) {
Expand Down
2 changes: 1 addition & 1 deletion src/components/cart/BundleSuggestionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function BundleSuggestionCard({ productId, onAdd, className }: BundleSugg
enabled: !!productId,
queryFn: async (): Promise<BundleSuggestion[]> => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { data, error } = await (supabase.rpc as any)("get_bundle_suggestions", {
const { data, error } = await (supabase.rpc as unknown)("get_bundle_suggestions", {
_product_id: productId,
});
if (error) {
Expand Down
2 changes: 1 addition & 1 deletion src/components/catalog/CatalogToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ interface CatalogToolbarProps {
resetFilters: () => void;
sortBy: SortOption;
setSortBy: (s: SortOption) => void;
statBadges: any[];
statBadges: unknown[];
viewMode: ViewMode;
setViewMode: (m: ViewMode) => void;
gridColumns: ColumnCount;
Expand Down
2 changes: 1 addition & 1 deletion src/components/collections/CollectionTableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ function SortHeader({ label, sortKey, currentKey, currentDir, onSort, className

interface CollectionTableRowProps {
collection: Collection;
products: any[];
products: unknown[];
isSelected: boolean;
isSelectionMode: boolean;
onToggleSelect: () => void;
Expand Down
Loading
Loading