Skip to content

Pyrefly support #160

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions cmd/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,9 @@ func runToolByName(toolName string, workDirectory string, pathsToCheck []string,
return tools.RunEnigma(workDirectory, tool.InstallDir, tool.Binaries["codacy-enigma-cli"], pathsToCheck, outputFile, outputFormat)
case "revive":
return reviveTool.RunRevive(workDirectory, tool.Binaries["revive"], pathsToCheck, outputFile, outputFormat)
case "pyrefly":
binaryPath := tool.Binaries["pyrefly"]
return tools.RunPyrefly(workDirectory, binaryPath, pathsToCheck, outputFile, outputFormat)
}
return fmt.Errorf("unsupported tool: %s", toolName)
}
Expand Down
1 change: 1 addition & 0 deletions plugins/tool-utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ func TestGetSupportedTools(t *testing.T) {
"lizard",
"codacy-enigma-cli",
"revive",
"pyrefly",
},
expectedError: false,
},
Expand Down
15 changes: 15 additions & 0 deletions plugins/tools/pyrefly/plugin.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: pyrefly
# Pyrefly: Fast, modern Python type checker (https://pyrefly.org/en/docs/installation/)
description: Pyrefly is a fast, modern static type checker for Python, designed for developer productivity and CI integration.
default_version: 0.22.0
runtime: python
runtime_binaries:
package_manager: python3
execution: python3
binaries:
- name: pyrefly
path: "venv/bin/pyrefly"
output_options:
file_flag: "--output"
analysis_options:
default_path: "."
78 changes: 78 additions & 0 deletions plugins/tools/pyrefly/test/actual.sarif
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"runs": [
{
"results": [
{
"level": "error",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/test_file.py"
},
"region": {
"startColumn": 24,
"startLine": 12
}
}
}
],
"message": {
"text": "Argument `Literal['one']` is not assignable to parameter `a` with type `int` in function `add_numbers`"
},
"ruleId": "bad-argument-type"
},
{
"level": "error",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "test_file.py"
},
"region": {
"startColumn": 25,
"startLine": 14
}
}
}
],
"message": {
"text": "Function declared to return `int` but is missing an explicit `return`"
},
"ruleId": "bad-return"
},
{
"level": "error",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/test_file.py"
},
"region": {
"startColumn": 12,
"startLine": 20
}
}
}
],
"message": {
"text": "Returned type `Literal[123]` is not assignable to declared return type `str`"
},
"ruleId": "bad-return"
}
],
"tool": {
"driver": {
"informationUri": "https://pyrefly.org",
"name": "Pyrefly",
"rules": null,
"version": "0.22.0"
}
}
}
],
"version": "2.1.0"
}
78 changes: 78 additions & 0 deletions plugins/tools/pyrefly/test/expected.sarif
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [
{
"results": [
{
"level": "error",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "test_file.py"
},
"region": {
"startColumn": 24,
"startLine": 12
}
}
}
],
"message": {
"text": "Argument `Literal['one']` is not assignable to parameter `a` with type `int` in function `add_numbers`"
},
"ruleId": "bad-argument-type"
},
{
"level": "error",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "test_file.py"
},
"region": {
"startColumn": 25,
"startLine": 14
}
}
}
],
"message": {
"text": "Function declared to return `int` but is missing an explicit `return`"
},
"ruleId": "bad-return"
},
{
"level": "error",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "test_file.py"
},
"region": {
"startColumn": 12,
"startLine": 20
}
}
}
],
"message": {
"text": "Returned type `Literal[123]` is not assignable to declared return type `str`"
},
"ruleId": "bad-return"
}
],
"tool": {
"driver": {
"informationUri": "https://pyrefly.org",
"name": "Pyrefly",
"rules": null,
"version": "0.22.0"
}
}
}
]
}
25 changes: 25 additions & 0 deletions plugins/tools/pyrefly/test/src/test_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Test file for pyrefly analysis
"""

def add_numbers(a: int, b: int) -> int:
return a + b

def call_with_wrong_type():
# This should trigger a type error in pyrefly
return add_numbers("one", 2)

def missing_return() -> int:
# This function is missing a return statement
pass

def wrong_return_type() -> str:
# This function returns an int instead of a str
return 123

if __name__ == "__main__":

Check notice on line 22 in plugins/tools/pyrefly/test/src/test_file.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

plugins/tools/pyrefly/test/src/test_file.py#L22

expected 2 blank lines after class or function definition, found 1 (E305)
call_with_wrong_type()
missing_return()
wrong_return_type()
12 changes: 8 additions & 4 deletions tools/language_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@ import (
"gopkg.in/yaml.v3"
)

//
// This file is responsible for building the languages-config.yaml file.
//

// buildToolLanguageInfoFromAPI builds tool language information from API data
// This is the core shared logic used by both GetToolLanguageMappingFromAPI and buildToolLanguageConfigFromAPI
func buildToolLanguageInfoFromAPI() (map[string]domain.ToolLanguageInfo, error) {
Expand Down Expand Up @@ -95,6 +91,14 @@ func buildToolLanguageInfoFromAPI() (map[string]domain.ToolLanguageInfo, error)
result[toolName] = configTool
}

// Fallback: Add Pyrefly mapping if not present in API
if _, ok := result["pyrefly"]; !ok {
result["pyrefly"] = domain.ToolLanguageInfo{
Name: "pyrefly",
Languages: []string{"Python"},
Extensions: []string{".py"},
}
}
return result, nil
}

Expand Down
78 changes: 78 additions & 0 deletions tools/pyreflyRunner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package tools

Check notice on line 1 in tools/pyreflyRunner.go

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

tools/pyreflyRunner.go#L1

should have a package comment

import (
"codacy/cli-v2/utils"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
)

// RunPyrefly executes Pyrefly type checking on the specified directory or files
func RunPyrefly(workDirectory string, binary string, files []string, outputFile string, outputFormat string) error {

Check warning on line 13 in tools/pyreflyRunner.go

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

tools/pyreflyRunner.go#L13

Method RunPyrefly has 56 lines of code (limit is 50)

Check failure on line 13 in tools/pyreflyRunner.go

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

tools/pyreflyRunner.go#L13

Method RunPyrefly has a cyclomatic complexity of 15 (limit is 10)
args := []string{"check"}

// Always use JSON output for SARIF conversion
var tempFile string
if outputFormat == "sarif" {
tmp, err := ioutil.TempFile("", "pyrefly-*.json")
if err != nil {
return fmt.Errorf("failed to create temporary file: %w", err)
}
tempFile = tmp.Name()
tmp.Close()
defer os.Remove(tempFile)
args = append(args, "--output", tempFile, "--output-format", "json")
} else if outputFile != "" {
args = append(args, "--output", outputFile)
}
if outputFormat == "json" && outputFile == "" {
args = append(args, "--output-format", "json")
}

// Detect config file (pyrefly.toml or pyproject.toml)
configFiles := []string{"pyrefly.toml", "pyproject.toml"}
for _, configFile := range configFiles {
if _, err := os.Stat(filepath.Join(workDirectory, configFile)); err == nil {
// Pyrefly auto-detects config, so no need to add a flag
break
}
}

// Add files to check, or "." for current directory
if len(files) > 0 {
args = append(args, files...)
} else {
args = append(args, ".")
}

cmd := exec.Command(binary, args...)
cmd.Dir = workDirectory
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

err := cmd.Run()
if err != nil {
if _, ok := err.(*exec.ExitError); !ok {
return fmt.Errorf("failed to run Pyrefly: %w", err)
}
}

if outputFormat == "sarif" {
jsonOutput, err := os.ReadFile(tempFile)
if err != nil {
return fmt.Errorf("failed to read Pyrefly output: %w", err)
}
sarifOutput := utils.ConvertPyreflyToSarif(jsonOutput)
if outputFile != "" {
err = os.WriteFile(outputFile, sarifOutput, 0644)
if err != nil {
return fmt.Errorf("failed to write SARIF output: %w", err)
}
} else {
fmt.Println(string(sarifOutput))
}
}
return nil
}
Loading
Loading