mirror of
https://github.com/hanzoai/firebase-js-sdk.git
synced 2026-09-24 16:59:10 +00:00
382 lines
11 KiB
TypeScript
382 lines
11 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
/* eslint-disable no-console */
|
|
|
|
import * as fs from 'fs';
|
|
import { getRandomValues } from 'node:crypto';
|
|
import * as path from 'path';
|
|
|
|
import * as ts from 'typescript';
|
|
import yargs from 'yargs';
|
|
import { hideBin } from 'yargs/helpers';
|
|
|
|
let isVerbose: boolean = false;
|
|
|
|
function log(message: any): void {
|
|
if (isVerbose) {
|
|
console.log(message);
|
|
}
|
|
}
|
|
|
|
// Define the names of the functions we are looking for
|
|
const targetFunctionNames: Set<string> = new Set(['fail', 'hardAssert']);
|
|
|
|
// Interface to store information about found call sites
|
|
interface CallSiteInfo {
|
|
fileName: string;
|
|
functionName: string;
|
|
line: number;
|
|
character: number;
|
|
argumentsText: string[]; // Added to store argument text
|
|
errorMessage: string | undefined;
|
|
assertionId: string;
|
|
}
|
|
|
|
/**
|
|
* Recursively finds all files with .ts extensions in a directory.
|
|
* @param dirPath The absolute path to the directory to scan.
|
|
* @returns An array of absolute paths to the found TypeScript files.
|
|
*/
|
|
function getTsFilesRecursive(dirPath: string): string[] {
|
|
let tsFiles: string[] = [];
|
|
try {
|
|
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(dirPath, entry.name);
|
|
|
|
if (entry.isDirectory()) {
|
|
// Ignore node_modules for performance and relevance
|
|
if (entry.name === 'node_modules') {
|
|
continue;
|
|
}
|
|
// Recursively scan subdirectories
|
|
tsFiles = tsFiles.concat(getTsFilesRecursive(fullPath));
|
|
} else if (entry.isFile() && entry.name.endsWith('.ts')) {
|
|
// Exclude declaration files (.d.ts) as they usually don't contain implementation
|
|
if (!entry.name.endsWith('.d.ts')) {
|
|
tsFiles.push(fullPath);
|
|
}
|
|
}
|
|
}
|
|
} catch (error: any) {
|
|
console.error(`Error reading directory ${dirPath}: ${error.message}`);
|
|
throw error;
|
|
}
|
|
return tsFiles;
|
|
}
|
|
|
|
/**
|
|
* Analyzes TypeScript source files to find calls to specific functions.
|
|
* @param filePaths An array of absolute paths to the TypeScript files to scan.
|
|
* @returns An array of objects detailing the found call sites.
|
|
*/
|
|
function findFunctionCalls(filePaths: string[]): CallSiteInfo[] {
|
|
const foundCalls: CallSiteInfo[] = [];
|
|
|
|
for (const filePath of filePaths) {
|
|
// Read the file content
|
|
const sourceText = fs.readFileSync(filePath, 'utf8');
|
|
|
|
// Create the SourceFile AST node
|
|
const sourceFile = ts.createSourceFile(
|
|
path.basename(filePath), // Use basename for AST node name
|
|
sourceText,
|
|
ts.ScriptTarget.ESNext,
|
|
true, // Set parent pointers
|
|
ts.ScriptKind.Unknown // Detect TS vs TSX automatically
|
|
);
|
|
|
|
// Define the visitor function
|
|
const visit = (node: ts.Node): void => {
|
|
// Check if the node is a CallExpression (e.g., myFunction(...))
|
|
if (ts.isCallExpression(node)) {
|
|
let functionName: string | null = null;
|
|
const expression = node.expression;
|
|
|
|
// Check if the call is directly to an identifier (e.g., fail())
|
|
if (ts.isIdentifier(expression)) {
|
|
functionName = expression.text;
|
|
}
|
|
|
|
// If we found a function name, and it's one we're looking for
|
|
if (functionName && targetFunctionNames.has(functionName)) {
|
|
// Get line and character number
|
|
const { line, character } = ts.getLineAndCharacterOfPosition(
|
|
sourceFile,
|
|
node.getStart() // Get start position of the call expression
|
|
);
|
|
|
|
// --- Extract Arguments ---
|
|
const argsText: string[] = [];
|
|
let errorMessage: string | undefined;
|
|
let assertionId: string | undefined;
|
|
if (node.arguments && node.arguments.length > 0) {
|
|
node.arguments.forEach((arg: ts.Expression) => {
|
|
// Get the source text of the argument node
|
|
argsText.push(arg.getText(sourceFile));
|
|
|
|
if (ts.isStringLiteral(arg)) {
|
|
errorMessage = arg.getText(sourceFile);
|
|
} else if (ts.isNumericLiteral(arg)) {
|
|
assertionId = arg.getText(sourceFile);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Store the information (add 1 to line/char for 1-based indexing)
|
|
foundCalls.push({
|
|
fileName: filePath, // Store the full path
|
|
functionName,
|
|
line: line + 1,
|
|
character: character + 1,
|
|
argumentsText: argsText, // Store the extracted arguments,
|
|
errorMessage,
|
|
assertionId: assertionId ?? 'INVALID'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Continue traversing down the AST
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
|
|
// Start traversal from the root SourceFile node
|
|
visit(sourceFile);
|
|
} // End loop through filePaths
|
|
|
|
return foundCalls;
|
|
}
|
|
|
|
// --- Action Handlers ---
|
|
|
|
function handleList(occurrences: CallSiteInfo[]): void {
|
|
if (occurrences.length === 0) {
|
|
log('No assertion ids found.');
|
|
return;
|
|
}
|
|
|
|
occurrences
|
|
.sort((a, b) => a.assertionId.localeCompare(b.assertionId))
|
|
.forEach(call => {
|
|
console.log(
|
|
`ID: ${call.assertionId}; MESSAGE: ${call.errorMessage}; SOURCE: '${
|
|
call.functionName
|
|
}' call at ${path.relative(process.cwd(), call.fileName)}:${
|
|
call.line
|
|
}:${call.character}`
|
|
);
|
|
});
|
|
}
|
|
|
|
function find(
|
|
occurrences: CallSiteInfo[],
|
|
targetId: string | number
|
|
): CallSiteInfo[] {
|
|
const target =
|
|
typeof targetId === 'number' ? targetId.toString(16) : targetId;
|
|
return occurrences.filter(o => String(o.assertionId) === String(target));
|
|
}
|
|
|
|
function handleFind(
|
|
occurrences: CallSiteInfo[],
|
|
targetId: string | number
|
|
): void {
|
|
const foundLocations = find(occurrences, targetId);
|
|
|
|
if (foundLocations.length === 0) {
|
|
log(`Assertion id "${targetId}" not found.`);
|
|
process.exit(1);
|
|
}
|
|
|
|
handleList(foundLocations);
|
|
}
|
|
|
|
function handleCheck(occurrences: CallSiteInfo[]): void {
|
|
if (occurrences.length === 0) {
|
|
log('No assertion ids found to check for duplicates.');
|
|
return;
|
|
}
|
|
const idCounts: { [id: string]: CallSiteInfo[] } = {};
|
|
|
|
occurrences.forEach(occ => {
|
|
// Count ID occurrences
|
|
const codeStr = String(occ.assertionId); // Use string representation as key
|
|
if (!idCounts[codeStr]) {
|
|
idCounts[codeStr] = [];
|
|
}
|
|
idCounts[codeStr].push(occ);
|
|
|
|
// validate formats
|
|
if (!/^0x[0-9a-f]{4}$/.test(occ.assertionId)) {
|
|
console.error(
|
|
`Invalid assertion ID '${occ.assertionId}'. Must match /^0x[0-9a-f]{4}$/`
|
|
);
|
|
|
|
const relativePath = path.relative(process.cwd(), occ.fileName);
|
|
console.error(`- at '${relativePath}:${occ.line}:${occ.character}`);
|
|
}
|
|
});
|
|
|
|
let duplicatesFound = false;
|
|
log('Checking for duplicate assertion id usage:');
|
|
Object.entries(idCounts).forEach(([code, locations]) => {
|
|
if (locations.length > 1) {
|
|
duplicatesFound = true;
|
|
console.error(
|
|
`\nDuplicate assertion id "${code}" found at ${locations.length} locations:`
|
|
);
|
|
locations.forEach(loc => {
|
|
const relativePath = path.relative(process.cwd(), loc.fileName);
|
|
console.error(`- ${relativePath}:${loc.line}:${loc.character}`);
|
|
});
|
|
}
|
|
});
|
|
|
|
if (!duplicatesFound) {
|
|
log('No duplicate assertion ids found.');
|
|
} else {
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function randomId(): string {
|
|
const randomBytes = new Uint8Array(2);
|
|
getRandomValues(randomBytes);
|
|
|
|
return (
|
|
'0x' +
|
|
Array.from(randomBytes)
|
|
.map(byte => byte.toString(16).padStart(2, '0'))
|
|
.join('')
|
|
);
|
|
}
|
|
|
|
function handleNew(occurrences: CallSiteInfo[]): void {
|
|
let newCode: string = randomId();
|
|
|
|
// If we find this code already is used, regenerate it.
|
|
while (find(occurrences, newCode).length > 0) {
|
|
newCode = randomId();
|
|
}
|
|
|
|
console.log(newCode);
|
|
}
|
|
|
|
// --- Main Execution ---
|
|
async function main(): Promise<void> {
|
|
const argv = yargs(hideBin(process.argv))
|
|
.usage('Usage: $0 [options]')
|
|
.option('dir', {
|
|
alias: 'D',
|
|
describe: 'Directory to scan recursively for TS files',
|
|
type: 'string',
|
|
demandOption: true
|
|
})
|
|
.option('verbose', {
|
|
alias: 'V',
|
|
describe: 'verbose',
|
|
type: 'boolean'
|
|
})
|
|
.option('find', {
|
|
alias: 'F',
|
|
describe: 'Find locations of a specific {assertionId}',
|
|
type: 'string',
|
|
nargs: 1
|
|
})
|
|
.option('list', {
|
|
alias: 'L',
|
|
describe: 'List all unique assertion ids found (default action)',
|
|
type: 'boolean'
|
|
})
|
|
.option('new', {
|
|
alias: 'N',
|
|
describe: 'Suggest a new assertion id based on existing ones',
|
|
type: 'boolean'
|
|
})
|
|
.option('check', {
|
|
alias: 'C',
|
|
describe: 'Check for duplicate usage of assertion ids',
|
|
type: 'boolean'
|
|
})
|
|
.check(argv => {
|
|
// Enforce mutual exclusivity among options *within* the scan command
|
|
const options = [argv.F, argv.L, argv.N, argv.C].filter(Boolean).length;
|
|
if (options > 1) {
|
|
throw new Error('Options -F, -L, -N, -C are mutually exclusive.');
|
|
}
|
|
return true;
|
|
})
|
|
.help()
|
|
.alias('help', 'h')
|
|
.strict()
|
|
.parse(); // Execute parsing
|
|
|
|
// Extract directory path (safe due to demandOption)
|
|
const targetDirectory = path.resolve(argv['dir'] as string);
|
|
|
|
// set verbosity
|
|
isVerbose = !!argv['verbose'];
|
|
|
|
// Validate directory
|
|
try {
|
|
const stats = fs.statSync(targetDirectory);
|
|
if (!stats.isDirectory()) {
|
|
console.error(
|
|
`Error: Provided path is not a directory: ${targetDirectory}`
|
|
);
|
|
process.exit(1);
|
|
}
|
|
} catch (error: any) {
|
|
console.error(
|
|
`Error accessing directory ${targetDirectory}: ${error.message}`
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
log(`Scanning directory: ${targetDirectory}`);
|
|
const filesToScan = getTsFilesRecursive(targetDirectory);
|
|
|
|
if (filesToScan.length === 0) {
|
|
log('No relevant .ts or .tsx files found.');
|
|
process.exit(0);
|
|
}
|
|
log(`Found ${filesToScan.length} files. Analyzing for assertion ids...`);
|
|
|
|
const allOccurrences = findFunctionCalls(filesToScan);
|
|
log(
|
|
`Scan complete. Found ${allOccurrences.length} potential assertion id occurrences.`
|
|
);
|
|
|
|
// Determine action based on flags
|
|
if (argv['find']) {
|
|
handleFind(allOccurrences, argv['find']);
|
|
} else if (argv['new']) {
|
|
handleNew(allOccurrences);
|
|
} else if (argv['check']) {
|
|
handleCheck(allOccurrences);
|
|
} else {
|
|
// Default action is List (-L or no flag)
|
|
handleList(allOccurrences);
|
|
}
|
|
}
|
|
|
|
// Run the main function
|
|
void main();
|