Skip to content

Commit

Permalink
Massive rewrite
Browse files Browse the repository at this point in the history
  • Loading branch information
rafed committed Oct 11, 2024
1 parent eb7fcb6 commit a066668
Show file tree
Hide file tree
Showing 46 changed files with 907 additions and 976 deletions.
357 changes: 13 additions & 344 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,353 +1,22 @@
# codemetrica
# CodeMetrica

########################EXhibit B #########
CodeMetrica is a work-in-progress framework designed to perform static analysis for multiple programming languages. It focuses on calculating various source code metrics and detecting code smells, providing a generalized approach to analyzing and improving code quality across different languages.

To design an elegant and scalable solution for extracting different metrics (e.g., Lines of Code, comments) and detecting code smells (e.g., god class, complex conditionals), with varying implementations across programming languages, you can use the Strategy Pattern combined with Factory Pattern for each language. Here's how:
## Features

Design Steps:
Define Common Interfaces for Metrics and Code Smells: Create abstract interfaces or base classes for the metrics and code smells you want to calculate/detect. Each programming language will implement these interfaces according to its specifics.
* Multi-language support: CodeMetrica is built to support multiple programming languages, with specific support for Java and Python currently in place.
* Generic code smell detection: Detect common code smells like Long Method, God Class, etc., with language-specific implementations where necessary.
* Code metrics calculation: Easily compute various code metrics such as method length, cyclomatic complexity, and more.
* Extensible design: Easily add support for new languages and custom code metrics.

Create Separate Implementations for Each Programming Language: Implement the metrics and code smells for each programming language in its own class, adhering to the interface. This allows flexibility when adding new languages or code smells.

Factory or Registry Pattern for Language-Specific Metrics/Smells: Use a factory pattern or registry to instantiate the correct metric/smell detection class based on the programming language. This keeps the code modular and extensible.
## How It Works

Composite Pattern for Combining Metrics and Smells: Since you need multiple metrics and code smells, consider using the Composite Pattern to group multiple checks (metrics + smells) together.
* Parsing: Use ANTLR-generated parsers to extract abstract syntax trees (AST) for various languages.
* Metrics & Smells: Calculate metrics and detect code smells using the parsed AST, with specific logic per language when required.

Example Implementation:
1. Define Common Interfaces
First, define common interfaces or abstract classes for metrics and code smells that will be implemented by each language-specific class.
## Status

javascript
Copy code
// Abstract interface for calculating metrics
class Metrics {
calculateLOC() {
throw new Error('Method calculateLOC() must be implemented');
}
The project is still under development, with plans to expand language support and refine smell detection capabilities.

calculateComments() {
throw new Error('Method calculateComments() must be implemented');
}
}

// Abstract interface for detecting code smells
class CodeSmells {
detectGodClass() {
throw new Error('Method detectGodClass() must be implemented');
}

detectComplexConditional() {
throw new Error('Method detectComplexConditional() must be implemented');
}
}
2. Language-Specific Implementations
Each programming language (e.g., Python, JavaScript) will have its own implementation of the metrics and code smells interface.

javascript
Copy code
// Python-specific implementation for metrics
class PythonMetrics extends Metrics {
constructor(sourceCode) {
super();
this.sourceCode = sourceCode;
}

calculateLOC() {
// Count lines of code excluding comments
return this.sourceCode.split('\n').filter(line => line.trim() !== '').length;
}

calculateComments() {
// Count the number of comment lines in Python (starting with #)
return this.sourceCode.split('\n').filter(line => line.trim().startsWith('#')).length;
}
}

// Python-specific implementation for code smells
class PythonCodeSmells extends CodeSmells {
constructor(ast) {
super();
this.ast = ast; // Assume this is the AST from an ANTLR parser or similar
}

detectGodClass() {
// Logic to detect god class in Python
// Based on class size, number of methods, etc.
}

detectComplexConditional() {
// Logic to detect complex conditionals in Python
// Look for long chains of if-else statements
}
}
Similarly, you can create JavaScript or other language implementations.

