chrome-extension-test-framework
Version:
Universal testing framework for Chrome extensions - Fast static analysis without browser dependencies
375 lines (330 loc) • 14.1 kB
JavaScript
/**
* ディレクトリ構造分析クラス
* Chrome拡張機能のディレクトリ構造を分析し、複雑度や深度の問題を検出する
*/
const fs = require('fs').promises;
const path = require('path');
class DirectoryAnalyzer {
constructor(options = {}) {
this.options = {
maxDepth: options.maxDepth || 5,
maxPathLength: options.maxPathLength || 260, // Windows limit
maxFilesPerDirectory: options.maxFilesPerDirectory || 50,
skipDotFiles: options.skipDotFiles !== false,
...options
};
// 分析結果
this.results = {
structure: {},
metrics: {
maxDepth: 0,
totalFiles: 0,
totalDirectories: 0,
averageDepth: 0,
deepestPath: '',
longestPath: '',
largestDirectory: { path: '', count: 0 }
},
issues: [],
suggestions: []
};
// ExcludeManagerを使用
this.excludeManager = options.excludeManager || null;
// ExcludeManagerがない場合のフォールバック
this.excludeDirs = new Set([
'node_modules',
'.git',
'.svn',
'.hg',
'dist',
'build',
'.cache',
'coverage',
'.nyc_output',
'test-results'
]);
}
/**
* ディレクトリ構造を分析
*/
async analyze(dirPath) {
// 結果をリセット
this.results = {
structure: {},
metrics: {
maxDepth: 0,
totalFiles: 0,
totalDirectories: 0,
averageDepth: 0,
deepestPath: '',
longestPath: '',
largestDirectory: { path: '', count: 0 }
},
issues: [],
suggestions: []
};
// ディレクトリ構造を構築
await this.buildStructure(dirPath, this.results.structure, '', 0);
// メトリクスを計算
this.calculateMetrics();
// 問題を検出
this.detectIssues();
// 提案を生成
this.generateSuggestions();
return this.results;
}
/**
* ディレクトリ構造を再帰的に構築
*/
async buildStructure(currentPath, structure, relativePath, depth) {
try {
const entries = await fs.readdir(currentPath, { withFileTypes: true });
const files = [];
const directories = [];
for (const entry of entries) {
// 除外ディレクトリをスキップ
const entryPath = path.join(currentPath, entry.name);
// ExcludeManagerがある場合はそれを使用
if (this.excludeManager) {
if (entry.isDirectory() && this.excludeManager.shouldExcludeDirectory(entryPath)) {
continue;
}
if (!entry.isDirectory() && this.excludeManager.shouldExclude(entryPath)) {
continue;
}
} else {
// フォールバック: 元の除外ロジック
if (entry.isDirectory() && this.excludeDirs.has(entry.name)) {
continue;
}
}
// ドットファイルをスキップ(設定による)
if (this.options.skipDotFiles && entry.name.startsWith('.')) {
continue;
}
const fullPath = path.join(currentPath, entry.name);
const newRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name;
if (entry.isDirectory()) {
directories.push(entry.name);
this.results.totalDirectories++;
// サブディレクトリの構造を構築
structure[entry.name] = {
type: 'directory',
path: newRelativePath,
depth: depth + 1,
children: {}
};
// 深度を更新
if (depth + 1 > this.results.metrics.maxDepth) {
this.results.metrics.maxDepth = depth + 1;
this.results.metrics.deepestPath = newRelativePath;
}
// パス長を更新
if (fullPath.length > this.results.metrics.longestPath.length) {
this.results.metrics.longestPath = fullPath;
}
// 再帰的に処理
await this.buildStructure(fullPath, structure[entry.name].children, newRelativePath, depth + 1);
} else if (entry.isFile()) {
files.push(entry.name);
this.results.totalFiles++;
structure[entry.name] = {
type: 'file',
path: newRelativePath,
depth: depth + 1
};
// パス長を更新
if (fullPath.length > this.results.metrics.longestPath.length) {
this.results.metrics.longestPath = fullPath;
}
}
}
// 最大ファイル数のディレクトリを更新
const totalItems = files.length + directories.length;
if (totalItems > this.results.metrics.largestDirectory.count) {
this.results.metrics.largestDirectory = {
path: relativePath || '.',
count: totalItems,
files: files.length,
directories: directories.length
};
}
// メタデータを追加
if (relativePath === '') {
structure._meta = {
files: files.length,
directories: directories.length,
totalItems: totalItems
};
}
} catch (error) {
console.error(`Error analyzing directory ${currentPath}:`, error);
}
}
/**
* メトリクスを計算
*/
calculateMetrics() {
// 平均深度を計算
let totalDepth = 0;
let fileCount = 0;
const calculateDepthSum = (structure, currentDepth = 0) => {
for (const [name, item] of Object.entries(structure)) {
if (name === '_meta') continue;
if (item.type === 'file') {
totalDepth += currentDepth + 1;
fileCount++;
} else if (item.type === 'directory' && item.children) {
calculateDepthSum(item.children, currentDepth + 1);
}
}
};
calculateDepthSum(this.results.structure);
this.results.metrics.averageDepth = fileCount > 0 ? (totalDepth / fileCount).toFixed(2) : 0;
}
/**
* 問題を検出
*/
detectIssues() {
// 深すぎるディレクトリ構造
if (this.results.metrics.maxDepth > this.options.maxDepth) {
this.results.issues.push({
type: 'excessive-depth',
severity: 'high',
message: `Directory structure too deep (${this.results.metrics.maxDepth} levels)`,
details: `Deepest path: ${this.results.metrics.deepestPath}`,
recommendation: `Keep directory depth under ${this.options.maxDepth} levels`
});
}
// パスが長すぎる
if (this.results.metrics.longestPath.length > this.options.maxPathLength) {
this.results.issues.push({
type: 'path-too-long',
severity: 'critical',
message: `Path exceeds ${this.options.maxPathLength} characters`,
details: `Longest path (${this.results.metrics.longestPath.length} chars): ${this.results.metrics.longestPath}`,
recommendation: 'Shorten directory and file names to avoid Windows compatibility issues'
});
}
// ディレクトリ内のファイルが多すぎる
if (this.results.metrics.largestDirectory.count > this.options.maxFilesPerDirectory) {
this.results.issues.push({
type: 'too-many-files',
severity: 'medium',
message: `Directory contains too many items (${this.results.metrics.largestDirectory.count})`,
details: `Path: ${this.results.metrics.largestDirectory.path}`,
recommendation: `Organize files into subdirectories (max ${this.options.maxFilesPerDirectory} items per directory)`
});
}
// フラット過ぎる構造(すべてがルートにある)
if (this.results.metrics.maxDepth === 1 && this.results.totalFiles > 10) {
this.results.issues.push({
type: 'too-flat',
severity: 'low',
message: 'Directory structure is too flat',
details: `${this.results.totalFiles} files in root directory`,
recommendation: 'Organize files into logical subdirectories'
});
}
// 不均衡な構造
const avgDepth = parseFloat(this.results.metrics.averageDepth);
if (this.results.metrics.maxDepth > 3 && avgDepth < 1.5) {
this.results.issues.push({
type: 'unbalanced-structure',
severity: 'low',
message: 'Directory structure is unbalanced',
details: `Max depth: ${this.results.metrics.maxDepth}, Average depth: ${avgDepth}`,
recommendation: 'Consider reorganizing to create a more balanced structure'
});
}
}
/**
* 改善提案を生成
*/
generateSuggestions() {
// 深い構造の改善
if (this.results.metrics.maxDepth > this.options.maxDepth) {
this.results.suggestions.push({
type: 'flatten-structure',
priority: 'high',
suggestion: 'Flatten deep directory structures',
examples: [
'src/components/ui/forms/inputs/validators → src/validators',
'assets/images/icons/ui/buttons → assets/icons'
]
});
}
// 推奨ディレクトリ構造
if (this.results.totalFiles > 5 && this.results.metrics.maxDepth < 2) {
this.results.suggestions.push({
type: 'organize-files',
priority: 'medium',
suggestion: 'Organize files into standard directories',
examples: [
'Create js/ for JavaScript files',
'Create css/ for stylesheets',
'Create assets/ or images/ for media files',
'Create lib/ or vendor/ for third-party code'
]
});
}
// モジュール化の提案
if (this.results.totalFiles > 20) {
this.results.suggestions.push({
type: 'modularize',
priority: 'medium',
suggestion: 'Consider modularizing your extension',
examples: [
'Group related functionality into modules',
'Use ES6 modules for better organization',
'Implement a clear separation of concerns'
]
});
}
}
/**
* 構造の可視化(シンプルなツリー表示)
*/
visualizeStructure(structure = this.results.structure, prefix = '', isLast = true) {
const lines = [];
const entries = Object.entries(structure).filter(([name]) => name !== '_meta');
entries.forEach(([name, item], index) => {
const isLastItem = index === entries.length - 1;
const connector = isLast ? '└── ' : '├── ';
const extension = isLast ? ' ' : '│ ';
if (item.type === 'directory') {
lines.push(prefix + connector + name + '/');
if (item.children && Object.keys(item.children).length > 0) {
const childLines = this.visualizeStructure(
item.children,
prefix + extension,
isLastItem
);
lines.push(...childLines);
}
} else {
lines.push(prefix + connector + name);
}
});
return lines;
}
/**
* レポートを生成
*/
generateReport() {
return {
summary: {
totalFiles: this.results.totalFiles,
totalDirectories: this.results.totalDirectories,
maxDepth: this.results.metrics.maxDepth,
averageDepth: this.results.metrics.averageDepth,
issueCount: this.results.issues.length
},
metrics: this.results.metrics,
issues: this.results.issues,
suggestions: this.results.suggestions,
tree: this.visualizeStructure().join('\n')
};
}
}
module.exports = DirectoryAnalyzer;