Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Cmf.CLI.Commands.build.business.BusinessLinter.Abstractions;

internal interface ILintLogger
{
void Warning(string message);
void Error(string message);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Cmf.CLI.Commands.build.business.BusinessLinter.Abstractions;

internal interface ILintRule
{
string RuleName { get; }
string RuleDescription { get; }
bool IsEnabled { get; set; }
void Analyze(MethodDeclarationSyntax methodNode, string filePath, string className);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Collections.Generic;

namespace Cmf.CLI.Commands.build.business.BusinessLinter.Abstractions;

internal interface IRuleFactory
{
IEnumerable<ILintRule> CreateEnabledRules();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Cmf.CLI.Commands.build.business.BusinessLinter.Abstractions;
using Cmf.CLI.Commands.build.business.BusinessLinter.Rules;
using Microsoft.Extensions.DependencyInjection;

namespace Cmf.CLI.Commands.build.business.BusinessLinter.Extensions;

internal static class ServiceCollectionExtensions
{
public static void AddLinterServices(this ServiceCollection services)
{
// Add factory
services.AddSingleton<IRuleFactory, RuleFactory>();

// Add logger
services.AddSingleton<ILintLogger, LintLogger>();

// Add rules
services.AddTransient<ILintRule, NoLoadInForeachRule>();
}
}
21 changes: 21 additions & 0 deletions cmf-cli/Commands/build/business/BusinessLinter/LintLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Cmf.CLI.Commands.build.business.BusinessLinter.Abstractions;
using System;

namespace Cmf.CLI.Commands.build.business.BusinessLinter;

internal class LintLogger : ILintLogger
{
public void Warning(string message)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Warning: {message}");
Console.ResetColor();
}

public void Error(string message)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Error: {message}");
Console.ResetColor();
}
}
140 changes: 140 additions & 0 deletions cmf-cli/Commands/build/business/BusinessLinter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Business Package Linter

## Overview

The Business Package Linter is a code analysis tool designed to detect problematic code patterns in Business packages. It uses Roslyn (Microsoft.CodeAnalysis) to parse and analyze C# code files.

## Usage

The linter can be invoked using the CLI:

```bash
cmf build business lint <solution-path> [files...]
```

### Parameters

- `solution-path` (required): Path to the solution file (.sln)
- `files` (optional): Specific files to lint. If not provided, all files in the solution will be linted.

### Example

```bash
# Lint all files in the solution
cmf build business lint ./MyProject/MyProject.sln

# Lint specific files
cmf build business lint ./MyProject/MyProject.sln MyOrchestration.cs MyService.cs
```

## Rules

The linter currently implements the following rules:

### NoLoadInForeach

**Description**: Detects calls to `Load()` methods inside `foreach` loops, which can lead to performance issues.

**Rationale**: Calling `Load()` for each item in a loop can result in unnecessary resource usage and poor performance. Collection loads should be used instead.

**Example of problematic code**:

```csharp
foreach (DataRow dataRow in results.Tables[0].Rows)
{
IMaterial material = _entityFactory.Create<IMaterial>();
material.Name = (string)dataRow["Name"];
material.Load(); // Bad: Load() inside foreach
material.Facility.Load(); // Bad: Load() inside foreach
materials.Add(material);
}
```

**Recommended approach**:

```csharp
// Collect all materials first
var materials = new List<IMaterial>();
foreach (DataRow dataRow in results.Tables[0].Rows)
{
IMaterial material = _entityFactory.Create<IMaterial>();
material.Name = (string)dataRow["Name"];
materials.Add(material);
}

// Load all at once using collection load
materials.LoadCollection();
```

## Architecture

The linter follows a modular, extensible architecture:

```
BusinessLinter/
├── Abstractions/ # Interfaces for core components
│ ├── ILintLogger.cs # Logging interface
│ ├── ILintRule.cs # Base interface for linting rules
│ └── IRuleFactory.cs # Factory for creating rule instances
├── Rules/ # Individual linting rules
│ ├── BaseLintRule.cs # Base class for rules
│ └── NoLoadInForeachRule.cs
├── Extensions/ # Extension methods
│ └── ServiceCollectionExtensions.cs
├── LintLogger.cs # Console logger implementation
├── RuleFactory.cs # Rule factory implementation
├── SolutionLinter.cs # Main orchestrator
└── SolutionLoader.cs # Loads solutions using Roslyn
```

## Adding New Rules

To add a new linting rule:

1. Create a new class in the `Rules/` folder that inherits from `BaseLintRule`
2. Implement the required properties and methods:
- `RuleName`: Unique identifier for the rule
- `RuleDescription`: Human-readable description
- `Analyze()`: The logic to detect the code pattern

3. Register the rule in `Extensions/ServiceCollectionExtensions.cs`:

```csharp
services.AddTransient<ILintRule, YourNewRule>();
```

### Example Rule Implementation

```csharp
internal class MyCustomRule : BaseLintRule
{
public MyCustomRule(ILintLogger logger) : base(logger) { }

public override string RuleName => "MyCustomRule";

public override string RuleDescription => "Description of what this rule checks";

public override void Analyze(MethodDeclarationSyntax methodNode, string filePath, string className)
{
// Your analysis logic here
// Use _logger.Warning() or _logger.Error() to report issues
}
}
```

## Configuration

Rules can be enabled or disabled using the `IsEnabled` property. By default, all rules are enabled. Future enhancements may include configuration file support for more granular control.

## Testing

Unit tests for the linter are located in `tests/Specs/BusinessLinter.cs`. Tests use Moq and Autofac for dependency injection and mocking.

## Future Enhancements

- Configuration file support for enabling/disabling rules
- Rule severity levels (Warning, Error, Info)
- Custom rule parameters via configuration
- Integration with build pipelines
- HTML/JSON report generation
- Additional rules for common anti-patterns
26 changes: 26 additions & 0 deletions cmf-cli/Commands/build/business/BusinessLinter/RuleFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Cmf.CLI.Commands.build.business.BusinessLinter.Abstractions;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Cmf.CLI.Commands.build.business.BusinessLinter;

internal class RuleFactory : IRuleFactory
{
private readonly IServiceProvider _serviceProvider;

public RuleFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}

public IEnumerable<ILintRule> CreateEnabledRules()
{
// Get all registered rules from DI container
var rules = _serviceProvider.GetServices<ILintRule>();

// Return only enabled rules
return rules.Where(r => r.IsEnabled);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Cmf.CLI.Commands.build.business.BusinessLinter.Abstractions;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Cmf.CLI.Commands.build.business.BusinessLinter.Rules;

internal abstract class BaseLintRule : ILintRule
{
protected readonly ILintLogger _logger;

protected BaseLintRule(ILintLogger logger)
{
_logger = logger;
}

public abstract string RuleName { get; }
public abstract string RuleDescription { get; }
public bool IsEnabled { get; set; } = true;

public abstract void Analyze(MethodDeclarationSyntax methodNode, string filePath, string className);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using Cmf.CLI.Commands.build.business.BusinessLinter.Abstractions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Linq;

namespace Cmf.CLI.Commands.build.business.BusinessLinter.Rules;

internal class NoLoadInForeachRule : BaseLintRule
{
public NoLoadInForeachRule(ILintLogger logger) : base(logger)
{
}

public override string RuleName => "NoLoadInForeach";

public override string RuleDescription => "Load() method should not be called inside foreach loops";

public override void Analyze(MethodDeclarationSyntax methodNode, string filePath, string className)
{
if (methodNode.Body == null)
{
return;
}

// Find all foreach statements in the method
var foreachStatements = methodNode.Body.DescendantNodes()
.OfType<ForEachStatementSyntax>();

foreach (var foreachStatement in foreachStatements)
{
// Find all invocation expressions within the foreach
var invocations = foreachStatement.DescendantNodes()
.OfType<InvocationExpressionSyntax>();

foreach (var invocation in invocations)
{
// Check if this is a Load() method call
if (IsLoadMethodCall(invocation))
{
var lineSpan = invocation.GetLocation().GetLineSpan();
var line = lineSpan.StartLinePosition.Line + 1; // Line numbers are 0-based

_logger.Warning($"[{RuleName}] {filePath}:{line} - {RuleDescription}. Found in class '{className}', method '{methodNode.Identifier.Text}'.");
}
}
}
}

private bool IsLoadMethodCall(InvocationExpressionSyntax invocation)
{
// Check if the invocation is for a method named "Load"
// Handle both simple calls like "obj.Load()" and chained calls like "material.Facility.Load()"

if (invocation.Expression is MemberAccessExpressionSyntax memberAccess)
{
return memberAccess.Name.Identifier.ValueText == "Load";
}

if (invocation.Expression is IdentifierNameSyntax identifier)
{
return identifier.Identifier.ValueText == "Load";
}

return false;
}
}
Loading