Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
258a81c
Added feature for users to modify the smells additional option
nivethakuruparan Mar 17, 2025
6fdcf06
Merge branch 'dev' of https://github.com/ssm-lab/capstone--source-cod…
Sevhena Mar 21, 2025
03a779a
changed refactor_smell.py
nivethakuruparan Mar 22, 2025
a6c7d47
Merge branch 'nivetha/dev-features' of https://github.com/ssm-lab/cap…
Sevhena Mar 22, 2025
01d0d3e
Merge branch 'dev' of https://github.com/ssm-lab/capstone--source-cod…
Sevhena Mar 24, 2025
7f21245
Added in refactor all of type (#363)
Sevhena Mar 24, 2025
7744e1e
made small fix to smell model
Sevhena Mar 25, 2025
016a6a1
Cleaned up failing tests
nivethakuruparan Mar 25, 2025
fb46553
Added comments and cleaned up formatting in analyzers
nivethakuruparan Mar 25, 2025
87f0004
Added comments and cleaned up formatting in api
nivethakuruparan Mar 25, 2025
7424404
Merge branch 'dev' into nivetha/dev-features
nivethakuruparan Mar 25, 2025
968ed90
Added comments and cleaned up formatting in data_types
nivethakuruparan Mar 25, 2025
c303255
Added comments and cleaned up formatting in measurements
nivethakuruparan Mar 25, 2025
d0a3147
Added comments and cleaned up formatting in utils
nivethakuruparan Mar 25, 2025
af3b3f6
Added comments and cleaned up formatting in main
nivethakuruparan Mar 25, 2025
cf2d732
Added comments and cleaned up formatting in refactorers
nivethakuruparan Mar 25, 2025
190ba9b
Added ignores in LEC
nivethakuruparan Mar 25, 2025
13ed36f
Fixed up __main__.py
nivethakuruparan Mar 25, 2025
911b0be
Fixed up lpl refactorer
nivethakuruparan Mar 25, 2025
2fba666
Fixed up lpl refactorer
nivethakuruparan Mar 25, 2025
eac9cc0
fixed refactoring test
nivethakuruparan Mar 25, 2025
6865607
fix api refactor route test - normalize paths for OS
Sevhena Mar 25, 2025
1c0fcc3
Merge branch 'nivetha/dev-features' of https://github.com/ssm-lab/cap…
Sevhena Mar 25, 2025
a2712a6
try another fix for os path error in tests
Sevhena Mar 25, 2025
14231a4
Fixed caching issue with pylint
nivethakuruparan Mar 27, 2025
58b122a
Fixed CRC inserting at wrong line bug
nivethakuruparan Mar 27, 2025
5d8cf2c
Added vehicle_management test folder for demo
nivethakuruparan Mar 27, 2025
d9f42d3
Changed imports to absolute (#503)
Sevhena Mar 28, 2025
92daf73
Added draft workflow for packaging (#503)
Sevhena Mar 28, 2025
721a365
Made final build workflow (#503)
Sevhena Mar 28, 2025
4074d3f
fixed failing tests + refined api error handling
Sevhena Mar 28, 2025
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
132 changes: 132 additions & 0 deletions .github/workflows/package-build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
name: Build and Release

on:
push:
tags:
- "v*"

jobs:
check-branch:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Verify tag is on main
run: |
if [ "$(git branch --contains $GITHUB_REF)" != "* main" ]; then
echo "Tag $GITHUB_REF is not on main branch"
exit 1
fi
build:
needs: check-branch
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
include:
- os: ubuntu-latest
artifact_name: linux-x64
- os: windows-latest
artifact_name: windows-x64.exe
- os: macos-latest
artifact_name: macos-x64

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
architecture: ${{ runner.os == 'Windows' && 'x64' || '' }}

- name: Install tools
run: |
python -m pip install --upgrade pip
pip install pyinstaller

- name: Install package
run: |
pip install .

- name: Create Linux executable
if: matrix.os == 'ubuntu-latest'
run: |
pyinstaller --onefile --name ecooptimizer-server $(which eco-ext)
mv dist/ecooptimizer-server dist/ecooptimizer-server-${{ matrix.artifact_name }}

pyinstaller --onefile --name ecooptimizer-server-dev $(which eco-ext-dev)
mv dist/ecooptimizer-server-dev dist/ecooptimizer-server-dev-${{ matrix.artifact_name }}

- name: Create Windows executable
if: matrix.os == 'windows-latest'
shell: pwsh
run: |
$entryProd = python -c "from importlib.metadata import entry_points; print([ep.value for ep in entry_points()['console_scripts'] if ep.name == 'eco-ext'][0])"
$pyPathProd = $entryProd.Split(':')[0].Replace('.', '\') + '.py'

$entryDev = python -c "from importlib.metadata import entry_points; print([ep.value for ep in entry_points()['console_scripts'] if ep.name == 'eco-ext-dev'][0])"
$pyPathDev = $entryDev.Split(':')[0].Replace('.', '\') + '.py'

pyinstaller --onefile --name ecooptimizer-server "src/$pyPathProd"
Move-Item dist\ecooptimizer-server.exe "dist\ecooptimizer-server-${{ matrix.artifact_name }}"

pyinstaller --onefile --name ecooptimizer-server-dev "src/$pyPathDev"
Move-Item dist\ecooptimizer-server-dev.exe "dist\ecooptimizer-server-dev-${{ matrix.artifact_name }}"

- name: Create macOS executable
if: matrix.os == 'macos-latest'
run: |
pyinstaller --onefile --name ecooptimizer-server $(which eco-ext)
mv dist/ecooptimizer-server dist/ecooptimizer-server-${{ matrix.artifact_name }}

pyinstaller --onefile --name ecooptimizer-server-dev $(which eco-ext-dev)
mv dist/ecooptimizer-server-dev dist/ecooptimizer-server-dev-${{ matrix.artifact_name }}

- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: artifacts-${{ matrix.os }}
path: |
dist/ecooptimizer-server-*
dist/ecooptimizer-server-dev-*
if-no-files-found: error

create-release:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
pattern: artifacts-*
merge-multiple: false # Keep separate folders per OS

- name: Create release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ github.ref }}
name: ${{ github.ref_name }}
body: |
${{ github.event.head_commit.message }}

## EcoOptimizer Server Executables
This release contains the standalone server executables for launching the EcoOptimizer analysis engine.
These are designed to work with the corresponding **EcoOptimizer VS Code Extension**.

### Included Artifacts
- **Production Server**: `ecooptimizer-server-<platform>`
(Stable version for production use)
- **Development Server**: `ecooptimizer-server-dev-<platform>`
(Development version with debug features)

### Platform Support
- Linux (`linux-x64`)
- Windows (`windows-x64.exe`)
- macOS (`macos-x64`)
files: |
artifacts/artifacts-ubuntu-latest/dist/*
artifacts/artifacts-windows-latest/dist/*
artifacts/artifacts-macos-latest/dist/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
26 changes: 16 additions & 10 deletions src/ecooptimizer/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,24 @@

import libcst as cst

from .utils.output_manager import LoggingManager
from .utils.output_manager import save_file, save_json_files, copy_file_to_output
from ecooptimizer.utils.output_manager import LoggingManager
from ecooptimizer.utils.output_manager import save_file, save_json_files, copy_file_to_output


from .api.routes.refactor_smell import ChangedFile, RefactoredData
from ecooptimizer.api.routes.refactor_smell import ChangedFile, RefactoredData

from .measurements.codecarbon_energy_meter import CodeCarbonEnergyMeter
from ecooptimizer.measurements.codecarbon_energy_meter import CodeCarbonEnergyMeter

from .analyzers.analyzer_controller import AnalyzerController
from ecooptimizer.analyzers.analyzer_controller import AnalyzerController

from .refactorers.refactorer_controller import RefactorerController
from ecooptimizer.refactorers.refactorer_controller import RefactorerController

from . import (
from ecooptimizer import (
SAMPLE_PROJ_DIR,
SOURCE,
)

from .config import CONFIG
from ecooptimizer.config import CONFIG

loggingManager = LoggingManager()

Expand Down Expand Up @@ -53,9 +53,15 @@ def main():
logging.error("Could not retrieve initial emissions. Exiting.")
exit(1)

enabled_smells = {
"cached-repeated-calls": {"threshold": 2},
"no-self-use": {},
"use-a-generator": {},
"too-many-arguments": {"max_args": 5},
}

analyzer_controller = AnalyzerController()
# update_smell_registry(["no-self-use"])
smells_data = analyzer_controller.run_analysis(SOURCE)
smells_data = analyzer_controller.run_analysis(SOURCE, enabled_smells)
save_json_files("code_smells.json", [smell.model_dump() for smell in smells_data])

copy_file_to_output(SOURCE, "refactored-test-case.py")
Expand Down
138 changes: 81 additions & 57 deletions src/ecooptimizer/analyzers/analyzer_controller.py
Original file line number Diff line number Diff line change
@@ -1,77 +1,86 @@
"""Controller class for coordinating multiple code analysis tools."""

# pyright: reportOptionalMemberAccess=false
from pathlib import Path
import traceback
from typing import Callable, Any

from ..data_types.smell_record import SmellRecord

from ..config import CONFIG

from ..data_types.smell import Smell
from ecooptimizer.data_types.smell_record import SmellRecord
from ecooptimizer.config import CONFIG
from ecooptimizer.data_types.smell import Smell
from ecooptimizer.analyzers.pylint_analyzer import PylintAnalyzer
from ecooptimizer.analyzers.ast_analyzer import ASTAnalyzer
from ecooptimizer.analyzers.astroid_analyzer import AstroidAnalyzer
from ecooptimizer.utils.smells_registry import retrieve_smell_registry

from .pylint_analyzer import PylintAnalyzer
from .ast_analyzer import ASTAnalyzer
from .astroid_analyzer import AstroidAnalyzer

from ..utils.smells_registry import retrieve_smell_registry
logger = CONFIG["detectLogger"]


class AnalyzerController:
"""Orchestrates multiple code analysis tools and aggregates their results."""

def __init__(self):
"""Initializes analyzers for different analysis methods."""
"""Initializes analyzers for Pylint, AST, and Astroid analysis methods."""
self.pylint_analyzer = PylintAnalyzer()
self.ast_analyzer = ASTAnalyzer()
self.astroid_analyzer = AstroidAnalyzer()

def run_analysis(self, file_path: Path, selected_smells: str | list[str] = "ALL"):
"""
Runs multiple analysis tools on the given Python file and logs the results.
Returns a list of detected code smells.
"""
def run_analysis(
self, file_path: Path, enabled_smells: dict[str, dict[str, int | str]] | list[str]
) -> list[Smell]:
"""Runs configured analyzers on a file and returns aggregated results.

Args:
file_path: Path to the Python file to analyze
enabled_smells: Dictionary or list specifying which smells to detect

Returns:
list[Smell]: All detected code smells

Raises:
TypeError: If no smells are selected for detection
Exception: Any errors during analysis are logged and re-raised
"""
smells_data: list[Smell] = []

if not selected_smells:
raise TypeError("At least 1 smell must be selected for detection")
if not enabled_smells:
raise TypeError("At least one smell must be selected for detection.")

SMELL_REGISTRY = retrieve_smell_registry(selected_smells)
SMELL_REGISTRY = retrieve_smell_registry(enabled_smells)

try:
pylint_smells = self.filter_smells_by_method(SMELL_REGISTRY, "pylint")
ast_smells = self.filter_smells_by_method(SMELL_REGISTRY, "ast")
astroid_smells = self.filter_smells_by_method(SMELL_REGISTRY, "astroid")

CONFIG["detectLogger"].info("🟢 Starting analysis process")
CONFIG["detectLogger"].info(f"📂 Analyzing file: {file_path}")
logger.info("🟢 Starting analysis process")
logger.info(f"📂 Analyzing file: {file_path}")

if pylint_smells:
CONFIG["detectLogger"].info(f"🔍 Running Pylint analysis on {file_path}")
logger.info(f"🔍 Running Pylint analysis on {file_path}")
pylint_options = self.generate_pylint_options(pylint_smells)
pylint_results = self.pylint_analyzer.analyze(file_path, pylint_options)
smells_data.extend(pylint_results)
CONFIG["detectLogger"].info(
f"✅ Pylint analysis completed. {len(pylint_results)} smells detected."
)
logger.info(f"✅ Pylint analysis completed. {len(pylint_results)} smells detected.")

if ast_smells:
CONFIG["detectLogger"].info(f"🔍 Running AST analysis on {file_path}")
logger.info(f"🔍 Running AST analysis on {file_path}")
ast_options = self.generate_custom_options(ast_smells)
ast_results = self.ast_analyzer.analyze(file_path, ast_options)
ast_results = self.ast_analyzer.analyze(file_path, ast_options) # type: ignore
smells_data.extend(ast_results)
CONFIG["detectLogger"].info(
f"✅ AST analysis completed. {len(ast_results)} smells detected."
)
logger.info(f"✅ AST analysis completed. {len(ast_results)} smells detected.")

if astroid_smells:
CONFIG["detectLogger"].info(f"🔍 Running Astroid analysis on {file_path}")
logger.info(f"🔍 Running Astroid analysis on {file_path}")
astroid_options = self.generate_custom_options(astroid_smells)
astroid_results = self.astroid_analyzer.analyze(file_path, astroid_options)
astroid_results = self.astroid_analyzer.analyze(file_path, astroid_options) # type: ignore
smells_data.extend(astroid_results)
CONFIG["detectLogger"].info(
logger.info(
f"✅ Astroid analysis completed. {len(astroid_results)} smells detected."
)

if smells_data:
CONFIG["detectLogger"].info("⚠️ Detected Code Smells:")
logger.info("⚠️ Detected Code Smells:")
for smell in smells_data:
if smell.occurences:
first_occurrence = smell.occurences[0]
Expand All @@ -84,54 +93,69 @@ def run_analysis(self, file_path: Path, selected_smells: str | list[str] = "ALL"
else:
line_info = ""

CONFIG["detectLogger"].info(f" • {smell.symbol} {line_info}: {smell.message}")
logger.info(f" • {smell.symbol} {line_info}: {smell.message}")
else:
CONFIG["detectLogger"].info("🎉 No code smells detected.")
logger.info("🎉 No code smells detected.")

except Exception as e:
CONFIG["detectLogger"].error(f"❌ Error during analysis: {e!s}")
logger.error(f"❌ Error during analysis: {e!s}")
traceback.print_exc()
raise e

return smells_data

@staticmethod
def filter_smells_by_method(
smell_registry: dict[str, SmellRecord], method: str
) -> dict[str, SmellRecord]:
filtered = {
"""Filters smell registry by analysis method.

Args:
smell_registry: Dictionary of all available smells
method: Analysis method to filter by ('pylint', 'ast', or 'astroid')

Returns:
dict[str, SmellRecord]: Filtered dictionary of smells for the specified method
"""
return {
name: smell
for name, smell in smell_registry.items()
if smell["enabled"] and (method == smell["analyzer_method"])
if smell["enabled"] and smell["analyzer_method"] == method
}
return filtered

@staticmethod
def generate_pylint_options(filtered_smells: dict[str, SmellRecord]) -> list[str]:
pylint_smell_symbols = []
extra_pylint_options = [
"--disable=all",
]
"""Generates Pylint command-line options from enabled smells.

for symbol, smell in zip(filtered_smells.keys(), filtered_smells.values()):
pylint_smell_symbols.append(symbol)
Args:
filtered_smells: Dictionary of smells enabled for Pylint analysis

Returns:
list[str]: Pylint command-line arguments
"""
pylint_options = ["--disable=all"]

for _smell_name, smell in filtered_smells.items():
if len(smell["analyzer_options"]) > 0:
for param_data in smell["analyzer_options"].values():
flag = param_data["flag"]
value = param_data["value"]
if value:
extra_pylint_options.append(f"{flag}={value}")
pylint_options.append(f"{flag}={value}")

extra_pylint_options.append(f"--enable={','.join(pylint_smell_symbols)}")
return extra_pylint_options
pylint_options.append(f"--enable={','.join(filtered_smells.keys())}")
return pylint_options

@staticmethod
def generate_custom_options(
filtered_smells: dict[str, SmellRecord],
) -> list[tuple[Callable, dict[str, Any]]]: # type: ignore
ast_options = []
for smell in filtered_smells.values():
method = smell["checker"]
options = smell["analyzer_options"]
ast_options.append((method, options))

return ast_options
) -> list[tuple[Callable | None, dict[str, Any]]]: # type: ignore
"""Generates options for custom AST/Astroid analyzers.

Args:
filtered_smells: Dictionary of smells enabled for custom analysis

Returns:
list[tuple]: List of (checker_function, options_dict) pairs
"""
return [(smell["checker"], smell["analyzer_options"]) for smell in filtered_smells.values()]
Loading