chrome-extension-test-framework
Version:
Universal testing framework for Chrome extensions - Fast static analysis without browser dependencies
279 lines (241 loc) • 11.2 kB
JavaScript
/**
* Console使用の分析
* console.logの使用をコンテキストに応じて評価
*/
class ConsoleAnalyzer {
constructor(config = {}) {
this.patterns = {
consoleLog: /console\.(log|debug|info|warn|error|table|trace|assert|group|groupEnd|groupCollapsed|time|timeEnd|timeLog|count|countReset|dir|dirxml|profile|profileEnd)\s*\(/g,
consoleDestructuring: /const\s*\{[^}]*(?:log|debug|info|warn|error)[^}]*\}\s*=\s*console/g,
consoleAlias: /const\s+\w+\s*=\s*console\.(log|debug|info|warn|error)/g,
debugComment: /\/\/\s*(debug|test|todo|fixme|hack|temp)/gi,
developmentFile: /\.(test|spec|mock|stub|development|dev)\.(js|ts)$/i,
productionExclude: /(dist|build|prod|production|release)\//i
};
// デフォルトの閾値(環境別)
const defaultThresholds = {
development: 100, // 開発ファイルでは100個まで許容
production: 10, // 本番コードでは10個まで
test: Infinity, // テストファイルでは無制限
'service-worker': 5, // Service Workerは最小限に
'content-script': 15, // Content Scriptは少し緩和
'production-build': Infinity // ビルド成果物は無視
};
// 設定ファイルからの閾値をマージ
this.thresholds = Object.assign({}, defaultThresholds, config.consoleThresholds || {});
// consoleメソッド別の乗数(error/warnは許容度高め)
this.methodWeights = {
log: 1.0,
debug: 1.0,
info: 0.8,
warn: 0.3,
error: 0.2,
table: 1.0,
trace: 0.5,
group: 0.5,
time: 0.3
};
}
/**
* ファイルタイプを判定(より詳細な環境検出)
*/
getFileType(filePath) {
// テストファイル
if (this.patterns.developmentFile.test(filePath)) {
return 'test';
}
if (filePath.includes('/test/') || filePath.includes('/tests/') ||
filePath.includes('/__tests__/') || filePath.includes('/spec/')) {
return 'test';
}
// デバッグ・開発用ファイル
if (filePath.includes('/debug/') || filePath.includes('/dev/') ||
filePath.includes('/development/') || filePath.includes('/demo/')) {
return 'development';
}
// ビルド成果物(無視すべき)
if (this.patterns.productionExclude.test(filePath)) {
return 'production-build';
}
// Service Worker(特別扱い)
if (filePath.includes('service-worker') || filePath.includes('background')) {
return 'service-worker';
}
// Content Script(特別扱い)
if (filePath.includes('content') || filePath.includes('inject')) {
return 'content-script';
}
// その他は本番コード
return 'production';
}
/**
* console使用を分析
*/
analyze(content, filePath) {
const fileType = this.getFileType(filePath);
// Direct console method calls
const directMatches = content.match(this.patterns.consoleLog) || [];
// Destructured console methods
const destructureMatches = content.match(this.patterns.consoleDestructuring) || [];
// Aliased console methods
const aliasMatches = content.match(this.patterns.consoleAlias) || [];
const allMatches = [...directMatches, ...destructureMatches, ...aliasMatches];
const details = this.getDetails(allMatches, content, destructureMatches.length > 0, aliasMatches.length > 0);
// 加重カウントを計算
const weightedCount = this.calculateWeightedCount(details.types);
const actualCount = allMatches.length;
const threshold = this.thresholds[fileType] || this.thresholds.production;
const result = {
count: actualCount,
weightedCount,
fileType,
threshold,
exceeds: weightedCount > threshold,
severity: this.getSeverity(weightedCount, threshold, fileType),
details
};
if (result.exceeds) {
result.suggestion = this.getSuggestion(fileType, actualCount, threshold, details);
}
return result;
}
/**
* 重要度を判定(環境を考慮)
*/
getSeverity(count, threshold, fileType) {
// テストファイルとビルド成果物は常に情報レベル
if (fileType === 'test' || fileType === 'production-build') return 'info';
// Service Workerは厳しく判定
if (fileType === 'service-worker') {
if (count === 0) return 'none';
if (count <= 2) return 'low';
if (count <= threshold) return 'medium';
if (count <= threshold * 1.5) return 'high';
return 'critical';
}
// 通常の判定
if (count === 0) return 'none';
if (count <= threshold / 2) return 'low';
if (count <= threshold) return 'medium';
if (count <= threshold * 2) return 'high';
return 'critical';
}
/**
* 詳細情報を取得
*/
getDetails(matches, content, hasDestructuring, hasAliasing) {
const types = {
log: 0,
debug: 0,
info: 0,
warn: 0,
error: 0,
table: 0,
trace: 0,
group: 0,
time: 0,
other: 0
};
matches.forEach(match => {
const typeMatch = match.match(/console\.(\w+)/);
if (typeMatch) {
const type = typeMatch[1];
if (types.hasOwnProperty(type)) {
types[type]++;
} else {
types.other++;
}
}
});
return {
types,
hasDebugComments: this.patterns.debugComment.test(content),
hasDestructuring,
hasAliasing
};
}
/**
* 修正提案を生成
*/
getSuggestion(fileType, count, threshold, details) {
const suggestions = [];
if (fileType === 'production') {
suggestions.push(`本番コードでconsole使用が${count}個検出されました(推奨: ${threshold}個以下)`);
suggestions.push('以下の方法で改善できます:');
suggestions.push('1. 本番ビルドでconsoleを自動削除するツールを使用');
suggestions.push('2. 環境変数でconsoleを制御: if (DEBUG) console.log(...)');
suggestions.push('3. 専用のロガーライブラリを使用');
if (details.hasDestructuring || details.hasAliasing) {
suggestions.push('\n⚠️ 間接的なconsole使用が検出されました(デストラクチャリングまたはエイリアス)');
}
} else if (fileType === 'development') {
suggestions.push(`開発ファイルでのconsole使用は問題ありませんが、${count}個は多すぎる可能性があります`);
suggestions.push('適切なログレベルを使用することを検討してください');
} else if (fileType === 'service-worker') {
suggestions.push(`Service Workerでconsole使用が${count}個検出されました(推奨: ${threshold}個以下)`);
suggestions.push('Service Workerでのconsole使用は最小限にすべきです:');
suggestions.push('1. パフォーマンスへの影響を考慮');
suggestions.push('2. chrome.runtime.lastErrorのチェックのみに限定');
suggestions.push('3. デバッグ時のみ有効化: if (chrome.runtime.id.includes("development"))');
} else if (fileType === 'content-script') {
suggestions.push(`Content Scriptでconsole使用が${count}個検出されました(推奨: ${threshold}個以下)`);
suggestions.push('Content Scriptでのconsole使用に注意:');
suggestions.push('1. ホストページのコンソールを汚染しないよう配慮');
suggestions.push('2. プレフィックスを付ける: console.log("[Extension]", ...)');
suggestions.push('3. 本番環境では無効化を推奨');
}
// console.errorとconsole.warnの使用を推奨
if (details.types.log > 0 && (details.types.error > 0 || details.types.warn > 0)) {
const errorWarnRatio = (details.types.error + details.types.warn) / details.types.log;
if (errorWarnRatio < 0.3) {
suggestions.push('\n💡 console.logの代わりにconsole.warnやconsole.errorの使用を検討してください');
}
}
return suggestions.join('\n');
}
/**
* レポート用のサマリーを生成
*/
generateSummary(results) {
const summary = {
total: 0,
byFileType: {},
bySeverity: {},
suggestions: []
};
results.forEach(result => {
summary.total += result.count;
// ファイルタイプ別
if (!summary.byFileType[result.fileType]) {
summary.byFileType[result.fileType] = {
files: 0,
count: 0
};
}
summary.byFileType[result.fileType].files++;
summary.byFileType[result.fileType].count += result.count;
// 重要度別
if (!summary.bySeverity[result.severity]) {
summary.bySeverity[result.severity] = 0;
}
summary.bySeverity[result.severity]++;
});
// 全体的な提案
if (summary.byFileType.production && summary.byFileType.production.count > 10) {
summary.suggestions.push('本番コードでのconsole使用を削減することを推奨します');
}
return summary;
}
/**
* メソッド別の重みづけでカウントを計算
*/
calculateWeightedCount(types) {
let weighted = 0;
for (const [method, count] of Object.entries(types)) {
const weight = this.methodWeights[method] || 1.0;
weighted += count * weight;
}
return Math.round(weighted);
}
}
module.exports = ConsoleAnalyzer;