From 96fd345cce8ca6f9a7a587ce2f38e91c2b5b9b41 Mon Sep 17 00:00:00 2001 From: Suren Poghosyan Date: Fri, 23 Aug 2024 19:13:35 +0400 Subject: [PATCH] [antipatterns] --- .gitignore | 2 ++ dist/ProjectAnalyzer.mjs | 36 --------------------------- dist/ProjectAnalyzer.mjs.map | 1 - src/Analyzers/TypeScriptAnalyzer.ts | 15 ++++++++++- src/Patterns/CodeSmellPattern.ts | 27 ++++++++++++++++++++ src/Patterns/GodObjectASTPattern.ts | 22 ++++++++++++++++ src/Patterns/GodeObjectPattern.ts | 24 ++++++++++++++++++ src/Patterns/MagicNumbersPattern.ts | 20 +++++++++++++++ src/Patterns/ShotgunSurgeryPattern.ts | 24 ++++++++++++++++++ 9 files changed, 133 insertions(+), 38 deletions(-) delete mode 100644 dist/ProjectAnalyzer.mjs delete mode 100644 dist/ProjectAnalyzer.mjs.map create mode 100644 src/Patterns/CodeSmellPattern.ts create mode 100644 src/Patterns/GodObjectASTPattern.ts create mode 100644 src/Patterns/GodeObjectPattern.ts create mode 100644 src/Patterns/MagicNumbersPattern.ts create mode 100644 src/Patterns/ShotgunSurgeryPattern.ts diff --git a/.gitignore b/.gitignore index 2db4792..ad95b19 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +dist/ +dist/Analyzers/ node_modules/ .node_modules/ built/* diff --git a/dist/ProjectAnalyzer.mjs b/dist/ProjectAnalyzer.mjs deleted file mode 100644 index 952dadc..0000000 --- a/dist/ProjectAnalyzer.mjs +++ /dev/null @@ -1,36 +0,0 @@ -import { AnalyzerEngine } from './AnalyzerEngine'; -import { getIgnorePatterns } from './Utils/ignorePatterns'; -import { isIgnored } from './Utils/isIgnored'; -import * as path from 'path'; -import * as fs from 'fs'; -export class ProjectAnalyzer { - constructor(basePath) { - this.engine = new AnalyzerEngine(); - this.ignorePatterns = getIgnorePatterns(basePath); - } - analyzeProject(directory) { - const files = this.getAllFiles(directory); - return this.engine.analyze(files); - } - getAllFiles(dir) { - const results = []; - fs.readdirSync(dir).forEach(file => { - const fullPath = path.join(dir, file); - const stat = fs.statSync(fullPath); - if (stat.isDirectory()) { - results.push(...this.getAllFiles(fullPath)); - } - else if (stat.isFile() && !isIgnored(fullPath, this.ignorePatterns)) { - results.push(fullPath); - } - }); - return results; - } -} -// Exporting the main functionality of the package -export function analyze(directory) { - const analyzer = new ProjectAnalyzer(directory); - const reports = analyzer.analyzeProject(directory); - return reports.map(report => report.generateSummary()); -} -//# sourceMappingURL=ProjectAnalyzer.mjs.map \ No newline at end of file diff --git a/dist/ProjectAnalyzer.mjs.map b/dist/ProjectAnalyzer.mjs.map deleted file mode 100644 index 7758c0d..0000000 --- a/dist/ProjectAnalyzer.mjs.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"ProjectAnalyzer.mjs","sourceRoot":"","sources":["../src/ProjectAnalyzer.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAE9C,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AAEzB,MAAM,OAAO,eAAe;IAI1B,YAAY,QAAgB;QAC1B,IAAI,CAAC,MAAM,GAAG,IAAI,cAAc,EAAE,CAAC;QACnC,IAAI,CAAC,cAAc,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IACpD,CAAC;IAED,cAAc,CAAC,SAAiB;QAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;QAC1C,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACpC,CAAC;IAEO,WAAW,CAAC,GAAW;QAC7B,MAAM,OAAO,GAAa,EAAE,CAAC;QAE7B,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;YACjC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YACtC,MAAM,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YAEnC,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;gBACvB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC;YAC9C,CAAC;iBAAM,IAAI,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC;gBACtE,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACzB,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,OAAO,OAAO,CAAC;IACjB,CAAC;CACF;AAED,kDAAkD;AAClD,MAAM,UAAU,OAAO,CAAC,SAAiB;IACvC,MAAM,QAAQ,GAAG,IAAI,eAAe,CAAC,SAAS,CAAC,CAAC;IAChD,MAAM,OAAO,GAAG,QAAQ,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC;IAEnD,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,eAAe,EAAE,CAAC,CAAC;AACzD,CAAC"} \ No newline at end of file diff --git a/src/Analyzers/TypeScriptAnalyzer.ts b/src/Analyzers/TypeScriptAnalyzer.ts index e90c2ce..2aa936f 100644 --- a/src/Analyzers/TypeScriptAnalyzer.ts +++ b/src/Analyzers/TypeScriptAnalyzer.ts @@ -5,9 +5,22 @@ import { Report } from '../Reports/Report'; import { Hint } from '../Reports/Hint'; import { CodeDuplicationPattern } from '../Patterns/CodeDuplicationPattern'; import { FileReader } from '../Utils/FileReader'; +import { CodeSmellPattern } from '../Patterns/CodeSmellPattern'; +import { GodObjectASTPattern } from '../Patterns/GodObjectASTPattern'; +import { GodObjectPattern } from '../Patterns/GodeObjectPattern'; +import { MagicNumbersPattern } from '../Patterns/MagicNumbersPattern'; +import { ShotgunSurgeryPattern } from '../Patterns/ShotgunSurgeryPattern'; export class TypeScriptAnalyzer extends BaseAnalyzer { - private patterns = [new CodeDuplicationPattern()]; + private patterns = [ + new CodeDuplicationPattern(), + new CodeSmellPattern(), + new GodObjectASTPattern(), + new GodObjectPattern(), + new MagicNumbersPattern(), + new ShotgunSurgeryPattern() + // Add other patterns here... + ]; supports(file: string): boolean { return file.endsWith('.ts'); diff --git a/src/Patterns/CodeSmellPattern.ts b/src/Patterns/CodeSmellPattern.ts new file mode 100644 index 0000000..2252d8a --- /dev/null +++ b/src/Patterns/CodeSmellPattern.ts @@ -0,0 +1,27 @@ +import { BasePattern } from './BasePattern'; +import { Hint } from '../Reports/Hint'; + +export class CodeSmellPattern extends BasePattern { + analyze(content: string): Hint[] { + const hints: Hint[] = []; + + // Deep Nesting + const deepNestingRegex = /if\s*\([^\)]+\)\s*\{[^]*(if\s*\([^\)]+\)\s*\{[^]*(if\s*\([^\)]+\)\s*\{[^]*\})\})\}/g; + let match: RegExpExecArray | null; + while ((match = deepNestingRegex.exec(content)) !== null) { + hints.push(new Hint(`Possible deep nesting detected. Consider refactoring.`)); + } + + // Long Methods + const methodRegex = /(?:public|private|protected|static)?\s*(?:async\s+)?(?:function)?\s*\w+\(.*?\)\s*\{[^]*?\}/g; + while ((match = methodRegex.exec(content)) !== null) { + const methodBody = match[0]; + const lineCount = methodBody.split('\n').length; + if (lineCount > 50) { // Threshold can be adjusted + hints.push(new Hint(`Possible long method detected with ${lineCount} lines. Consider refactoring.`)); + } + } + + return hints; + } +} \ No newline at end of file diff --git a/src/Patterns/GodObjectASTPattern.ts b/src/Patterns/GodObjectASTPattern.ts new file mode 100644 index 0000000..1c8d61e --- /dev/null +++ b/src/Patterns/GodObjectASTPattern.ts @@ -0,0 +1,22 @@ +import { BasePattern } from './BasePattern'; +import { Hint } from '../Reports/Hint'; +import * as ts from 'typescript'; + +export class GodObjectASTPattern extends BasePattern { + analyze(content: string): Hint[] { + const hints: Hint[] = []; + const sourceFile = ts.createSourceFile('file.ts', content, ts.ScriptTarget.Latest, true); + + function analyzeNode(node: ts.Node) { + if (ts.isClassDeclaration(node) && node.members.length > 20) { // Threshold can be adjusted + const className = node.name ? node.name.getText() : 'Unnamed Class'; + hints.push(new Hint(`Possible God Object detected: ${className} has too many members.`)); + } + node.forEachChild(analyzeNode); + } + + analyzeNode(sourceFile); + + return hints; + } +} \ No newline at end of file diff --git a/src/Patterns/GodeObjectPattern.ts b/src/Patterns/GodeObjectPattern.ts new file mode 100644 index 0000000..f37cbdf --- /dev/null +++ b/src/Patterns/GodeObjectPattern.ts @@ -0,0 +1,24 @@ +import { BasePattern } from './BasePattern'; +import { Hint } from '../Reports/Hint'; + +export class GodObjectPattern extends BasePattern { + analyze(content: string): Hint[] { + const hints: Hint[] = []; + const classRegex = /class\s+\w+\s*\{[^]*?\}/g; + const methodRegex = /(?:public|private|protected|static)?\s*(?:async\s+)?(?:function)?\s*\w+\(.*?\)\s*\{[^]*?\}/g; + const propertyRegex = /(?:public|private|protected|static)?\s*(?:async\s+)?\w+\s*:\s*\w+\s*;/g; + + let match: RegExpExecArray | null; + while ((match = classRegex.exec(content)) !== null) { + const classBody = match[0]; + const methodCount = (classBody.match(methodRegex) || []).length; + const propertyCount = (classBody.match(propertyRegex) || []).length; + + if (methodCount > 10 || propertyCount > 10) { // Threshold can be adjusted + hints.push(new Hint(`Possible God Object detected: Class has ${methodCount} methods and ${propertyCount} properties.`)); + } + } + + return hints; + } +} \ No newline at end of file diff --git a/src/Patterns/MagicNumbersPattern.ts b/src/Patterns/MagicNumbersPattern.ts new file mode 100644 index 0000000..3538747 --- /dev/null +++ b/src/Patterns/MagicNumbersPattern.ts @@ -0,0 +1,20 @@ +import { BasePattern } from './BasePattern'; +import { Hint } from '../Reports/Hint'; + +export class MagicNumbersPattern extends BasePattern { + analyze(content: string): Hint[] { + const hints: Hint[] = []; + const magicNumberRegex = /\b\d+\b/g; + const allowedNumbers = new Set([0, 1]); // Add more allowed numbers if necessary + + let match: RegExpExecArray | null; + while ((match = magicNumberRegex.exec(content)) !== null) { + const number = parseInt(match[0], 10); + if (!allowedNumbers.has(number)) { + hints.push(new Hint(`Possible Magic Number detected: "${number}" should be replaced with a named constant.`)); + } + } + + return hints; + } +} \ No newline at end of file diff --git a/src/Patterns/ShotgunSurgeryPattern.ts b/src/Patterns/ShotgunSurgeryPattern.ts new file mode 100644 index 0000000..c8d643f --- /dev/null +++ b/src/Patterns/ShotgunSurgeryPattern.ts @@ -0,0 +1,24 @@ +import { BasePattern } from './BasePattern'; +import { Hint } from '../Reports/Hint'; + +export class ShotgunSurgeryPattern extends BasePattern { + analyze(content: string): Hint[] { + const hints: Hint[] = []; + const methodOrPropertyRegex = /\b\w+\b/g; + const occurrences = new Map(); + + let match: RegExpExecArray | null; + while ((match = methodOrPropertyRegex.exec(content)) !== null) { + const name = match[0]; + occurrences.set(name, (occurrences.get(name) || 0) + 1); + } + + occurrences.forEach((count, name) => { + if (count > 5) { // Threshold can be adjusted + hints.push(new Hint(`Possible Shotgun Surgery detected: "${name}" is modified in ${count} places.`)); + } + }); + + return hints; + } +} \ No newline at end of file