#!/usr/bin/env node /** * Scans the Rust codebase to find unused turbo-tasks items: * - #[turbo_tasks::function] → fn definitions * - #[turbo_tasks::value] → struct/enum definitions * - #[turbo_tasks::value_trait] → trait definitions * * Exit code 0: no unused items found * Exit code 1: unused items found * * Usage: node scripts/check-unused-turbo-tasks.mjs */ import { readdir, readFile } from 'node:fs/promises' import { join, relative } from 'node:path' import { fileURLToPath } from 'node:url' const __dirname = fileURLToPath(new URL('.', import.meta.url)) const ROOT = join(__dirname, '..') const SCAN_DIRS = [join(ROOT, 'turbopack/crates'), join(ROOT, 'crates')] const EXCLUDE_DIRS = ['turbo-tasks-macros-tests'] // After seeing an attribute, give up looking for the item it annotates // if we haven't found it within this many lines. const MAX_ANNOTATION_DISTANCE = 10 // --------------------------------------------------------------------------- // Regexes // --------------------------------------------------------------------------- // Attribute detection (applied to trimmed lines) const TT_FUNCTION_RE = /^#\[turbo_tasks::function/ const TT_VALUE_RE = /^#\[turbo_tasks::value(?![_a-zA-Z0-9])/ // not value_impl or value_trait const TT_VALUE_IMPL_RE = /^#\[turbo_tasks::value_impl/ const TT_VALUE_TRAIT_RE = /^#\[turbo_tasks::value_trait/ // Item-header extraction const FN_NAME_RE = /\bfn\s+([a-zA-Z_][a-zA-Z0-9_]*)/ const STRUCT_ENUM_RE = /^\s*(?:pub(?:\([^)]*\))?\s+)?(?:struct|enum)\s+([A-Za-z_][A-Za-z0-9_]*)/ const TRAIT_RE = /^\s*(?:pub\s+)?trait\s+([A-Za-z_][A-Za-z0-9_]*)/ const IMPL_TRAIT_FOR_RE = /^\s*impl\s*(?:<[^>]*>\s*)?([A-Za-z_][A-Za-z0-9_:]*(?:<[^>]*>)?)\s+for\s+([A-Za-z_][A-Za-z0-9_:]*)/ const IMPL_INHERENT_RE = /^\s*impl\s*(?:<[^>]*>\s*)?([A-Za-z_][A-Za-z0-9_:]*)/ // Usage scanning const IDENT_RE = /\b([a-zA-Z_][a-zA-Z0-9_]*)\b/g // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- /** * @typedef {{ * name: string, * kind: 'function' | 'value' | 'value_trait', * filePath: string, * line: number, * context: 'free' | 'inherent_impl' | 'trait_impl' | 'trait_def', * typeName?: string, * traitName?: string, * }} Definition */ // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /** * Tracks a "pending annotation": an attribute was seen on some line and we are * scanning forward to find the item it annotates (a fn, struct, enum, or trait). * * Returns null when a match is found or the search window expires, along with * the matched name (or null). */ function resolvePending(pending, currentLine, trimmed, itemRe) { if (pending < 0) return { pending, match: null } const m = itemRe.exec(trimmed) if (m) return { pending: -1, match: m[1] } if (currentLine - pending > MAX_ANNOTATION_DISTANCE) return { pending: -1, match: null } return { pending, match: null } } /** * Count unbalanced braces on a line, ignoring string literals and comments. */ function countBraces(line) { let depth = 0 let inString = false let escape = false let inChar = false for (let i = 0; i < line.length; i++) { const ch = line[i] if (escape) { escape = false continue } if (ch === '\\') { escape = true continue } // Line comment — stop processing if (!inString && !inChar && ch === '/' && line[i + 1] === '/') break if (inString) { if (ch === '"') inString = false continue } if (inChar) { if (ch === "'") inChar = false continue } if (ch === '"') { inString = true continue } // Simplified char literal detection ('x') if (ch === "'" && i + 2 < line.length && line[i + 2] === "'") { i += 2 continue } if (ch === '{') depth++ if (ch === '}') depth-- } return depth } /** * Format a Definition for display. */ function formatDefinition(def, relPath) { const loc = `${relPath}:${def.line}` if (def.kind === 'value') return ` ${loc} - value ${def.name}` if (def.kind === 'value_trait') return ` ${loc} - value_trait ${def.name}` const contextLabels = { free: 'free function', inherent_impl: `method on ${def.typeName}`, trait_impl: `impl ${def.traitName} for ${def.typeName}`, trait_def: `trait ${def.traitName} default method`, } const ctx = contextLabels[def.context] ?? def.context return ` ${loc} - fn ${def.name} (${ctx})` } // --------------------------------------------------------------------------- // Phase 0: Discover all .rs files // --------------------------------------------------------------------------- async function discoverRsFiles(dirs) { const files = [] for (const dir of dirs) { let entries try { entries = await readdir(dir, { recursive: true, withFileTypes: false }) } catch { continue } for (const entry of entries) { if (entry.endsWith('.rs')) { const parts = entry.split('/') if (parts.some((p) => EXCLUDE_DIRS.includes(p))) continue files.push(join(dir, entry)) } } } return files } // --------------------------------------------------------------------------- // Phase 1: Extract definitions // --------------------------------------------------------------------------- /** * Parse a single .rs file for turbo-tasks definitions. * * Tracks three kinds of pending annotations: * - #[turbo_tasks::function] → expects `fn ` * - #[turbo_tasks::value] → expects `struct ` or `enum ` * - #[turbo_tasks::value_trait] → expects `trait ` * * Also maintains a block stack to determine whether a function lives inside a * #[turbo_tasks::value_impl] or #[turbo_tasks::value_trait] block, which gives * the function its context (inherent method, trait impl, trait default method). */ function parseDefinitions(filePath, content) { const lines = content.split('\n') /** @type {Definition[]} */ const definitions = [] let braceDepth = 0 /** * Stack of impl/trait blocks we're inside (for function context). * @type {Array<{ startDepth: number, typeName?: string, traitName?: string, implKind: 'inherent' | 'trait' | 'trait_def' }>} */ const blockStack = [] // Pending attribute state: line index where the attribute was seen, or -1. let pendingFn = -1 let pendingValue = -1 let pendingValueTrait = -1 let pendingBlockType = null // 'value_impl' | 'value_trait' for (let i = 0; i < lines.length; i++) { const line = lines[i] const trimmed = line.trimStart() const isComment = trimmed.startsWith('//') if (!isComment) { // --- Detect attributes --- if (TT_VALUE_IMPL_RE.test(trimmed)) pendingBlockType = 'value_impl' if (TT_VALUE_TRAIT_RE.test(trimmed)) { pendingBlockType = 'value_trait' pendingValueTrait = i } if (TT_VALUE_RE.test(trimmed)) pendingValue = i if (TT_FUNCTION_RE.test(trimmed)) pendingFn = i // --- Resolve pending value/value_trait annotations into definitions --- { const r = resolvePending(pendingValue, i, trimmed, STRUCT_ENUM_RE) pendingValue = r.pending if (r.match) { definitions.push({ name: r.match, kind: 'value', filePath, line: i + 1, context: 'free', }) } } { const r = resolvePending(pendingValueTrait, i, trimmed, TRAIT_RE) pendingValueTrait = r.pending if (r.match) { definitions.push({ name: r.match, kind: 'value_trait', filePath, line: i + 1, context: 'free', }) } } // --- Resolve pending function annotations --- { const r = resolvePending(pendingFn, i, trimmed, FN_NAME_RE) pendingFn = r.pending if (r.match) { const block = blockStack.length > 0 ? blockStack[blockStack.length - 1] : null let context = 'free' let typeName, traitName if (block) { if (block.implKind === 'inherent') { context = 'inherent_impl' typeName = block.typeName } else if (block.implKind === 'trait') { context = 'trait_impl' typeName = block.typeName traitName = block.traitName } else if (block.implKind === 'trait_def') { context = 'trait_def' traitName = block.traitName } } definitions.push({ name: r.match, kind: 'function', filePath, line: i + 1, context, ...(typeName && { typeName }), ...(traitName && { traitName }), }) } } // --- Resolve pending block headers (value_impl / value_trait) --- if (pendingBlockType === 'value_impl') { const traitImplMatch = IMPL_TRAIT_FOR_RE.exec(trimmed) if (traitImplMatch) { blockStack.push({ startDepth: braceDepth, traitName: traitImplMatch[1], typeName: traitImplMatch[2], implKind: 'trait', }) pendingBlockType = null } else { const inherentMatch = IMPL_INHERENT_RE.exec(trimmed) if (inherentMatch && trimmed.includes('{')) { blockStack.push({ startDepth: braceDepth, typeName: inherentMatch[1], implKind: 'inherent', }) pendingBlockType = null } // else: impl line without opening brace yet — keep waiting } } else if (pendingBlockType === 'value_trait') { const traitMatch = TRAIT_RE.exec(trimmed) if (traitMatch) { blockStack.push({ startDepth: braceDepth, traitName: traitMatch[1], implKind: 'trait_def', }) pendingBlockType = null } } } // Track brace depth (even for comment lines — countBraces skips //) braceDepth += countBraces(line) // Pop blocks that have closed while ( blockStack.length > 0 && braceDepth <= blockStack[blockStack.length - 1].startDepth ) { blockStack.pop() } } return definitions } // --------------------------------------------------------------------------- // Phase 2: Find used names // --------------------------------------------------------------------------- /** * Scan all file contents and return the set of definition names that appear * at least once outside their definition site. * * @param {Map} fileContents * @param {Set} names * @param {Map>} definitionLocations name → Set<"path:line"> * @returns {Set} */ function findUsedNames(fileContents, names, definitionLocations) { const used = new Set() for (const [filePath, content] of fileContents) { const lines = content.split('\n') for (let i = 0; i < lines.length; i++) { const line = lines[i] if (line.trimStart().startsWith('//')) continue let match IDENT_RE.lastIndex = 0 while ((match = IDENT_RE.exec(line)) !== null) { const name = match[1] if (used.has(name) || !names.has(name)) continue // Skip the definition site itself const defLocs = definitionLocations.get(name) if (defLocs && defLocs.has(`${filePath}:${i + 1}`)) continue used.add(name) if (used.size === names.size) return used } } } return used } // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- async function main() { const rsFiles = await discoverRsFiles(SCAN_DIRS) /** @type {Map} */ const fileContents = new Map() await Promise.all( rsFiles.map(async (filePath) => { try { fileContents.set(filePath, await readFile(filePath, 'utf-8')) } catch { /* skip unreadable files */ } }) ) // Parse definitions from all files /** @type {Definition[]} */ const allDefinitions = [] for (const [filePath, content] of fileContents) { allDefinitions.push(...parseDefinitions(filePath, content)) } // Build indexes const allNames = new Set(allDefinitions.map((d) => d.name)) /** @type {Map>} */ const definitionLocations = new Map() for (const def of allDefinitions) { const key = `${def.filePath}:${def.line}` if (!definitionLocations.has(def.name)) { definitionLocations.set(def.name, new Set()) } definitionLocations.get(def.name).add(key) } // Find which names have external usage const usedNames = findUsedNames(fileContents, allNames, definitionLocations) // Collect and sort unused definitions const unused = allDefinitions .filter((d) => !usedNames.has(d.name)) .sort((a, b) => a.filePath.localeCompare(b.filePath) || a.line - b.line) // Report if (unused.length === 0) { console.log( `No unused turbo-tasks items found (${allDefinitions.length} total checked).` ) process.exit(0) } console.log('Unused turbo-tasks items:\n') for (const def of unused) { console.log(formatDefinition(def, relative(ROOT, def.filePath))) } console.log( `\nFound ${unused.length} unused turbo-tasks item(s) out of ${allDefinitions.length} total.` ) process.exit(1) } main().catch((err) => { console.error(err) process.exit(2) })