Skip to content

Commit bfeb9f9

Browse files
SevhenanivethakuruparanAyushi1972
authored
[DEV] Add backend version 1 to dev (#539)
Co-authored-by: Nivetha Kuruparan <niv.kuru@gmail.com> Co-authored-by: Ayushi Amin <66652121+Ayushi1972@users.noreply.github.com>
1 parent c4a01b7 commit bfeb9f9

57 files changed

Lines changed: 2162 additions & 984 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
name: Build and Release
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
8+
jobs:
9+
check-branch:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- name: Verify tag is on main
14+
run: |
15+
if [ "$(git branch --contains $GITHUB_REF)" != "* main" ]; then
16+
echo "Tag $GITHUB_REF is not on main branch"
17+
exit 1
18+
fi
19+
build:
20+
needs: check-branch
21+
runs-on: ${{ matrix.os }}
22+
strategy:
23+
matrix:
24+
os: [ubuntu-latest, windows-latest, macos-latest]
25+
include:
26+
- os: ubuntu-latest
27+
artifact_name: linux-x64
28+
- os: windows-latest
29+
artifact_name: windows-x64.exe
30+
- os: macos-latest
31+
artifact_name: macos-x64
32+
33+
steps:
34+
- uses: actions/checkout@v4
35+
36+
- name: Set up Python
37+
uses: actions/setup-python@v4
38+
with:
39+
python-version: "3.10"
40+
architecture: ${{ runner.os == 'Windows' && 'x64' || '' }}
41+
42+
- name: Install tools
43+
run: |
44+
python -m pip install --upgrade pip
45+
pip install pyinstaller
46+
47+
- name: Install package
48+
run: |
49+
pip install .
50+
51+
- name: Create Linux executable
52+
if: matrix.os == 'ubuntu-latest'
53+
run: |
54+
pyinstaller --onefile --name ecooptimizer-server $(which eco-ext)
55+
mv dist/ecooptimizer-server dist/ecooptimizer-server-${{ matrix.artifact_name }}
56+
57+
pyinstaller --onefile --name ecooptimizer-server-dev $(which eco-ext-dev)
58+
mv dist/ecooptimizer-server-dev dist/ecooptimizer-server-dev-${{ matrix.artifact_name }}
59+
60+
- name: Create Windows executable
61+
if: matrix.os == 'windows-latest'
62+
shell: pwsh
63+
run: |
64+
$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])"
65+
$pyPathProd = $entryProd.Split(':')[0].Replace('.', '\') + '.py'
66+
67+
$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])"
68+
$pyPathDev = $entryDev.Split(':')[0].Replace('.', '\') + '.py'
69+
70+
pyinstaller --onefile --name ecooptimizer-server "src/$pyPathProd"
71+
Move-Item dist\ecooptimizer-server.exe "dist\ecooptimizer-server-${{ matrix.artifact_name }}"
72+
73+
pyinstaller --onefile --name ecooptimizer-server-dev "src/$pyPathDev"
74+
Move-Item dist\ecooptimizer-server-dev.exe "dist\ecooptimizer-server-dev-${{ matrix.artifact_name }}"
75+
76+
- name: Create macOS executable
77+
if: matrix.os == 'macos-latest'
78+
run: |
79+
pyinstaller --onefile --name ecooptimizer-server $(which eco-ext)
80+
mv dist/ecooptimizer-server dist/ecooptimizer-server-${{ matrix.artifact_name }}
81+
82+
pyinstaller --onefile --name ecooptimizer-server-dev $(which eco-ext-dev)
83+
mv dist/ecooptimizer-server-dev dist/ecooptimizer-server-dev-${{ matrix.artifact_name }}
84+
85+
- name: Upload artifacts
86+
uses: actions/upload-artifact@v4
87+
with:
88+
name: artifacts-${{ matrix.os }}
89+
path: |
90+
dist/ecooptimizer-server-*
91+
dist/ecooptimizer-server-dev-*
92+
if-no-files-found: error
93+
94+
create-release:
95+
needs: build
96+
runs-on: ubuntu-latest
97+
steps:
98+
- name: Download all artifacts
99+
uses: actions/download-artifact@v4
100+
with:
101+
path: artifacts
102+
pattern: artifacts-*
103+
merge-multiple: false # Keep separate folders per OS
104+
105+
- name: Create release
106+
uses: softprops/action-gh-release@v1
107+
with:
108+
tag_name: ${{ github.ref }}
109+
name: ${{ github.ref_name }}
110+
body: |
111+
${{ github.event.head_commit.message }}
112+
113+
## EcoOptimizer Server Executables
114+
This release contains the standalone server executables for launching the EcoOptimizer analysis engine.
115+
These are designed to work with the corresponding **EcoOptimizer VS Code Extension**.
116+
117+
### Included Artifacts
118+
- **Production Server**: `ecooptimizer-server-<platform>`
119+
(Stable version for production use)
120+
- **Development Server**: `ecooptimizer-server-dev-<platform>`
121+
(Development version with debug features)
122+
123+
### Platform Support
124+
- Linux (`linux-x64`)
125+
- Windows (`windows-x64.exe`)
126+
- macOS (`macos-x64`)
127+
files: |
128+
artifacts/artifacts-ubuntu-latest/dist/*
129+
artifacts/artifacts-windows-latest/dist/*
130+
artifacts/artifacts-macos-latest/dist/*
131+
env:
132+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

src/ecooptimizer/__main__.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,24 @@
66

77
import libcst as cst
88

9-
from .utils.output_manager import LoggingManager
10-
from .utils.output_manager import save_file, save_json_files, copy_file_to_output
9+
from ecooptimizer.utils.output_manager import LoggingManager
10+
from ecooptimizer.utils.output_manager import save_file, save_json_files, copy_file_to_output
1111

1212

13-
from .api.routes.refactor_smell import ChangedFile, RefactoredData
13+
from ecooptimizer.api.routes.refactor_smell import ChangedFile, RefactoredData
1414

15-
from .measurements.codecarbon_energy_meter import CodeCarbonEnergyMeter
15+
from ecooptimizer.measurements.codecarbon_energy_meter import CodeCarbonEnergyMeter
1616

17-
from .analyzers.analyzer_controller import AnalyzerController
17+
from ecooptimizer.analyzers.analyzer_controller import AnalyzerController
1818

19-
from .refactorers.refactorer_controller import RefactorerController
19+
from ecooptimizer.refactorers.refactorer_controller import RefactorerController
2020

21-
from . import (
21+
from ecooptimizer import (
2222
SAMPLE_PROJ_DIR,
2323
SOURCE,
2424
)
2525

26-
from .config import CONFIG
26+
from ecooptimizer.config import CONFIG
2727

2828
loggingManager = LoggingManager()
2929

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

56+
enabled_smells = {
57+
"cached-repeated-calls": {"threshold": 2},
58+
"no-self-use": {},
59+
"use-a-generator": {},
60+
"too-many-arguments": {"max_args": 5},
61+
}
62+
5663
analyzer_controller = AnalyzerController()
57-
# update_smell_registry(["no-self-use"])
58-
smells_data = analyzer_controller.run_analysis(SOURCE)
64+
smells_data = analyzer_controller.run_analysis(SOURCE, enabled_smells)
5965
save_json_files("code_smells.json", [smell.model_dump() for smell in smells_data])
6066

6167
copy_file_to_output(SOURCE, "refactored-test-case.py")
Lines changed: 81 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,86 @@
1+
"""Controller class for coordinating multiple code analysis tools."""
2+
13
# pyright: reportOptionalMemberAccess=false
24
from pathlib import Path
5+
import traceback
36
from typing import Callable, Any
47

5-
from ..data_types.smell_record import SmellRecord
6-
7-
from ..config import CONFIG
8-
9-
from ..data_types.smell import Smell
8+
from ecooptimizer.data_types.smell_record import SmellRecord
9+
from ecooptimizer.config import CONFIG
10+
from ecooptimizer.data_types.smell import Smell
11+
from ecooptimizer.analyzers.pylint_analyzer import PylintAnalyzer
12+
from ecooptimizer.analyzers.ast_analyzer import ASTAnalyzer
13+
from ecooptimizer.analyzers.astroid_analyzer import AstroidAnalyzer
14+
from ecooptimizer.utils.smells_registry import retrieve_smell_registry
1015

11-
from .pylint_analyzer import PylintAnalyzer
12-
from .ast_analyzer import ASTAnalyzer
13-
from .astroid_analyzer import AstroidAnalyzer
14-
15-
from ..utils.smells_registry import retrieve_smell_registry
16+
logger = CONFIG["detectLogger"]
1617

1718

1819
class AnalyzerController:
20+
"""Orchestrates multiple code analysis tools and aggregates their results."""
21+
1922
def __init__(self):
20-
"""Initializes analyzers for different analysis methods."""
23+
"""Initializes analyzers for Pylint, AST, and Astroid analysis methods."""
2124
self.pylint_analyzer = PylintAnalyzer()
2225
self.ast_analyzer = ASTAnalyzer()
2326
self.astroid_analyzer = AstroidAnalyzer()
2427

25-
def run_analysis(self, file_path: Path, selected_smells: str | list[str] = "ALL"):
26-
"""
27-
Runs multiple analysis tools on the given Python file and logs the results.
28-
Returns a list of detected code smells.
29-
"""
28+
def run_analysis(
29+
self, file_path: Path, enabled_smells: dict[str, dict[str, int | str]] | list[str]
30+
) -> list[Smell]:
31+
"""Runs configured analyzers on a file and returns aggregated results.
32+
33+
Args:
34+
file_path: Path to the Python file to analyze
35+
enabled_smells: Dictionary or list specifying which smells to detect
3036
37+
Returns:
38+
list[Smell]: All detected code smells
39+
40+
Raises:
41+
TypeError: If no smells are selected for detection
42+
Exception: Any errors during analysis are logged and re-raised
43+
"""
3144
smells_data: list[Smell] = []
3245

33-
if not selected_smells:
34-
raise TypeError("At least 1 smell must be selected for detection")
46+
if not enabled_smells:
47+
raise TypeError("At least one smell must be selected for detection.")
3548

36-
SMELL_REGISTRY = retrieve_smell_registry(selected_smells)
49+
SMELL_REGISTRY = retrieve_smell_registry(enabled_smells)
3750

3851
try:
3952
pylint_smells = self.filter_smells_by_method(SMELL_REGISTRY, "pylint")
4053
ast_smells = self.filter_smells_by_method(SMELL_REGISTRY, "ast")
4154
astroid_smells = self.filter_smells_by_method(SMELL_REGISTRY, "astroid")
4255

43-
CONFIG["detectLogger"].info("🟢 Starting analysis process")
44-
CONFIG["detectLogger"].info(f"📂 Analyzing file: {file_path}")
56+
logger.info("🟢 Starting analysis process")
57+
logger.info(f"📂 Analyzing file: {file_path}")
4558

4659
if pylint_smells:
47-
CONFIG["detectLogger"].info(f"🔍 Running Pylint analysis on {file_path}")
60+
logger.info(f"🔍 Running Pylint analysis on {file_path}")
4861
pylint_options = self.generate_pylint_options(pylint_smells)
4962
pylint_results = self.pylint_analyzer.analyze(file_path, pylint_options)
5063
smells_data.extend(pylint_results)
51-
CONFIG["detectLogger"].info(
52-
f"✅ Pylint analysis completed. {len(pylint_results)} smells detected."
53-
)
64+
logger.info(f"✅ Pylint analysis completed. {len(pylint_results)} smells detected.")
5465

5566
if ast_smells:
56-
CONFIG["detectLogger"].info(f"🔍 Running AST analysis on {file_path}")
67+
logger.info(f"🔍 Running AST analysis on {file_path}")
5768
ast_options = self.generate_custom_options(ast_smells)
58-
ast_results = self.ast_analyzer.analyze(file_path, ast_options)
69+
ast_results = self.ast_analyzer.analyze(file_path, ast_options) # type: ignore
5970
smells_data.extend(ast_results)
60-
CONFIG["detectLogger"].info(
61-
f"✅ AST analysis completed. {len(ast_results)} smells detected."
62-
)
71+
logger.info(f"✅ AST analysis completed. {len(ast_results)} smells detected.")
6372

6473
if astroid_smells:
65-
CONFIG["detectLogger"].info(f"🔍 Running Astroid analysis on {file_path}")
74+
logger.info(f"🔍 Running Astroid analysis on {file_path}")
6675
astroid_options = self.generate_custom_options(astroid_smells)
67-
astroid_results = self.astroid_analyzer.analyze(file_path, astroid_options)
76+
astroid_results = self.astroid_analyzer.analyze(file_path, astroid_options) # type: ignore
6877
smells_data.extend(astroid_results)
69-
CONFIG["detectLogger"].info(
78+
logger.info(
7079
f"✅ Astroid analysis completed. {len(astroid_results)} smells detected."
7180
)
7281

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

87-
CONFIG["detectLogger"].info(f" • {smell.symbol} {line_info}: {smell.message}")
96+
logger.info(f" • {smell.symbol} {line_info}: {smell.message}")
8897
else:
89-
CONFIG["detectLogger"].info("🎉 No code smells detected.")
98+
logger.info("🎉 No code smells detected.")
9099

91100
except Exception as e:
92-
CONFIG["detectLogger"].error(f"❌ Error during analysis: {e!s}")
101+
logger.error(f"❌ Error during analysis: {e!s}")
102+
traceback.print_exc()
103+
raise e
93104

94105
return smells_data
95106

96107
@staticmethod
97108
def filter_smells_by_method(
98109
smell_registry: dict[str, SmellRecord], method: str
99110
) -> dict[str, SmellRecord]:
100-
filtered = {
111+
"""Filters smell registry by analysis method.
112+
113+
Args:
114+
smell_registry: Dictionary of all available smells
115+
method: Analysis method to filter by ('pylint', 'ast', or 'astroid')
116+
117+
Returns:
118+
dict[str, SmellRecord]: Filtered dictionary of smells for the specified method
119+
"""
120+
return {
101121
name: smell
102122
for name, smell in smell_registry.items()
103-
if smell["enabled"] and (method == smell["analyzer_method"])
123+
if smell["enabled"] and smell["analyzer_method"] == method
104124
}
105-
return filtered
106125

107126
@staticmethod
108127
def generate_pylint_options(filtered_smells: dict[str, SmellRecord]) -> list[str]:
109-
pylint_smell_symbols = []
110-
extra_pylint_options = [
111-
"--disable=all",
112-
]
128+
"""Generates Pylint command-line options from enabled smells.
113129
114-
for symbol, smell in zip(filtered_smells.keys(), filtered_smells.values()):
115-
pylint_smell_symbols.append(symbol)
130+
Args:
131+
filtered_smells: Dictionary of smells enabled for Pylint analysis
116132
133+
Returns:
134+
list[str]: Pylint command-line arguments
135+
"""
136+
pylint_options = ["--disable=all"]
137+
138+
for _smell_name, smell in filtered_smells.items():
117139
if len(smell["analyzer_options"]) > 0:
118140
for param_data in smell["analyzer_options"].values():
119141
flag = param_data["flag"]
120142
value = param_data["value"]
121143
if value:
122-
extra_pylint_options.append(f"{flag}={value}")
144+
pylint_options.append(f"{flag}={value}")
123145

124-
extra_pylint_options.append(f"--enable={','.join(pylint_smell_symbols)}")
125-
return extra_pylint_options
146+
pylint_options.append(f"--enable={','.join(filtered_smells.keys())}")
147+
return pylint_options
126148

127149
@staticmethod
128150
def generate_custom_options(
129151
filtered_smells: dict[str, SmellRecord],
130-
) -> list[tuple[Callable, dict[str, Any]]]: # type: ignore
131-
ast_options = []
132-
for smell in filtered_smells.values():
133-
method = smell["checker"]
134-
options = smell["analyzer_options"]
135-
ast_options.append((method, options))
136-
137-
return ast_options
152+
) -> list[tuple[Callable | None, dict[str, Any]]]: # type: ignore
153+
"""Generates options for custom AST/Astroid analyzers.
154+
155+
Args:
156+
filtered_smells: Dictionary of smells enabled for custom analysis
157+
158+
Returns:
159+
list[tuple]: List of (checker_function, options_dict) pairs
160+
"""
161+
return [(smell["checker"], smell["analyzer_options"]) for smell in filtered_smells.values()]

0 commit comments

Comments
 (0)