javascript
Copy code
// JavaScript-specific implementation for metrics
class JavaScriptMetrics extends Metrics {
constructor(sourceCode) {
super();
this.sourceCode = sourceCode;
}

calculateLOC() {
// Count lines of code in JavaScript
}

calculateComments() {
// Count comments in JavaScript (// or /* */)
}
}

// JavaScript-specific implementation for code smells
class JavaScriptCodeSmells extends CodeSmells {
constructor(ast) {
super();
this.ast = ast;
}

detectGodClass() {
// Logic for detecting god class in JavaScript
}

detectComplexConditional() {
// Logic for detecting complex conditionals in JavaScript
}
}
3. Factory Pattern to Choose Implementation
Now, use the Factory Pattern to instantiate the appropriate class based on the programming language.

javascript
Copy code
class MetricsFactory {
static getMetrics(language, sourceCode) {
if (language === 'python') {
return new PythonMetrics(sourceCode);
} else if (language === 'javascript') {
return new JavaScriptMetrics(sourceCode);
}
throw new Error(`Metrics for language ${language} are not supported.`);
}
}

class CodeSmellsFactory {
static getCodeSmells(language, ast) {
if (language === 'python') {
return new PythonCodeSmells(ast);
} else if (language === 'javascript') {
return new JavaScriptCodeSmells(ast);
}
throw new Error(`Code smells for language ${language} are not supported.`);
}
}
4. Usage Example
You can now use the factories to calculate metrics and detect code smells based on the source code and language.

javascript
Copy code
const sourceCode = `# Python sample code\nclass MyClass:\n pass`;
const language = 'python';

// Get Python-specific metrics
const metrics = MetricsFactory.getMetrics(language, sourceCode);
console.log('Lines of Code:', metrics.calculateLOC());
console.log('Number of Comments:', metrics.calculateComments());

// Assume we have the AST (Abstract Syntax Tree) from parsing the code
const ast = {}; // Placeholder for actual AST

// Get Python-specific code smells
const smells = CodeSmellsFactory.getCodeSmells(language, ast);
console.log('God Class Detected:', smells.detectGodClass());
console.log('Complex Conditional Detected:', smells.detectComplexConditional());
Extending to Other Languages
To support another language, such as Java, you simply create new classes JavaMetrics and JavaCodeSmells, then add them to the factories.

Benefits of This Design:
Modularity: Each language’s specific logic for calculating metrics and detecting code smells is encapsulated in separate classes, which makes the code easy to maintain and extend.

Flexibility: You can easily add new programming languages by implementing their own Metrics and CodeSmells classes without affecting the rest of the system.

Separation of Concerns: Each class is responsible for a single aspect of the problem (e.g., detecting smells, calculating metrics), ensuring the code follows SOLID principles.

Factory Pattern: The factory pattern provides an elegant way to choose the correct implementation at runtime, based on the input programming language.

This design allows for flexibility and scalability while keeping the code clean and easy to maintain.



Exhibit CCC

Yes, this File class can be improved for readability, maintainability, and separation of concerns. Here are some suggestions for improving its design:

1. Avoid Nesting Visitor Classes Inside Methods
Nesting visitor classes inside methods can make the class harder to maintain and reuse. A better approach would be to move the ClassVisitor and FunctionVisitor outside of the File class. This separation improves code readability and allows reuse of visitors in other contexts if needed.

typescript
Copy code
class ClassVisitor extends PythonParserVisitor<void> {
classes: Class[] = [];

visitClass_def(ctx: Class_defContext) {
this.classes.push(new Class(ctx));
}
}

class FunctionVisitor extends PythonParserVisitor<void> {
functions: Function[] = [];

visitFunction_def(ctx: Function_defContext) {
this.functions.push(new Function(ctx));
}
}
2. Separate Concerns
The File class is currently handling file parsing, class/function extraction, and the visitor pattern implementation. You could break these responsibilities into separate components to follow the single responsibility principle.

File Parsing: A separate class or utility to handle parsing.
File Class: Should focus on high-level operations (e.g., getting classes and functions).
Visitors: Extract visitors into their own classes.
3. Improve Naming and Typing
Some improvements in naming and typing could make the code more intuitive. For example:

