234 lines
7.3 KiB
JavaScript
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();
|