diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e40dbb73..a66df28e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,6 +25,13 @@ jobs: pip install setuptools==69.5.1 wheel pip install -r requirements.txt + # Static analysis tools + - name: Static Code Analysis + if: runner.os == 'Linux' + run: | + pip install mypy==1.14.1 flake8==7.0.0 + python static_analysis.py + - name: Run tests run: python -m pytest -s -rs diff --git a/lean/commands/lean.py b/lean/commands/lean.py index 0de5c04d..749000bb 100644 --- a/lean/commands/lean.py +++ b/lean/commands/lean.py @@ -10,7 +10,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import Optional from click import group, option, Context, pass_context, echo diff --git a/lean/commands/live/deploy.py b/lean/commands/live/deploy.py index de7576ed..669666bf 100644 --- a/lean/commands/live/deploy.py +++ b/lean/commands/live/deploy.py @@ -12,7 +12,7 @@ # limitations under the License. from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple +from typing import List, Optional, Tuple from click import option, argument, Choice from lean.click import LeanCommand, PathParameter from lean.components.util.name_rename import rename_internal_config_to_user_friendly_format diff --git a/lean/components/api/live_client.py b/lean/components/api/live_client.py index 8dbc862e..729e2d8a 100644 --- a/lean/components/api/live_client.py +++ b/lean/components/api/live_client.py @@ -11,11 +11,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from datetime import datetime from typing import List, Optional from lean.components.api.api_client import * -from lean.models.api import QCFullLiveAlgorithm, QCLiveAlgorithmStatus, QCMinimalLiveAlgorithm, QCNotificationMethod, QCRestResponse +from lean.models.api import QCFullLiveAlgorithm, QCMinimalLiveAlgorithm, QCNotificationMethod, QCRestResponse class LiveClient: diff --git a/lean/components/util/compiler.py b/lean/components/util/compiler.py index 585c1a4d..4ec3fa62 100644 --- a/lean/components/util/compiler.py +++ b/lean/components/util/compiler.py @@ -111,13 +111,14 @@ def _compile() -> Dict[str, Any]: "mounts": [], "volumes": {} } - lean_runner.mount_project_and_library_directories(project_dir, run_options) - lean_runner.setup_language_specific_run_options(run_options, project_dir, algorithm_file, False, False) project_config = project_config_manager.get_project_config(project_dir) engine_image = cli_config_manager.get_engine_image( project_config.get("engine-image", None)) + lean_runner.mount_project_and_library_directories(project_dir, run_options) + lean_runner.setup_language_specific_run_options(run_options, project_dir, algorithm_file, False, False, engine_image) + message["result"] = docker_manager.run_image(engine_image, **run_options) temp_manager.delete_temporary_directories_when_done = False return message @@ -153,8 +154,7 @@ def _parse_python_errors(python_output: str, color_coding_required: bool) -> lis errors.append(f"{bcolors.FAIL}Build Error File: {match[0]} Line {match[1]} Column {match[2]} - {match[3]}{bcolors.ENDC}\n") else: errors.append(f"Build Error File: {match[0]} Line {match[1]} Column {match[2]} - {match[3]}\n") - - for match in re.findall(r"\*\*\* Sorry: ([^(]+) \(([^,]+), line (\d+)\)", python_output): + for match in findall(r"\*\*\* Sorry: ([^(]+) \(([^,]+), line (\d+)\)", python_output): if color_coding_required: errors.append(f"{bcolors.FAIL}Build Error File: {match[1]} Line {match[2]} Column 0 - {match[0]}{bcolors.ENDC}\n") else: diff --git a/lean/components/util/project_manager.py b/lean/components/util/project_manager.py index 60dffc12..25533217 100644 --- a/lean/components/util/project_manager.py +++ b/lean/components/util/project_manager.py @@ -366,7 +366,6 @@ def restore_csharp_project(self, csproj_file: Path, no_local: bool) -> None: """ from shutil import which from subprocess import run, STDOUT, PIPE - from lean.models.errors import MoreInfoError if no_local: return diff --git a/lean/main.py b/lean/main.py index 851cd1f2..061c721b 100644 --- a/lean/main.py +++ b/lean/main.py @@ -97,7 +97,7 @@ def main() -> None: if temp_manager.delete_temporary_directories_when_done: temp_manager.delete_temporary_directories() except Exception as exception: - from traceback import format_exc, print_exc + from traceback import format_exc from click import UsageError, Abort from requests import exceptions from io import StringIO diff --git a/static_analysis.py b/static_analysis.py new file mode 100644 index 00000000..89f5eaf4 --- /dev/null +++ b/static_analysis.py @@ -0,0 +1,152 @@ +import subprocess +import sys + +def display_warning_summary(warnings): + print("\nWarnings:") + unused_count = sum(1 for e in warnings if e.startswith('F401:')) + if unused_count > 0: + print(f" - Unused imports: {unused_count}") + + print(" Consider addressing warnings in future updates.") + +def run_analysis(): + print("Running static analysis...") + print("=" * 60) + + all_critical_errors = [] + all_warnings = [] + + # Check for missing arguments with mypy - CRITICAL + print("\n1. Checking for missing function arguments...") + print("-" * 40) + + result = subprocess.run( + ["python", "-m", "mypy", "lean/", + "--show-error-codes", + "--no-error-summary", + "--ignore-missing-imports", + "--check-untyped-defs"], + capture_output=True, + text=True + ) + + # Filter for critical call argument mismatches + call_arg_errors = [] + + for line in (result.stdout + result.stderr).split('\n'): + if not line.strip(): + continue + + # Look for call-arg errors (this covers both "too many" and "missing" arguments) + if '[call-arg]' in line: + # Skip false positives + if any(pattern in line for pattern in + ['click.', 'subprocess.', 'Module "', 'has incompatible type "Optional', + 'validator', 'pydantic', '__call__', 'OnlyValueValidator', 'V1Validator', + 'QCParameter', 'QCBacktest']): + continue + call_arg_errors.append(line.strip()) + + # Display call argument mismatches + if call_arg_errors: + print("CRITICAL: Missing function arguments found:") + for error in call_arg_errors: + # Clean path for better display + clean_error = error.replace('/home/runner/work/lean-cli/lean-cli/', '') + print(f" {clean_error}") + + all_critical_errors.extend(call_arg_errors) + else: + print("No argument mismatch errors found") + + # Check for undefined variables with flake8 - CRITICAL + print("\n2. Checking for undefined variables...") + print("-" * 40) + + result = subprocess.run( + ["python", "-m", "flake8", "lean/", + "--select=F821", + "--ignore=ALL", + "--count"], + capture_output=True, + text=True + ) + + if result.stdout.strip() and result.stdout.strip() != "0": + detail = subprocess.run( + ["python", "-m", "flake8", "lean/", "--select=F821", "--ignore=ALL"], + capture_output=True, + text=True + ) + + undefined_errors = [e.strip() for e in detail.stdout.split('\n') if e.strip()] + print(f"CRITICAL: {len(undefined_errors)} undefined variable(s) found:") + + for error in undefined_errors: + print(f" {error}") + + all_critical_errors.extend([f"F821: {e}" for e in undefined_errors]) + else: + print("No undefined variables found") + + # Check for unused imports with flake8 - WARNING + print("\n3. Checking for unused imports...") + print("-" * 40) + + result = subprocess.run( + ["python", "-m", "flake8", "lean/", + "--select=F401", + "--ignore=ALL", + "--count", + "--exit-zero"], + capture_output=True, + text=True + ) + + if result.stdout.strip() and result.stdout.strip() != "0": + detail = subprocess.run( + ["python", "-m", "flake8", "lean/", "--select=F401", "--ignore=ALL", "--exit-zero"], + capture_output=True, + text=True + ) + + unused_imports = [e.strip() for e in detail.stdout.split('\n') if e.strip()] + if unused_imports: + print(f"WARNING: {len(unused_imports)} unused import(s) found:") + + for error in unused_imports: + print(f" {error}") + + all_warnings.extend([f"F401: {e}" for e in unused_imports]) + else: + print("No unused imports found") + else: + print("No unused imports found") + + print("\n" + "=" * 60) + + # Summary + if all_critical_errors: + total_errors = len(all_critical_errors) + print(f"BUILD FAILED: Found {total_errors} critical error(s)") + + print("\nSummary of critical errors:") + print(f" - Function call argument mismatches: {len(call_arg_errors)}") + undefined_count = sum(1 for e in all_critical_errors if e.startswith('F821:')) + print(f" - Undefined variables: {undefined_count}") + + if all_warnings: + display_warning_summary(all_warnings) + + return 1 + + if all_warnings: + print(f"BUILD PASSED with {len(all_warnings)} warning(s)") + display_warning_summary(all_warnings) + return 0 + + print("SUCCESS: All checks passed with no warnings") + return 0 + +if __name__ == "__main__": + sys.exit(run_analysis())