The getClasses() method could be named extractClasses() to indicate that it’s performing an action, not simply retrieving a value.
Similarly, getFunctions() could be renamed to extractFunctions().
4. Use a Parser Manager
Instead of directly calling Parser.getANTLRContext(path) in the constructor, it would be cleaner to have a dedicated parser manager class to handle the parsing. This would decouple the File class from the parser logic.

Example:

typescript
Copy code
class ParserManager {
static getContext(path: string): File_inputContext {
return Parser.getANTLRContext(path);
}
}

export class File extends CodeBlock<File_inputContext> {
path: string;

constructor(path: string) {
super(ParserManager.getContext(path));
this.path = path;
}

get name() {
return this.path.split('/').pop();
}

get extension() {
return this.path.includes('.') ? this.path.split('.').pop() : null;
}

extractClasses(): Class[] {
const visitor = new ClassVisitor();
visitor.visit(this.ctx);
return visitor.classes;
}

extractFunctions(): Function[] {
const visitor = new FunctionVisitor();
visitor.visit(this.ctx);
return visitor.functions;
}
}
5. Handle Errors Gracefully
If parsing fails (e.g., the file path is invalid or contains syntax errors), it’s good practice to handle this gracefully with error handling:

typescript
Copy code
constructor(path: string) {
try {
const context = ParserManager.getContext(path);
super(context);
this.path = path;
} catch (error) {
throw new Error(`Failed to parse file: ${path}. Error: ${error.message}`);
}
}
6. DRY Principle (Don’t Repeat Yourself)
You have similar logic for class and function extraction. You could abstract this pattern into a more generic method that accepts a visitor:

typescript
Copy code
private visitWith<T>(VisitorClass: new () => { visit: (ctx: File_inputContext) => void }): T[] {
const visitor = new VisitorClass();
visitor.visit(this.ctx);
return visitor as unknown as T[];
}

extractClasses(): Class[] {
return this.visitWith<Class>(ClassVisitor);
}

extractFunctions(): Function[] {
return this.visitWith<Function>(FunctionVisitor);
}
7. Encapsulation and Modularity
To enhance encapsulation, consider making the visitor classes private or at least moving them to their own modules/files. This will keep the class focused on its role and encapsulate complexity within appropriate modules.

Final Version (Refactored):
typescript
Copy code
// ParserManager.ts
export class ParserManager {
static getContext(path: string): File_inputContext {
return Parser.getANTLRContext(path);
}
}

// ClassVisitor.ts
export class ClassVisitor extends PythonParserVisitor<void> {
classes: Class[] = [];

visitClass_def(ctx: Class_defContext) {
this.classes.push(new Class(ctx));
}
}

// FunctionVisitor.ts
export class FunctionVisitor extends PythonParserVisitor<void> {
functions: Function[] = [];

visitFunction_def(ctx: Function_defContext) {
this.functions.push(new Function(ctx));
}
}

// File.ts
export class File extends CodeBlock<File_inputContext> {
path: string;

constructor(path: string) {
super(ParserManager.getContext(path));
this.path = path;
}

get name(): string | null {
return this.path.split('/').pop();
}

get extension(): string | null {
return this.path.includes('.') ? this.path.split('.').pop() : null;
}

extractClasses(): Class[] {
const visitor = new ClassVisitor();
visitor.visit(this.ctx);
return visitor.classes;
}

extractFunctions(): Function[] {
const visitor = new FunctionVisitor();
visitor.visit(this.ctx);
return visitor.functions;
}
}
Key Changes:
Moved visitor classes outside to separate files.
Encapsulated parsing logic inside ParserManager.
Renamed getClasses and getFunctions to extractClasses and extractFunctions for better clarity.
Added basic error handling in the constructor.
This design improves modularity, reusability, and maintainability, keeping the File class focused on its main responsibilities.
Stay tuned for further updates as the project progresses!
Loading

0 comments on commit a066668

Please sign in to comment.