Skip to content
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
2 changes: 2 additions & 0 deletions crates/pyrefly_config/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ pub enum OutputFormat {
Json,
/// Emit GitHub Actions workflow commands
Github,
/// Emit CodeClimate issues in a JSON array (e.g. for GitLab Code Quality reports)
Codeclimate,
/// Only show error count, omitting individual errors
OmitErrors,
}
Expand Down
41 changes: 41 additions & 0 deletions pyrefly/lib/commands/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ use crate::commands::files::FilesArgs;
use crate::commands::util::CommandExitStatus;
use crate::config::error_kind::Severity;
use crate::config::finder::ConfigFinder;
use crate::error::codeclimate::CodeClimateIssues;
use crate::error::error::Error;
use crate::error::error::print_error_counts;
use crate::error::legacy::LegacyError;
Expand Down Expand Up @@ -397,6 +398,7 @@ fn write_errors_to_file(
OutputFormat::FullText => write_error_text_to_file(path, relative_to, errors, true),
OutputFormat::Json => write_error_json_to_file(path, relative_to, errors),
OutputFormat::Github => write_error_github_to_file(path, errors),
OutputFormat::Codeclimate => write_error_codeclimate_to_file(path, relative_to, errors),
OutputFormat::OmitErrors => Ok(()),
}
}
Expand All @@ -411,6 +413,7 @@ fn write_errors_to_console(
OutputFormat::FullText => write_error_text_to_console(relative_to, errors, true),
OutputFormat::Json => write_error_json_to_console(relative_to, errors),
OutputFormat::Github => write_error_github_to_console(errors),
OutputFormat::Codeclimate => write_error_codeclimate_to_console(relative_to, errors),
OutputFormat::OmitErrors => Ok(()),
}
}
Expand Down Expand Up @@ -550,6 +553,44 @@ fn escape_workflow_property(value: &str) -> String {
utf8_percent_encode(value, WORKFLOW_PROPERTY_ENCODE_SET).to_string()
}

fn write_error_codeclimate(
writer: &mut impl Write,
relative_to: &Path,
errors: &[Error],
) -> anyhow::Result<()> {
let issues = CodeClimateIssues::from_errors(relative_to, errors);
serde_json::to_writer_pretty(writer, &issues)?;
Ok(())
}

fn buffered_write_error_codeclimate(
writer: impl Write,
relative_to: &Path,
errors: &[Error],
) -> anyhow::Result<()> {
let mut writer = BufWriter::new(writer);
write_error_codeclimate(&mut writer, relative_to, errors)?;
writer.flush()?;
Ok(())
}

fn write_error_codeclimate_to_file(
path: &Path,
relative_to: &Path,
errors: &[Error],
) -> anyhow::Result<()> {
fn f(path: &Path, relative_to: &Path, errors: &[Error]) -> anyhow::Result<()> {
let file = File::create(path)?;
buffered_write_error_codeclimate(file, relative_to, errors)
}
f(path, relative_to, errors)
.with_context(|| format!("while writing CodeClimate issues to `{}`", path.display()))
}

fn write_error_codeclimate_to_console(relative_to: &Path, errors: &[Error]) -> anyhow::Result<()> {
buffered_write_error_codeclimate(stdout(), relative_to, errors)
}

