Files
AI/참고/playwright-main/utils/validate_deps.js
2026-05-12 19:40:31 +09:00

234 lines
7.3 KiB
JavaScript

#!/usr/bin/env node
/**
* Copyright (c) Microsoft Corporation.
*
* 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.
*/
// @ts-check
/**
* Validates DEPS.list against DEPS.true (generated by build_deps_true.js).
* Reports declared dependencies in DEPS.list that are not actually used by
* any file, according to DEPS.true.
*
* Usage: node utils/build_deps_true.js && node utils/validate_deps.js
*/
const fs = require('fs');
const path = require('path').posix;
const packagesDir = path.resolve(path.join(__dirname, '..', 'packages'));
const packages = new Map();
packages.set('web', packagesDir + '/web/src/');
packages.set('injected', packagesDir + '/injected/src/');
packages.set('isomorphic', packagesDir + '/isomorphic/');
packages.set('utils', packagesDir + '/utils/');
packages.set('testIsomorphic', packagesDir + '/playwright/src/isomorphic/');
let hasErrors = false;
function main() {
const depsListFiles = [];
findFiles(packagesDir, 'DEPS.list', depsListFiles);
for (const depsListPath of depsListFiles.sort()) {
const depsDir = path.dirname(depsListPath);
const depsTruePath = path.join(depsDir, 'DEPS.true');
// Parse DEPS.list into groups.
const declared = parseDepsFile(depsListPath, depsDir);
// Parse DEPS.true (actual imports). Missing file means no cross-dir deps.
const actual = fs.existsSync(depsTruePath) ? parseDepsTrue(depsTruePath) : new Map();
validateDirectory(depsDir, declared, actual);
}
if (hasErrors)
process.exit(1);
}
/**
* Parse a DEPS.list file into a Map<group, entry[]>.
* Each entry is { original, resolved } where resolved is the absolute path
* (or node_modules/... or special token).
*/
function parseDepsFile(filePath, depsDir) {
/** @type {Map<string, Array<{original: string, resolved: string}>>} */
const groups = new Map();
let currentGroup = '*';
groups.set('*', []);
for (const line of fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean).filter(l => !l.startsWith('#'))) {
const groupMatch = line.match(/^\[(.*)\]$/);
if (groupMatch) {
currentGroup = groupMatch[1];
if (!groups.has(currentGroup))
groups.set(currentGroup, []);
continue;
}
let resolved;
if (line === '***' || line === '**' || line === '"strict"') {
resolved = line;
} else if (line.startsWith('node_modules/')) {
resolved = line;
} else if (line.startsWith('@')) {
resolved = line.replace(/@([\w-]+)\/(.*)/, (_, arg1, arg2) => {
const base = packages.get(arg1);
return base ? base + arg2 : '<unknown>/' + arg1 + '/' + arg2;
});
} else {
resolved = path.resolve(depsDir, line);
}
groups.get(currentGroup).push({ original: line, resolved });
}
return groups;
}
/**
* Parse a DEPS.true file into a Map<fileName, Set<importPath>>.
* Import paths are relative to the DEPS.true directory (same as they appear in the file).
*/
function parseDepsTrue(filePath) {
/** @type {Map<string, Set<string>>} */
const files = new Map();
let currentFile = null;
for (const line of fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean)) {
const groupMatch = line.match(/^\[(.*)\]$/);
if (groupMatch) {
currentFile = groupMatch[1];
files.set(currentFile, new Set());
continue;
}
if (currentFile)
files.get(currentFile).add(line);
}
return files;
}
/**
* Check each entry in DEPS.list: is it matched by at least one actual import
* from a file the entry governs?
*/
function validateDirectory(depsDir, declared, actual) {
// Collect all actual import paths (resolved to absolute) for files governed by each group.
// [*] group governs all files; [file.ts] governs only that file.
const errors = [];
for (const [group, entries] of declared) {
// Skip groups with wildcard — everything is allowed.
if (entries.some(e => e.resolved === '***' || e.resolved === '**'))
continue;
// Determine which files this group governs.
/** @type {Set<string>} absolute import paths from governed files */
const governedImports = new Set();
if (group === '*') {
// All files in the directory.
for (const [, imports] of actual) {
for (const imp of imports)
governedImports.add(imp);
}
} else {
// Specific file.
const fileImports = actual.get(group);
if (fileImports) {
for (const imp of fileImports)
governedImports.add(imp);
}
}
for (const entry of entries) {
if (entry.resolved === '"strict"')
continue;
if (!isEntryUsed(entry, governedImports, depsDir))
errors.push(` Unused: '${entry.original}' in [${group}]`);
}
}
if (errors.length) {
hasErrors = true;
const rel = path.relative(packagesDir, path.join(depsDir, 'DEPS.list'));
console.log(rel + ':');
for (const error of errors)
console.log(error);
}
}
/**
* Check if a DEPS.list entry is matched by at least one actual import.
* The entry.resolved is an absolute path (possibly with ** glob suffix),
* or a node_modules/ specifier.
* governedImports are relative paths from DEPS.true (relative to depsDir).
*/
function isEntryUsed(entry, governedImports, depsDir) {
const { resolved } = entry;
for (const imp of governedImports) {
const absImp = imp.startsWith('node_modules/') ? imp : path.resolve(depsDir, imp);
if (resolved.startsWith('node_modules/')) {
if (absImp === resolved || absImp.startsWith(resolved + '/'))
return true;
continue;
}
// Glob: foo/** matches anything under foo/.
if (resolved.endsWith('**')) {
const parent = resolved.substring(0, resolved.length - 2);
if (absImp.startsWith(parent))
return true;
continue;
}
// Directory dep (e.g., ./codegen/ or ../protocol/): matches imports whose
// directory is at or under the resolved path.
if (isDirectory(resolved)) {
if (absImp.startsWith(resolved + '/') || absImp === resolved)
return true;
continue;
}
// Exact file match.
if (absImp === resolved)
return true;
// Match without extension.
if (absImp === resolved + '.ts' || absImp === resolved + '.tsx' || absImp === resolved + '.d.ts')
return true;
// The dep might point to a directory (with index.ts) that we resolved as a file.
const impDir = path.dirname(absImp);
if (impDir === resolved)
return true;
}
return false;
}
function isDirectory(p) {
return fs.existsSync(p) && fs.statSync(p).isDirectory();
}
function findFiles(dir, name, result) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.resolve(dir, entry.name);
if (entry.isDirectory() && entry.name !== 'node_modules' && entry.name !== 'bundles')
findFiles(full, name, result);
else if (entry.name === name)
result.push(full);
}
}
main();