Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled
460 lines
14 KiB
JavaScript
460 lines
14 KiB
JavaScript
#!/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 <name>`
|
|
* - #[turbo_tasks::value] → expects `struct <name>` or `enum <name>`
|
|
* - #[turbo_tasks::value_trait] → expects `trait <name>`
|
|
*
|
|
* 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<string, string>} fileContents
|
|
* @param {Set<string>} names
|
|
* @param {Map<string, Set<string>>} definitionLocations name → Set<"path:line">
|
|
* @returns {Set<string>}
|
|
*/
|
|
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<string, string>} */
|
|
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<string, Set<string>>} */
|
|
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)
|
|
})
|