/// A data structure to facilitate the creation of handles for all the files we want to check.
pub struct Handles {
/// A mapping from a file to all other information needed to create a `Handle`.
Expand Down
119 changes: 119 additions & 0 deletions pyrefly/lib/error/codeclimate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

use std::hash::DefaultHasher;
use std::hash::Hash as _;
use std::hash::Hasher as _;
use std::path::Path;

use pyrefly_config::error_kind::Severity;
use pyrefly_util::prelude::SliceExt;
use serde::Deserialize;
use serde::Serialize;

use crate::error::error::Error;

pub(crate) fn severity_to_str(severity: Severity) -> String {
match severity {
Severity::Ignore => "info".to_owned(), // This is the lowest valid severity level
Severity::Info => "info".to_owned(),
Severity::Warn => "minor".to_owned(),
Severity::Error => "major".to_owned(),
}
}

/// The structure for a CodeClimate issue
/// <https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#issues>.
///
/// Used to serialize errors for platforms that expect the CodeClimate format, like GitLab CI/CD's
/// Code Quality report artifact <https://docs.gitlab.com/ci/testing/code_quality>.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct CodeClimateIssue {
#[serde(rename = "type")]
issue_type: String,
check_name: String,
description: String,
#[serde(skip_serializing_if = "Option::is_none")]
content: Option<CodeClimateIssueContent>,
categories: Vec<String>,
location: CodeClimateIssueLocation,
severity: String,
fingerprint: String,
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
struct CodeClimateIssueContent {
body: String,
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
struct CodeClimateIssueLocation {
path: String,
positions: CodeClimateIssuePositions,
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
struct CodeClimateIssuePositions {
begin: CodeClimateIssuePosition,
end: CodeClimateIssuePosition,
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
struct CodeClimateIssuePosition {
line: u32,
column: u32,
}

impl CodeClimateIssue {
pub fn from_error(relative_to: &Path, error: &Error) -> Self {
let error_range = error.display_range();
let error_path = error.path().as_path();

let mut hasher = DefaultHasher::new();
error.hash(&mut hasher);
let fingerprint = format!("{:x}", hasher.finish());

Self {
issue_type: "issue".to_owned(),
check_name: format!("Pyrefly/{}", error.error_kind()),
description: error.msg_header().to_owned(),
content: error.msg_details().map(|details| CodeClimateIssueContent {
body: details.to_owned(),
}),
categories: vec!["Bug Risk".to_owned()],
location: CodeClimateIssueLocation {
path: error_path
.strip_prefix(relative_to)
.unwrap_or(error_path)
.to_string_lossy()
.into_owned(),
positions: CodeClimateIssuePositions {
begin: CodeClimateIssuePosition {
line: error_range.start.line_within_cell().get(),
column: error_range.start.column().get(),
},
end: CodeClimateIssuePosition {
line: error_range.end.line_within_cell().get(),
column: error_range.end.column().get(),
},
},
},
severity: severity_to_str(error.severity()),
fingerprint,
}
}
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(transparent)]
pub struct CodeClimateIssues(Vec<CodeClimateIssue>);

impl CodeClimateIssues {
pub fn from_errors(relative_to: &Path, errors: &[Error]) -> Self {
Self(errors.map(|e| CodeClimateIssue::from_error(relative_to, e)))
}
}
1 change: 1 addition & 0 deletions pyrefly/lib/error/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

pub mod baseline;
pub mod codeclimate;
pub mod collector;
pub mod context;
pub mod display;
Expand Down
32 changes: 32 additions & 0 deletions test/snippet.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,38 @@ $ $PYREFLY snippet "x: int = 'hello'" --output-format=json
[1]
```

## Snippet with CodeClimate output format

```scrut
$ $PYREFLY snippet "x: int = 'hello'" --output-format=codeclimate
[
{
"type": "issue",
"check_name": "Pyrefly/BadAssignment",
"description": "`Literal['hello']` is not assignable to `int`",
"categories": [
"Bug Risk"
],
"location": {
"path": "snippet",
"positions": {
"begin": {
"line": 1,
"column": 10
},
"end": {
"line": 1,
"column": 17
}
}
},
"severity": "major",
"fingerprint": "*" (glob)
}
] (no-eol)
[1]
```

## Snippet with config file

```scrut {output_stream: stderr}
Expand Down
2 changes: 1 addition & 1 deletion website/docs/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ min-severity = "warn"
Default format for `pyrefly check` error output when `--output-format` is not
set on the CLI.

- Type: `"min-text" | "full-text" | "json" | "github" | "omit-errors"`
- Type: `"min-text" | "full-text" | "json" | "github" | "codeclimate" | "omit-errors"`
- Default: `full-text`
- Flag equivalent: `--output-format`
- Notes:
Expand Down
Loading