Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
057414ddd4 | ||
|
|
ca94e9aa26 | ||
|
|
797b5bd261 | ||
|
|
a64f62f439 | ||
|
|
058ee8e6bf | ||
|
|
39fc1f0c1b | ||
|
|
34842b4c4b | ||
|
|
dfa6fa8f83 | ||
|
|
6131c819ed | ||
|
|
79ba420faa | ||
|
|
d16ba890ae | ||
|
|
6b6851bf1f | ||
|
|
056718ce75 | ||
|
|
4521fdf021 | ||
|
|
8e479628f2 | ||
|
|
2a11c4b1f1 | ||
|
|
a8cde5a936 |
54
.github/workflows/ci.yaml
vendored
54
.github/workflows/ci.yaml
vendored
@@ -121,31 +121,35 @@ jobs:
|
||||
- run: cargo test --all
|
||||
- run: cargo test --package ruff --test black_compatibility_test -- --ignored
|
||||
|
||||
wasm-pack-test:
|
||||
name: "wasm-pack test"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: nightly-2022-11-01
|
||||
override: true
|
||||
- uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-cargo
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
${{ runner.os }}-build-
|
||||
${{ runner.os }}-
|
||||
- uses: jetli/wasm-pack-action@v0.4.0
|
||||
- uses: jetli/wasm-bindgen-action@v0.2.0
|
||||
- run: wasm-pack test --node
|
||||
# TODO(charlie): Re-enable the `wasm-pack` tests.
|
||||
# See: https://github.com/charliermarsh/ruff/issues/1425
|
||||
# wasm-pack-test:
|
||||
# name: "wasm-pack test"
|
||||
# runs-on: ubuntu-latest
|
||||
# env:
|
||||
# WASM_BINDGEN_TEST_TIMEOUT: 60
|
||||
# steps:
|
||||
# - uses: actions/checkout@v3
|
||||
# - uses: actions-rs/toolchain@v1
|
||||
# with:
|
||||
# profile: minimal
|
||||
# toolchain: nightly-2022-11-01
|
||||
# override: true
|
||||
# - uses: actions/cache@v3
|
||||
# env:
|
||||
# cache-name: cache-cargo
|
||||
# with:
|
||||
# path: |
|
||||
# ~/.cargo/registry
|
||||
# ~/.cargo/git
|
||||
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
# restore-keys: |
|
||||
# ${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
# ${{ runner.os }}-build-
|
||||
# ${{ runner.os }}-
|
||||
# - uses: jetli/wasm-pack-action@v0.4.0
|
||||
# - uses: jetli/wasm-bindgen-action@v0.2.0
|
||||
# - run: wasm-pack test --node
|
||||
|
||||
maturin-build:
|
||||
name: "maturin build"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.0.198
|
||||
rev: v0.0.199
|
||||
hooks:
|
||||
- id: ruff
|
||||
|
||||
|
||||
9
Cargo.lock
generated
9
Cargo.lock
generated
@@ -750,7 +750,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.198-dev.0"
|
||||
version = "0.0.199-dev.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.0.32",
|
||||
@@ -1878,7 +1878,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.198"
|
||||
version = "0.0.199"
|
||||
dependencies = [
|
||||
"annotate-snippets 0.9.1",
|
||||
"anyhow",
|
||||
@@ -1930,6 +1930,7 @@ dependencies = [
|
||||
"serde-wasm-bindgen",
|
||||
"serde_json",
|
||||
"shellexpand",
|
||||
"similar",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"test-case",
|
||||
@@ -1945,7 +1946,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_dev"
|
||||
version = "0.0.198"
|
||||
version = "0.0.199"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.0.32",
|
||||
@@ -1966,7 +1967,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_macros"
|
||||
version = "0.0.198"
|
||||
version = "0.0.199"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
11
Cargo.toml
11
Cargo.toml
@@ -6,9 +6,15 @@ members = [
|
||||
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.198"
|
||||
version = "0.0.199"
|
||||
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.65.0"
|
||||
documentation = "https://github.com/charliermarsh/ruff"
|
||||
homepage = "https://github.com/charliermarsh/ruff"
|
||||
repository = "https://github.com/charliermarsh/ruff"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
||||
[lib]
|
||||
name = "ruff"
|
||||
@@ -45,7 +51,7 @@ path-absolutize = { version = "3.0.14", features = ["once_cell_cache", "use_unix
|
||||
quick-junit = { version = "0.3.2" }
|
||||
regex = { version = "1.6.0" }
|
||||
ropey = { version = "1.5.0", features = ["cr_lines", "simd"], default-features = false }
|
||||
ruff_macros = { version = "0.0.198", path = "ruff_macros" }
|
||||
ruff_macros = { version = "0.0.199", path = "ruff_macros" }
|
||||
rustc-hash = { version = "1.1.0" }
|
||||
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "68d26955b3e24198a150315e7959719b03709dee" }
|
||||
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "68d26955b3e24198a150315e7959719b03709dee" }
|
||||
@@ -55,6 +61,7 @@ semver = { version = "1.0.16" }
|
||||
serde = { version = "1.0.147", features = ["derive"] }
|
||||
serde_json = { version = "1.0.87" }
|
||||
shellexpand = { version = "3.0.0" }
|
||||
similar = { version = "2.2.1" }
|
||||
strum = { version = "0.24.1", features = ["strum_macros"] }
|
||||
strum_macros = { version = "0.24.3" }
|
||||
textwrap = { version = "0.16.0" }
|
||||
|
||||
13
README.md
13
README.md
@@ -167,7 +167,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
|
||||
```yaml
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: 'v0.0.198'
|
||||
rev: 'v0.0.199'
|
||||
hooks:
|
||||
- id: ruff
|
||||
# Respect `exclude` and `extend-exclude` settings.
|
||||
@@ -323,6 +323,8 @@ Options:
|
||||
Attempt to automatically fix lint errors
|
||||
--fix-only
|
||||
Fix any fixable lint errors, but don't report on leftover violations. Implies `--fix`
|
||||
--diff
|
||||
Avoid writing any fixed files back; instead, output a diff for each changed file to stdout
|
||||
-n, --no-cache
|
||||
Disable cache reads
|
||||
--select <SELECT>
|
||||
@@ -344,7 +346,7 @@ Options:
|
||||
--per-file-ignores <PER_FILE_IGNORES>
|
||||
List of mappings from file pattern to code to exclude
|
||||
--format <FORMAT>
|
||||
Output serialization format for error messages [possible values: text, json, junit, grouped, github]
|
||||
Output serialization format for error messages [possible values: text, json, junit, grouped, github, gitlab]
|
||||
--show-source
|
||||
Show violations with source code
|
||||
--respect-gitignore
|
||||
@@ -671,6 +673,8 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/3.2.0/) on PyPI.
|
||||
| UP019 | TypingTextStrAlias | `typing.Text` is deprecated, use `str` | 🛠 |
|
||||
| UP020 | OpenAlias | Use builtin `open` | 🛠 |
|
||||
| UP021 | ReplaceUniversalNewlines | `universal_newlines` is deprecated, use `text` | 🛠 |
|
||||
| UP022 | ReplaceStdoutStderr | Sending stdout and stderr to pipe is deprecated, use `capture_output` | 🛠 |
|
||||
| UP023 | RewriteCElementTree | `cElementTree` is deprecated, use `ElementTree` | 🛠 |
|
||||
|
||||
### pep8-naming (N)
|
||||
|
||||
@@ -1929,8 +1933,9 @@ force-exclude = true
|
||||
|
||||
The style in which violation messages should be formatted: `"text"`
|
||||
(default), `"grouped"` (group messages by file), `"json"`
|
||||
(machine-readable), `"junit"` (machine-readable XML), or `"github"`
|
||||
(GitHub Actions annotations).
|
||||
(machine-readable), `"junit"` (machine-readable XML), `"github"`
|
||||
(GitHub Actions annotations) or `"gitlab"`
|
||||
(GitLab CI code quality report).
|
||||
|
||||
**Default value**: `"text"`
|
||||
|
||||
|
||||
4
flake8_to_ruff/Cargo.lock
generated
4
flake8_to_ruff/Cargo.lock
generated
@@ -771,7 +771,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flake8_to_ruff"
|
||||
version = "0.0.198"
|
||||
version = "0.0.199"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -1975,7 +1975,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.198"
|
||||
version = "0.0.199"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.198-dev.0"
|
||||
version = "0.0.199-dev.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
42
resources/test/fixtures/pyupgrade/UP022.py
vendored
Normal file
42
resources/test/fixtures/pyupgrade/UP022.py
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
from subprocess import run
|
||||
import subprocess
|
||||
|
||||
output = run(["foo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
output = subprocess.run(["foo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
output = subprocess.run(stdout=subprocess.PIPE, args=["foo"], stderr=subprocess.PIPE)
|
||||
|
||||
output = subprocess.run(
|
||||
["foo"], stdout=subprocess.PIPE, check=True, stderr=subprocess.PIPE
|
||||
)
|
||||
|
||||
output = subprocess.run(
|
||||
["foo"], stderr=subprocess.PIPE, check=True, stdout=subprocess.PIPE
|
||||
)
|
||||
|
||||
output = subprocess.run(
|
||||
["foo"],
|
||||
stdout=subprocess.PIPE,
|
||||
check=True,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
close_fds=True,
|
||||
)
|
||||
|
||||
if output:
|
||||
output = subprocess.run(
|
||||
["foo"],
|
||||
stdout=subprocess.PIPE,
|
||||
check=True,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
# Examples that should NOT trigger the rule
|
||||
from foo import PIPE
|
||||
subprocess.run(["foo"], stdout=PIPE, stderr=PIPE)
|
||||
run(["foo"], stdout=None, stderr=PIPE)
|
||||
31
resources/test/fixtures/pyupgrade/UP023.py
vendored
Normal file
31
resources/test/fixtures/pyupgrade/UP023.py
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# These two imports have something after cElementTree, so they should be fixed.
|
||||
from xml.etree.cElementTree import XML, Element, SubElement
|
||||
import xml.etree.cElementTree as ET
|
||||
|
||||
# Weird spacing should not cause issues.
|
||||
from xml.etree.cElementTree import XML
|
||||
import xml.etree.cElementTree as ET
|
||||
|
||||
# Multi line imports should also work fine.
|
||||
from xml.etree.cElementTree import (
|
||||
XML,
|
||||
Element,
|
||||
SubElement,
|
||||
)
|
||||
if True:
|
||||
import xml.etree.cElementTree as ET
|
||||
from xml.etree import cElementTree as CET
|
||||
|
||||
from xml.etree import cElementTree as ET
|
||||
|
||||
import contextlib, xml.etree.cElementTree as ET
|
||||
|
||||
# This should fix the second, but not the first invocation.
|
||||
import xml.etree.cElementTree, xml.etree.cElementTree as ET
|
||||
|
||||
# The below items should NOT be changed.
|
||||
import xml.etree.cElementTree
|
||||
|
||||
from .xml.etree.cElementTree import XML
|
||||
|
||||
from xml.etree import cElementTree
|
||||
2
resources/test/fixtures/ruff/RUF004.py
vendored
2
resources/test/fixtures/ruff/RUF004.py
vendored
@@ -5,9 +5,11 @@ def f(*args, **kwargs):
|
||||
a = (1, 2)
|
||||
b = (3, 4)
|
||||
c = (5, 6)
|
||||
d = (7, 8)
|
||||
|
||||
f(a, b)
|
||||
f(a, kw=b)
|
||||
f(*a, kw=b)
|
||||
f(kw=a, *b)
|
||||
f(kw=a, *b, *c)
|
||||
f(*a, kw=b, *c, kw1=d)
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
}
|
||||
},
|
||||
"flake8-annotations": {
|
||||
"description": "Plugins Options for the `flake8-annotations` plugin.",
|
||||
"description": "Options for the `flake8-annotations` plugin.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Flake8AnnotationsOptions"
|
||||
@@ -195,7 +195,7 @@
|
||||
]
|
||||
},
|
||||
"format": {
|
||||
"description": "The style in which violation messages should be formatted: `\"text\"` (default), `\"grouped\"` (group messages by file), `\"json\"` (machine-readable), `\"junit\"` (machine-readable XML), or `\"github\"` (GitHub Actions annotations).",
|
||||
"description": "The style in which violation messages should be formatted: `\"text\"` (default), `\"grouped\"` (group messages by file), `\"json\"` (machine-readable), `\"junit\"` (machine-readable XML), `\"github\"` (GitHub Actions annotations) or `\"gitlab\"` (GitLab CI code quality report).",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SerializationFormat"
|
||||
@@ -881,6 +881,8 @@
|
||||
"UP02",
|
||||
"UP020",
|
||||
"UP021",
|
||||
"UP022",
|
||||
"UP023",
|
||||
"W",
|
||||
"W2",
|
||||
"W29",
|
||||
@@ -1264,7 +1266,8 @@
|
||||
"json",
|
||||
"junit",
|
||||
"grouped",
|
||||
"github"
|
||||
"github",
|
||||
"gitlab"
|
||||
]
|
||||
},
|
||||
"Strictness": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_dev"
|
||||
version = "0.0.198"
|
||||
version = "0.0.199"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_macros"
|
||||
version = "0.0.198"
|
||||
version = "0.0.199"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::source_code_locator::SourceCodeLocator;
|
||||
pub enum Mode {
|
||||
Generate,
|
||||
Apply,
|
||||
Diff,
|
||||
None,
|
||||
}
|
||||
|
||||
|
||||
17
src/cache.rs
17
src/cache.rs
@@ -12,7 +12,6 @@ use once_cell::sync::Lazy;
|
||||
use path_absolutize::Absolutize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::autofix::fixer;
|
||||
use crate::message::Message;
|
||||
use crate::settings::{flags, Settings};
|
||||
|
||||
@@ -48,7 +47,7 @@ fn content_dir() -> &'static Path {
|
||||
Path::new("content")
|
||||
}
|
||||
|
||||
fn cache_key<P: AsRef<Path>>(path: P, settings: &Settings, autofix: fixer::Mode) -> u64 {
|
||||
fn cache_key<P: AsRef<Path>>(path: P, settings: &Settings, autofix: flags::Autofix) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
CARGO_PKG_VERSION.hash(&mut hasher);
|
||||
path.as_ref().absolutize().unwrap().hash(&mut hasher);
|
||||
@@ -93,13 +92,8 @@ pub fn get<P: AsRef<Path>>(
|
||||
path: P,
|
||||
metadata: &Metadata,
|
||||
settings: &Settings,
|
||||
autofix: fixer::Mode,
|
||||
cache: flags::Cache,
|
||||
autofix: flags::Autofix,
|
||||
) -> Option<Vec<Message>> {
|
||||
if matches!(cache, flags::Cache::Disabled) {
|
||||
return None;
|
||||
};
|
||||
|
||||
let encoded = read_sync(&settings.cache_dir, cache_key(path, settings, autofix)).ok()?;
|
||||
let (mtime, messages) = match bincode::deserialize::<CheckResult>(&encoded[..]) {
|
||||
Ok(CheckResult {
|
||||
@@ -122,14 +116,9 @@ pub fn set<P: AsRef<Path>>(
|
||||
path: P,
|
||||
metadata: &Metadata,
|
||||
settings: &Settings,
|
||||
autofix: fixer::Mode,
|
||||
autofix: flags::Autofix,
|
||||
messages: &[Message],
|
||||
cache: flags::Cache,
|
||||
) {
|
||||
if matches!(cache, flags::Cache::Disabled) {
|
||||
return;
|
||||
};
|
||||
|
||||
let check_result = CheckResultRef {
|
||||
metadata: &CacheMetadata {
|
||||
mtime: FileTime::from_last_modification_time(metadata).unix_seconds(),
|
||||
|
||||
@@ -650,6 +650,9 @@ where
|
||||
));
|
||||
}
|
||||
}
|
||||
if self.settings.enabled.contains(&CheckCode::UP023) {
|
||||
pyupgrade::plugins::replace_c_element_tree(self, stmt);
|
||||
}
|
||||
|
||||
for alias in names {
|
||||
if alias.node.name.contains('.') && alias.node.asname.is_none() {
|
||||
@@ -819,6 +822,9 @@ where
|
||||
} => {
|
||||
// Track `import from` statements, to ensure that we can correctly attribute
|
||||
// references like `from typing import Union`.
|
||||
if self.settings.enabled.contains(&CheckCode::UP023) {
|
||||
pyupgrade::plugins::replace_c_element_tree(self, stmt);
|
||||
}
|
||||
if level.map(|level| level == 0).unwrap_or(true) {
|
||||
if let Some(module) = module {
|
||||
self.from_imports
|
||||
@@ -1552,9 +1558,6 @@ where
|
||||
pyupgrade::plugins::use_pep585_annotation(self, expr, attr);
|
||||
}
|
||||
|
||||
if self.settings.enabled.contains(&CheckCode::UP019) {
|
||||
pyupgrade::plugins::typing_text_str_alias(self, expr);
|
||||
}
|
||||
if self.settings.enabled.contains(&CheckCode::UP016) {
|
||||
pyupgrade::plugins::remove_six_compat(self, expr);
|
||||
}
|
||||
@@ -1564,7 +1567,9 @@ where
|
||||
{
|
||||
pyupgrade::plugins::datetime_utc_alias(self, expr);
|
||||
}
|
||||
|
||||
if self.settings.enabled.contains(&CheckCode::UP019) {
|
||||
pyupgrade::plugins::typing_text_str_alias(self, expr);
|
||||
}
|
||||
if self.settings.enabled.contains(&CheckCode::YTT202) {
|
||||
flake8_2020::plugins::name_or_attribute(self, expr);
|
||||
}
|
||||
@@ -1674,6 +1679,9 @@ where
|
||||
if self.settings.enabled.contains(&CheckCode::UP021) {
|
||||
pyupgrade::plugins::replace_universal_newlines(self, expr, keywords);
|
||||
}
|
||||
if self.settings.enabled.contains(&CheckCode::UP022) {
|
||||
pyupgrade::plugins::replace_stdout_stderr(self, expr, keywords);
|
||||
}
|
||||
|
||||
// flake8-print
|
||||
if self.settings.enabled.contains(&CheckCode::T201)
|
||||
|
||||
@@ -229,6 +229,8 @@ pub enum CheckCode {
|
||||
UP019,
|
||||
UP020,
|
||||
UP021,
|
||||
UP022,
|
||||
UP023,
|
||||
// pydocstyle
|
||||
D100,
|
||||
D101,
|
||||
@@ -844,6 +846,8 @@ pub enum CheckKind {
|
||||
NativeLiterals,
|
||||
OpenAlias,
|
||||
ReplaceUniversalNewlines,
|
||||
ReplaceStdoutStderr,
|
||||
RewriteCElementTree,
|
||||
// pydocstyle
|
||||
BlankLineAfterLastSection(String),
|
||||
BlankLineAfterSection(String),
|
||||
@@ -1223,6 +1227,8 @@ impl CheckCode {
|
||||
CheckCode::UP019 => CheckKind::TypingTextStrAlias,
|
||||
CheckCode::UP020 => CheckKind::OpenAlias,
|
||||
CheckCode::UP021 => CheckKind::ReplaceUniversalNewlines,
|
||||
CheckCode::UP022 => CheckKind::ReplaceStdoutStderr,
|
||||
CheckCode::UP023 => CheckKind::RewriteCElementTree,
|
||||
// pydocstyle
|
||||
CheckCode::D100 => CheckKind::PublicModule,
|
||||
CheckCode::D101 => CheckKind::PublicClass,
|
||||
@@ -1647,6 +1653,8 @@ impl CheckCode {
|
||||
CheckCode::UP019 => CheckCategory::Pyupgrade,
|
||||
CheckCode::UP020 => CheckCategory::Pyupgrade,
|
||||
CheckCode::UP021 => CheckCategory::Pyupgrade,
|
||||
CheckCode::UP022 => CheckCategory::Pyupgrade,
|
||||
CheckCode::UP023 => CheckCategory::Pyupgrade,
|
||||
CheckCode::W292 => CheckCategory::Pycodestyle,
|
||||
CheckCode::W605 => CheckCategory::Pycodestyle,
|
||||
CheckCode::YTT101 => CheckCategory::Flake82020,
|
||||
@@ -1862,6 +1870,8 @@ impl CheckKind {
|
||||
CheckKind::TypingTextStrAlias => &CheckCode::UP019,
|
||||
CheckKind::OpenAlias => &CheckCode::UP020,
|
||||
CheckKind::ReplaceUniversalNewlines => &CheckCode::UP021,
|
||||
CheckKind::ReplaceStdoutStderr => &CheckCode::UP022,
|
||||
CheckKind::RewriteCElementTree => &CheckCode::UP023,
|
||||
// pydocstyle
|
||||
CheckKind::BlankLineAfterLastSection(..) => &CheckCode::D413,
|
||||
CheckKind::BlankLineAfterSection(..) => &CheckCode::D410,
|
||||
@@ -2595,6 +2605,12 @@ impl CheckKind {
|
||||
CheckKind::ReplaceUniversalNewlines => {
|
||||
"`universal_newlines` is deprecated, use `text`".to_string()
|
||||
}
|
||||
CheckKind::ReplaceStdoutStderr => {
|
||||
"Sending stdout and stderr to pipe is deprecated, use `capture_output`".to_string()
|
||||
}
|
||||
CheckKind::RewriteCElementTree => {
|
||||
"`cElementTree` is deprecated, use `ElementTree`".to_string()
|
||||
}
|
||||
CheckKind::ConvertNamedTupleFunctionalToClass(name) => {
|
||||
format!("Convert `{name}` from `NamedTuple` functional to class syntax")
|
||||
}
|
||||
@@ -3040,6 +3056,8 @@ impl CheckKind {
|
||||
| CheckKind::OpenAlias
|
||||
| CheckKind::NewLineAfterLastParagraph
|
||||
| CheckKind::ReplaceUniversalNewlines
|
||||
| CheckKind::ReplaceStdoutStderr
|
||||
| CheckKind::RewriteCElementTree
|
||||
| CheckKind::NewLineAfterSectionName(..)
|
||||
| CheckKind::NoBlankLineAfterFunction(..)
|
||||
| CheckKind::NoBlankLineBeforeClass(..)
|
||||
|
||||
@@ -532,6 +532,8 @@ pub enum CheckCodePrefix {
|
||||
UP02,
|
||||
UP020,
|
||||
UP021,
|
||||
UP022,
|
||||
UP023,
|
||||
W,
|
||||
W2,
|
||||
W29,
|
||||
@@ -759,6 +761,8 @@ impl CheckCodePrefix {
|
||||
CheckCode::UP019,
|
||||
CheckCode::UP020,
|
||||
CheckCode::UP021,
|
||||
CheckCode::UP022,
|
||||
CheckCode::UP023,
|
||||
CheckCode::D100,
|
||||
CheckCode::D101,
|
||||
CheckCode::D102,
|
||||
@@ -2415,6 +2419,8 @@ impl CheckCodePrefix {
|
||||
CheckCode::UP019,
|
||||
CheckCode::UP020,
|
||||
CheckCode::UP021,
|
||||
CheckCode::UP022,
|
||||
CheckCode::UP023,
|
||||
]
|
||||
}
|
||||
CheckCodePrefix::U0 => {
|
||||
@@ -2445,6 +2451,8 @@ impl CheckCodePrefix {
|
||||
CheckCode::UP019,
|
||||
CheckCode::UP020,
|
||||
CheckCode::UP021,
|
||||
CheckCode::UP022,
|
||||
CheckCode::UP023,
|
||||
]
|
||||
}
|
||||
CheckCodePrefix::U00 => {
|
||||
@@ -2659,6 +2667,8 @@ impl CheckCodePrefix {
|
||||
CheckCode::UP019,
|
||||
CheckCode::UP020,
|
||||
CheckCode::UP021,
|
||||
CheckCode::UP022,
|
||||
CheckCode::UP023,
|
||||
],
|
||||
CheckCodePrefix::UP0 => vec![
|
||||
CheckCode::UP001,
|
||||
@@ -2681,6 +2691,8 @@ impl CheckCodePrefix {
|
||||
CheckCode::UP019,
|
||||
CheckCode::UP020,
|
||||
CheckCode::UP021,
|
||||
CheckCode::UP022,
|
||||
CheckCode::UP023,
|
||||
],
|
||||
CheckCodePrefix::UP00 => vec![
|
||||
CheckCode::UP001,
|
||||
@@ -2722,9 +2734,16 @@ impl CheckCodePrefix {
|
||||
CheckCodePrefix::UP017 => vec![CheckCode::UP017],
|
||||
CheckCodePrefix::UP018 => vec![CheckCode::UP018],
|
||||
CheckCodePrefix::UP019 => vec![CheckCode::UP019],
|
||||
CheckCodePrefix::UP02 => vec![CheckCode::UP020, CheckCode::UP021],
|
||||
CheckCodePrefix::UP02 => vec![
|
||||
CheckCode::UP020,
|
||||
CheckCode::UP021,
|
||||
CheckCode::UP022,
|
||||
CheckCode::UP023,
|
||||
],
|
||||
CheckCodePrefix::UP020 => vec![CheckCode::UP020],
|
||||
CheckCodePrefix::UP021 => vec![CheckCode::UP021],
|
||||
CheckCodePrefix::UP022 => vec![CheckCode::UP022],
|
||||
CheckCodePrefix::UP023 => vec![CheckCode::UP023],
|
||||
CheckCodePrefix::W => vec![CheckCode::W292, CheckCode::W605],
|
||||
CheckCodePrefix::W2 => vec![CheckCode::W292],
|
||||
CheckCodePrefix::W29 => vec![CheckCode::W292],
|
||||
@@ -3288,6 +3307,8 @@ impl CheckCodePrefix {
|
||||
CheckCodePrefix::UP02 => SuffixLength::Two,
|
||||
CheckCodePrefix::UP020 => SuffixLength::Three,
|
||||
CheckCodePrefix::UP021 => SuffixLength::Three,
|
||||
CheckCodePrefix::UP022 => SuffixLength::Three,
|
||||
CheckCodePrefix::UP023 => SuffixLength::Three,
|
||||
CheckCodePrefix::W => SuffixLength::Zero,
|
||||
CheckCodePrefix::W2 => SuffixLength::One,
|
||||
CheckCodePrefix::W29 => SuffixLength::Two,
|
||||
|
||||
@@ -50,6 +50,10 @@ pub struct Cli {
|
||||
fix_only: bool,
|
||||
#[clap(long, overrides_with("fix_only"), hide = true)]
|
||||
no_fix_only: bool,
|
||||
/// Avoid writing any fixed files back; instead, output a diff for each
|
||||
/// changed file to stdout.
|
||||
#[arg(long)]
|
||||
pub diff: bool,
|
||||
/// Disable cache reads.
|
||||
#[arg(short, long)]
|
||||
pub no_cache: bool,
|
||||
@@ -154,6 +158,7 @@ impl Cli {
|
||||
add_noqa: self.add_noqa,
|
||||
autoformat: self.autoformat,
|
||||
config: self.config,
|
||||
diff: self.diff,
|
||||
exit_zero: self.exit_zero,
|
||||
explain: self.explain,
|
||||
files: self.files,
|
||||
@@ -213,6 +218,7 @@ pub struct Arguments {
|
||||
pub add_noqa: bool,
|
||||
pub autoformat: bool,
|
||||
pub config: Option<PathBuf>,
|
||||
pub diff: bool,
|
||||
pub exit_zero: bool,
|
||||
pub explain: Option<CheckCode>,
|
||||
pub files: Vec<PathBuf>,
|
||||
|
||||
@@ -332,6 +332,9 @@ pub fn explain(code: &CheckCode, format: &SerializationFormat) -> Result<()> {
|
||||
SerializationFormat::Github => {
|
||||
bail!("`--explain` does not support GitHub format")
|
||||
}
|
||||
SerializationFormat::Gitlab => {
|
||||
bail!("`--explain` does not support GitLab format")
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
204
src/linter.rs
204
src/linter.rs
@@ -5,8 +5,10 @@ use std::ops::AddAssign;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use colored::Colorize;
|
||||
use log::debug;
|
||||
use rustpython_parser::lexer::LexResult;
|
||||
use similar::TextDiff;
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::autofix::fixer;
|
||||
@@ -26,6 +28,9 @@ use crate::source_code_locator::SourceCodeLocator;
|
||||
use crate::source_code_style::SourceCodeStyleDetector;
|
||||
use crate::{cache, directives, fs, rustpython_helpers};
|
||||
|
||||
const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
|
||||
const CARGO_PKG_REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY");
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Diagnostics {
|
||||
pub messages: Vec<Message>,
|
||||
@@ -183,26 +188,47 @@ pub fn lint_path(
|
||||
// Validate the `Settings` and return any errors.
|
||||
settings.validate()?;
|
||||
|
||||
let metadata = path.metadata()?;
|
||||
|
||||
// Check the cache.
|
||||
if let Some(messages) = cache::get(path, &metadata, settings, autofix, cache) {
|
||||
debug!("Cache hit for: {}", path.to_string_lossy());
|
||||
return Ok(Diagnostics::new(messages));
|
||||
}
|
||||
let metadata = if matches!(cache, flags::Cache::Enabled) {
|
||||
let metadata = path.metadata()?;
|
||||
if let Some(messages) = cache::get(path, &metadata, settings, autofix.into()) {
|
||||
debug!("Cache hit for: {}", path.to_string_lossy());
|
||||
return Ok(Diagnostics::new(messages));
|
||||
}
|
||||
Some(metadata)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Read the file from disk.
|
||||
let contents = fs::read_file(path)?;
|
||||
|
||||
// Lint the file.
|
||||
let (contents, fixed, messages) = lint(contents, path, package, settings, autofix)?;
|
||||
let (messages, fixed) = if matches!(autofix, fixer::Mode::Apply | fixer::Mode::Diff) {
|
||||
let (transformed, fixed, messages) = lint_fix(&contents, path, package, settings)?;
|
||||
if fixed > 0 {
|
||||
if matches!(autofix, fixer::Mode::Apply) {
|
||||
write(path, transformed)?;
|
||||
} else if matches!(autofix, fixer::Mode::Diff) {
|
||||
let mut stdout = io::stdout().lock();
|
||||
TextDiff::from_lines(&contents, &transformed)
|
||||
.unified_diff()
|
||||
.header(&fs::relativize_path(path), &fs::relativize_path(path))
|
||||
.to_writer(&mut stdout)?;
|
||||
stdout.write_all(b"\n")?;
|
||||
stdout.flush()?;
|
||||
}
|
||||
}
|
||||
(messages, fixed)
|
||||
} else {
|
||||
let messages = lint_only(&contents, path, package, settings, autofix.into())?;
|
||||
let fixed = 0;
|
||||
(messages, fixed)
|
||||
};
|
||||
|
||||
// Re-populate the cache.
|
||||
cache::set(path, &metadata, settings, autofix, &messages, cache);
|
||||
|
||||
// If we applied any fixes, write the contents back to disk.
|
||||
if fixed > 0 {
|
||||
write(path, contents)?;
|
||||
if let Some(metadata) = metadata {
|
||||
cache::set(path, &metadata, settings, autofix.into(), &messages);
|
||||
}
|
||||
|
||||
Ok(Diagnostics { messages, fixed })
|
||||
@@ -286,40 +312,121 @@ pub fn autoformat_path(path: &Path, settings: &Settings) -> Result<()> {
|
||||
pub fn lint_stdin(
|
||||
path: Option<&Path>,
|
||||
package: Option<&Path>,
|
||||
stdin: &str,
|
||||
contents: &str,
|
||||
settings: &Settings,
|
||||
autofix: fixer::Mode,
|
||||
) -> Result<Diagnostics> {
|
||||
// Validate the `Settings` and return any errors.
|
||||
settings.validate()?;
|
||||
|
||||
// Read the file from disk.
|
||||
let contents = stdin.to_string();
|
||||
// Lint the inputs.
|
||||
let (messages, fixed) = if matches!(autofix, fixer::Mode::Apply | fixer::Mode::Diff) {
|
||||
let (transformed, fixed, messages) = lint_fix(
|
||||
contents,
|
||||
path.unwrap_or_else(|| Path::new("-")),
|
||||
package,
|
||||
settings,
|
||||
)?;
|
||||
|
||||
// Lint the file.
|
||||
let (contents, fixed, messages) = lint(
|
||||
contents,
|
||||
path.unwrap_or_else(|| Path::new("-")),
|
||||
package,
|
||||
settings,
|
||||
autofix,
|
||||
)?;
|
||||
if matches!(autofix, fixer::Mode::Apply) {
|
||||
// Write the contents to stdout, regardless of whether any errors were fixed.
|
||||
io::stdout().write_all(transformed.as_bytes())?;
|
||||
} else if matches!(autofix, fixer::Mode::Diff) {
|
||||
// But only write a diff if it's non-empty.
|
||||
if fixed > 0 {
|
||||
let text_diff = TextDiff::from_lines(contents, &transformed);
|
||||
let mut unified_diff = text_diff.unified_diff();
|
||||
if let Some(path) = path {
|
||||
unified_diff.header(&fs::relativize_path(path), &fs::relativize_path(path));
|
||||
}
|
||||
|
||||
// Write the fixed contents to stdout.
|
||||
if matches!(autofix, fixer::Mode::Apply) {
|
||||
io::stdout().write_all(contents.as_bytes())?;
|
||||
}
|
||||
let mut stdout = io::stdout().lock();
|
||||
unified_diff.to_writer(&mut stdout)?;
|
||||
stdout.write_all(b"\n")?;
|
||||
stdout.flush()?;
|
||||
}
|
||||
}
|
||||
|
||||
(messages, fixed)
|
||||
} else {
|
||||
let messages = lint_only(
|
||||
contents,
|
||||
path.unwrap_or_else(|| Path::new("-")),
|
||||
package,
|
||||
settings,
|
||||
autofix.into(),
|
||||
)?;
|
||||
let fixed = 0;
|
||||
(messages, fixed)
|
||||
};
|
||||
|
||||
Ok(Diagnostics { messages, fixed })
|
||||
}
|
||||
|
||||
fn lint(
|
||||
mut contents: String,
|
||||
/// Generate a list of `Check` violations (optionally including any autofix
|
||||
/// patches) from source code content.
|
||||
fn lint_only(
|
||||
contents: &str,
|
||||
path: &Path,
|
||||
package: Option<&Path>,
|
||||
settings: &Settings,
|
||||
autofix: flags::Autofix,
|
||||
) -> Result<Vec<Message>> {
|
||||
// Tokenize once.
|
||||
let tokens: Vec<LexResult> = rustpython_helpers::tokenize(contents);
|
||||
|
||||
// Map row and column locations to byte slices (lazily).
|
||||
let locator = SourceCodeLocator::new(contents);
|
||||
|
||||
// Detect the current code style (lazily).
|
||||
let stylist = SourceCodeStyleDetector::from_contents(contents, &locator);
|
||||
|
||||
// Extract the `# noqa` and `# isort: skip` directives from the source.
|
||||
let directives = directives::extract_directives(
|
||||
&tokens,
|
||||
&locator,
|
||||
directives::Flags::from_settings(settings),
|
||||
);
|
||||
|
||||
// Generate checks.
|
||||
let checks = check_path(
|
||||
path,
|
||||
package,
|
||||
contents,
|
||||
tokens,
|
||||
&locator,
|
||||
&stylist,
|
||||
&directives,
|
||||
settings,
|
||||
autofix,
|
||||
flags::Noqa::Enabled,
|
||||
)?;
|
||||
|
||||
// Convert from checks to messages.
|
||||
let path_lossy = path.to_string_lossy();
|
||||
Ok(checks
|
||||
.into_iter()
|
||||
.map(|check| {
|
||||
let source = if settings.show_source {
|
||||
Some(Source::from_check(&check, &locator))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Message::from_check(check, path_lossy.to_string(), source)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Generate a list of `Check` violations from source code content, iteratively
|
||||
/// autofixing any violations until stable.
|
||||
fn lint_fix(
|
||||
contents: &str,
|
||||
path: &Path,
|
||||
package: Option<&Path>,
|
||||
settings: &Settings,
|
||||
autofix: fixer::Mode,
|
||||
) -> Result<(String, usize, Vec<Message>)> {
|
||||
let mut contents = contents.to_string();
|
||||
|
||||
// Track the number of fixed errors across iterations.
|
||||
let mut fixed = 0;
|
||||
|
||||
@@ -327,7 +434,7 @@ fn lint(
|
||||
let mut iterations = 0;
|
||||
|
||||
// Continuously autofix until the source code stabilizes.
|
||||
let messages = loop {
|
||||
loop {
|
||||
// Tokenize once.
|
||||
let tokens: Vec<LexResult> = rustpython_helpers::tokenize(&contents);
|
||||
|
||||
@@ -354,13 +461,13 @@ fn lint(
|
||||
&stylist,
|
||||
&directives,
|
||||
settings,
|
||||
autofix.into(),
|
||||
flags::Autofix::Enabled,
|
||||
flags::Noqa::Enabled,
|
||||
)?;
|
||||
|
||||
// Apply autofix.
|
||||
if matches!(autofix, fixer::Mode::Apply) && iterations < MAX_ITERATIONS {
|
||||
if let Some((fixed_contents, applied)) = fix_file(&checks, &locator) {
|
||||
if let Some((fixed_contents, applied)) = fix_file(&checks, &locator) {
|
||||
if iterations < MAX_ITERATIONS {
|
||||
// Count the number of fixed errors.
|
||||
fixed += applied;
|
||||
|
||||
@@ -373,11 +480,29 @@ fn lint(
|
||||
// Re-run the linter pass (by avoiding the break).
|
||||
continue;
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"
|
||||
{}: Failed to converge after {} iterations.
|
||||
|
||||
This likely indicates a bug in `{}`. If you could open an issue at:
|
||||
|
||||
{}/issues
|
||||
|
||||
quoting the contents of `{}`, along with the `pyproject.toml` settings and executed command, we'd \
|
||||
be very appreciative!
|
||||
",
|
||||
"warning".yellow().bold(),
|
||||
MAX_ITERATIONS,
|
||||
CARGO_PKG_NAME,
|
||||
CARGO_PKG_REPOSITORY,
|
||||
fs::relativize_path(path),
|
||||
);
|
||||
}
|
||||
|
||||
// Convert to messages.
|
||||
let filename = path.to_string_lossy().to_string();
|
||||
break checks
|
||||
let path_lossy = path.to_string_lossy();
|
||||
let messages = checks
|
||||
.into_iter()
|
||||
.map(|check| {
|
||||
let source = if settings.show_source {
|
||||
@@ -385,12 +510,11 @@ fn lint(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Message::from_check(check, filename.clone(), source)
|
||||
Message::from_check(check, path_lossy.to_string(), source)
|
||||
})
|
||||
.collect();
|
||||
};
|
||||
|
||||
Ok((contents, fixed, messages))
|
||||
return Ok((contents, fixed, messages));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -123,19 +123,6 @@ pub(crate) fn inner_main() -> Result<ExitCode> {
|
||||
(settings.fix, settings.fix_only, settings.format)
|
||||
}
|
||||
};
|
||||
let autofix = if fix || fix_only {
|
||||
fixer::Mode::Apply
|
||||
} else if matches!(format, SerializationFormat::Json) {
|
||||
fixer::Mode::Generate
|
||||
} else {
|
||||
fixer::Mode::None
|
||||
};
|
||||
let violations = if fix_only {
|
||||
Violations::Hide
|
||||
} else {
|
||||
Violations::Show
|
||||
};
|
||||
let cache = !cli.no_cache;
|
||||
|
||||
if let Some(code) = cli.explain {
|
||||
commands::explain(&code, &format)?;
|
||||
@@ -150,9 +137,35 @@ pub(crate) fn inner_main() -> Result<ExitCode> {
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
// Autofix rules are as follows:
|
||||
// - If `--fix` or `--fix-only` is set, always apply fixes to the filesystem (or
|
||||
// print them to stdout, if we're reading from stdin).
|
||||
// - Otherwise, if `--format json` is set, generate the fixes (so we print them
|
||||
// out as part of the JSON payload), but don't write them to disk.
|
||||
// - If `--diff` or `--fix-only` are set, don't print any violations (only
|
||||
// fixes).
|
||||
// TODO(charlie): Consider adding ESLint's `--fix-dry-run`, which would generate
|
||||
// but not apply fixes. That would allow us to avoid special-casing JSON
|
||||
// here.
|
||||
let autofix = if cli.diff {
|
||||
fixer::Mode::Diff
|
||||
} else if fix || fix_only {
|
||||
fixer::Mode::Apply
|
||||
} else if matches!(format, SerializationFormat::Json) {
|
||||
fixer::Mode::Generate
|
||||
} else {
|
||||
fixer::Mode::None
|
||||
};
|
||||
let violations = if cli.diff || fix_only {
|
||||
Violations::Hide
|
||||
} else {
|
||||
Violations::Show
|
||||
};
|
||||
let cache = !cli.no_cache;
|
||||
|
||||
let printer = Printer::new(&format, &log_level, &autofix, &violations);
|
||||
if cli.watch {
|
||||
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if !matches!(autofix, fixer::Mode::None) {
|
||||
eprintln!("Warning: --fix is not enabled in watch mode.");
|
||||
}
|
||||
if cli.add_noqa {
|
||||
@@ -251,7 +264,7 @@ pub(crate) fn inner_main() -> Result<ExitCode> {
|
||||
// Always try to print violations (the printer itself may suppress output),
|
||||
// unless we're writing fixes via stdin (in which case, the transformed
|
||||
// source code goes to stdout).
|
||||
if !(is_stdin && matches!(autofix, fixer::Mode::Apply)) {
|
||||
if !(is_stdin && matches!(autofix, fixer::Mode::Apply | fixer::Mode::Diff)) {
|
||||
printer.write_once(&diagnostics)?;
|
||||
}
|
||||
|
||||
@@ -261,8 +274,14 @@ pub(crate) fn inner_main() -> Result<ExitCode> {
|
||||
drop(updates::check_for_updates());
|
||||
}
|
||||
|
||||
if !diagnostics.messages.is_empty() && !cli.exit_zero && !fix_only {
|
||||
return Ok(ExitCode::FAILURE);
|
||||
if !cli.exit_zero {
|
||||
if cli.diff || fix_only {
|
||||
if diagnostics.fixed > 0 {
|
||||
return Ok(ExitCode::FAILURE);
|
||||
}
|
||||
} else if !diagnostics.messages.is_empty() {
|
||||
return Ok(ExitCode::FAILURE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use colored::Colorize;
|
||||
use itertools::iterate;
|
||||
use rustpython_parser::ast::Location;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::autofix::{fixer, Fix};
|
||||
use crate::checks::CheckCode;
|
||||
@@ -89,7 +90,11 @@ impl<'a> Printer<'a> {
|
||||
Violations::Hide => {
|
||||
let fixed = diagnostics.fixed;
|
||||
if fixed > 0 {
|
||||
println!("Fixed {fixed} error(s).");
|
||||
if matches!(self.autofix, fixer::Mode::Apply) {
|
||||
println!("Fixed {fixed} error(s).");
|
||||
} else if matches!(self.autofix, fixer::Mode::Diff) {
|
||||
println!("Would fix {fixed} error(s).");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,17 +139,8 @@ impl<'a> Printer<'a> {
|
||||
SerializationFormat::Junit => {
|
||||
use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite};
|
||||
|
||||
// Group by filename.
|
||||
let mut grouped_messages = BTreeMap::default();
|
||||
for message in &diagnostics.messages {
|
||||
grouped_messages
|
||||
.entry(&message.filename)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(message);
|
||||
}
|
||||
|
||||
let mut report = Report::new("ruff");
|
||||
for (filename, messages) in grouped_messages {
|
||||
for (filename, messages) in group_messages_by_filename(&diagnostics.messages) {
|
||||
let mut test_suite = TestSuite::new(filename);
|
||||
test_suite
|
||||
.extra
|
||||
@@ -183,16 +179,7 @@ impl<'a> Printer<'a> {
|
||||
self.post_text(diagnostics);
|
||||
}
|
||||
SerializationFormat::Grouped => {
|
||||
// Group by filename.
|
||||
let mut grouped_messages = BTreeMap::default();
|
||||
for message in &diagnostics.messages {
|
||||
grouped_messages
|
||||
.entry(&message.filename)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(message);
|
||||
}
|
||||
|
||||
for (filename, messages) in grouped_messages {
|
||||
for (filename, messages) in group_messages_by_filename(&diagnostics.messages) {
|
||||
// Compute the maximum number of digits in the row and column, for messages in
|
||||
// this file.
|
||||
let row_length = num_digits(
|
||||
@@ -239,6 +226,34 @@ impl<'a> Printer<'a> {
|
||||
);
|
||||
});
|
||||
}
|
||||
SerializationFormat::Gitlab => {
|
||||
// Generate JSON with errors in GitLab CI format
|
||||
// https://docs.gitlab.com/ee/ci/testing/code_quality.html#implementing-a-custom-tool
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(
|
||||
&diagnostics
|
||||
.messages
|
||||
.iter()
|
||||
.map(|message| {
|
||||
json!({
|
||||
"description": format!("({}) {}", message.kind.code(), message.kind.body()),
|
||||
"severity": "major",
|
||||
"fingerprint": message.kind.code(),
|
||||
"location": {
|
||||
"path": relativize_path(Path::new(&message.filename)),
|
||||
"lines": {
|
||||
"begin": message.location.row(),
|
||||
"end": message.end_location.row()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
.collect::<Vec<_>>()
|
||||
)?
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -275,6 +290,17 @@ impl<'a> Printer<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn group_messages_by_filename(messages: &Vec<Message>) -> BTreeMap<&String, Vec<&Message>> {
|
||||
let mut grouped_messages = BTreeMap::default();
|
||||
for message in messages {
|
||||
grouped_messages
|
||||
.entry(&message.filename)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(message);
|
||||
}
|
||||
grouped_messages
|
||||
}
|
||||
|
||||
fn num_digits(n: usize) -> usize {
|
||||
iterate(n, |&n| n / 10)
|
||||
.take_while(|&n| n > 0)
|
||||
|
||||
@@ -40,6 +40,8 @@ mod tests {
|
||||
#[test_case(CheckCode::UP018, Path::new("UP018.py"); "UP018")]
|
||||
#[test_case(CheckCode::UP019, Path::new("UP019.py"); "UP019")]
|
||||
#[test_case(CheckCode::UP021, Path::new("UP021.py"); "UP021")]
|
||||
#[test_case(CheckCode::UP022, Path::new("UP022.py"); "UP022")]
|
||||
#[test_case(CheckCode::UP023, Path::new("UP023.py"); "UP023")]
|
||||
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
|
||||
let mut checks = test_path(
|
||||
|
||||
@@ -6,7 +6,9 @@ pub use native_literals::native_literals;
|
||||
pub use open_alias::open_alias;
|
||||
pub use redundant_open_modes::redundant_open_modes;
|
||||
pub use remove_six_compat::remove_six_compat;
|
||||
pub use replace_stdout_stderr::replace_stdout_stderr;
|
||||
pub use replace_universal_newlines::replace_universal_newlines;
|
||||
pub use rewrite_c_element_tree::replace_c_element_tree;
|
||||
pub use super_call_with_parameters::super_call_with_parameters;
|
||||
pub use type_of_primitive::type_of_primitive;
|
||||
pub use typing_text_str_alias::typing_text_str_alias;
|
||||
@@ -26,7 +28,9 @@ mod native_literals;
|
||||
mod open_alias;
|
||||
mod redundant_open_modes;
|
||||
mod remove_six_compat;
|
||||
mod replace_stdout_stderr;
|
||||
mod replace_universal_newlines;
|
||||
mod rewrite_c_element_tree;
|
||||
mod super_call_with_parameters;
|
||||
mod type_of_primitive;
|
||||
mod typing_text_str_alias;
|
||||
|
||||
112
src/pyupgrade/plugins/replace_stdout_stderr.rs
Normal file
112
src/pyupgrade/plugins/replace_stdout_stderr.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use rustpython_ast::{Expr, Keyword};
|
||||
|
||||
use crate::ast::helpers::{find_keyword, match_module_member};
|
||||
use crate::ast::types::Range;
|
||||
use crate::ast::whitespace::indentation;
|
||||
use crate::autofix::Fix;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MiddleContent<'a> {
|
||||
contents: &'a str,
|
||||
multi_line: bool,
|
||||
}
|
||||
|
||||
/// Return the number of "dirty" characters.
|
||||
fn dirty_count(iter: impl Iterator<Item = char>) -> usize {
|
||||
let mut the_count = 0;
|
||||
for current_char in iter {
|
||||
if current_char == ' ' || current_char == ',' || current_char == '\n' {
|
||||
the_count += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
the_count
|
||||
}
|
||||
|
||||
/// Extract the `Middle` content between two arguments.
|
||||
fn extract_middle(contents: &str) -> Option<MiddleContent> {
|
||||
let multi_line = contents.contains('\n');
|
||||
let start_gap = dirty_count(contents.chars());
|
||||
if contents.len() == start_gap {
|
||||
return None;
|
||||
}
|
||||
let end_gap = dirty_count(contents.chars().rev());
|
||||
Some(MiddleContent {
|
||||
contents: &contents[start_gap..contents.len() - end_gap],
|
||||
multi_line,
|
||||
})
|
||||
}
|
||||
|
||||
/// UP022
|
||||
pub fn replace_stdout_stderr(checker: &mut Checker, expr: &Expr, kwargs: &[Keyword]) {
|
||||
if match_module_member(
|
||||
expr,
|
||||
"subprocess",
|
||||
"run",
|
||||
&checker.from_imports,
|
||||
&checker.import_aliases,
|
||||
) {
|
||||
// Find `stdout` and `stderr` kwargs.
|
||||
let Some(stdout) = find_keyword(kwargs, "stdout") else {
|
||||
return;
|
||||
};
|
||||
let Some(stderr) = find_keyword(kwargs, "stderr") else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Verify that they're both set to `subprocess.PIPE`.
|
||||
if !match_module_member(
|
||||
&stdout.node.value,
|
||||
"subprocess",
|
||||
"PIPE",
|
||||
&checker.from_imports,
|
||||
&checker.import_aliases,
|
||||
) || !match_module_member(
|
||||
&stderr.node.value,
|
||||
"subprocess",
|
||||
"PIPE",
|
||||
&checker.from_imports,
|
||||
&checker.import_aliases,
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut check = Check::new(CheckKind::ReplaceStdoutStderr, Range::from_located(expr));
|
||||
if checker.patch(check.kind.code()) {
|
||||
let first = if stdout.location < stderr.location {
|
||||
stdout
|
||||
} else {
|
||||
stderr
|
||||
};
|
||||
let last = if stdout.location > stderr.location {
|
||||
stdout
|
||||
} else {
|
||||
stderr
|
||||
};
|
||||
let mut contents = String::from("capture_output=True");
|
||||
if let Some(middle) = extract_middle(&checker.locator.slice_source_code_range(&Range {
|
||||
location: first.end_location.unwrap(),
|
||||
end_location: last.location,
|
||||
})) {
|
||||
if middle.multi_line {
|
||||
contents.push(',');
|
||||
contents.push('\n');
|
||||
contents.push_str(&indentation(checker, first));
|
||||
} else {
|
||||
contents.push(',');
|
||||
contents.push(' ');
|
||||
}
|
||||
contents.push_str(middle.contents);
|
||||
}
|
||||
check.amend(Fix::replacement(
|
||||
contents,
|
||||
first.location,
|
||||
last.end_location.unwrap(),
|
||||
));
|
||||
}
|
||||
checker.add_check(check);
|
||||
}
|
||||
}
|
||||
57
src/pyupgrade/plugins/rewrite_c_element_tree.rs
Normal file
57
src/pyupgrade/plugins/rewrite_c_element_tree.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use rustpython_ast::{Located, Stmt, StmtKind};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::autofix::Fix;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::checks::{Check, CheckKind};
|
||||
|
||||
fn add_check_for_node<T>(checker: &mut Checker, node: &Located<T>) {
|
||||
let mut check = Check::new(CheckKind::RewriteCElementTree, Range::from_located(node));
|
||||
if checker.patch(check.kind.code()) {
|
||||
let contents = checker
|
||||
.locator
|
||||
.slice_source_code_range(&Range::from_located(node));
|
||||
check.amend(Fix::replacement(
|
||||
contents.replacen("cElementTree", "ElementTree", 1),
|
||||
node.location,
|
||||
node.end_location.unwrap(),
|
||||
));
|
||||
}
|
||||
checker.add_check(check);
|
||||
}
|
||||
|
||||
/// UP023
|
||||
pub fn replace_c_element_tree(checker: &mut Checker, stmt: &Stmt) {
|
||||
match &stmt.node {
|
||||
StmtKind::Import { names } => {
|
||||
// Ex) `import xml.etree.cElementTree as ET`
|
||||
for name in names {
|
||||
if name.node.name == "xml.etree.cElementTree" && name.node.asname.is_some() {
|
||||
add_check_for_node(checker, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
StmtKind::ImportFrom {
|
||||
module,
|
||||
names,
|
||||
level,
|
||||
} => {
|
||||
if level.map_or(false, |level| level > 0) {
|
||||
// Ex) `import .xml.etree.cElementTree as ET`
|
||||
} else if let Some(module) = module {
|
||||
if module == "xml.etree.cElementTree" {
|
||||
// Ex) `from xml.etree.cElementTree import XML`
|
||||
add_check_for_node(checker, stmt);
|
||||
} else if module == "xml.etree" {
|
||||
// Ex) `from xml.etree import cElementTree as ET`
|
||||
for name in names {
|
||||
if name.node.name == "cElementTree" && name.node.asname.is_some() {
|
||||
add_check_for_node(checker, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => unreachable!("Expected StmtKind::Import | StmtKind::ImportFrom"),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
---
|
||||
source: src/pyupgrade/mod.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind: ReplaceStdoutStderr
|
||||
location:
|
||||
row: 4
|
||||
column: 9
|
||||
end_location:
|
||||
row: 4
|
||||
column: 69
|
||||
fix:
|
||||
content: capture_output=True
|
||||
location:
|
||||
row: 4
|
||||
column: 22
|
||||
end_location:
|
||||
row: 4
|
||||
column: 68
|
||||
- kind: ReplaceStdoutStderr
|
||||
location:
|
||||
row: 6
|
||||
column: 9
|
||||
end_location:
|
||||
row: 6
|
||||
column: 80
|
||||
fix:
|
||||
content: capture_output=True
|
||||
location:
|
||||
row: 6
|
||||
column: 33
|
||||
end_location:
|
||||
row: 6
|
||||
column: 79
|
||||
- kind: ReplaceStdoutStderr
|
||||
location:
|
||||
row: 8
|
||||
column: 9
|
||||
end_location:
|
||||
row: 8
|
||||
column: 86
|
||||
fix:
|
||||
content: "capture_output=True, args=[\"foo\"]"
|
||||
location:
|
||||
row: 8
|
||||
column: 24
|
||||
end_location:
|
||||
row: 8
|
||||
column: 85
|
||||
- kind: ReplaceStdoutStderr
|
||||
location:
|
||||
row: 10
|
||||
column: 9
|
||||
end_location:
|
||||
row: 12
|
||||
column: 1
|
||||
fix:
|
||||
content: "capture_output=True, check=True"
|
||||
location:
|
||||
row: 11
|
||||
column: 13
|
||||
end_location:
|
||||
row: 11
|
||||
column: 71
|
||||
- kind: ReplaceStdoutStderr
|
||||
location:
|
||||
row: 14
|
||||
column: 9
|
||||
end_location:
|
||||
row: 16
|
||||
column: 1
|
||||
fix:
|
||||
content: "capture_output=True, check=True"
|
||||
location:
|
||||
row: 15
|
||||
column: 13
|
||||
end_location:
|
||||
row: 15
|
||||
column: 71
|
||||
- kind: ReplaceStdoutStderr
|
||||
location:
|
||||
row: 18
|
||||
column: 9
|
||||
end_location:
|
||||
row: 26
|
||||
column: 1
|
||||
fix:
|
||||
content: "capture_output=True,\n check=True"
|
||||
location:
|
||||
row: 20
|
||||
column: 4
|
||||
end_location:
|
||||
row: 22
|
||||
column: 26
|
||||
- kind: ReplaceStdoutStderr
|
||||
location:
|
||||
row: 29
|
||||
column: 13
|
||||
end_location:
|
||||
row: 36
|
||||
column: 5
|
||||
fix:
|
||||
content: "capture_output=True,\n check=True"
|
||||
location:
|
||||
row: 31
|
||||
column: 8
|
||||
end_location:
|
||||
row: 33
|
||||
column: 30
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
---
|
||||
source: src/pyupgrade/mod.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind: RewriteCElementTree
|
||||
location:
|
||||
row: 2
|
||||
column: 0
|
||||
end_location:
|
||||
row: 2
|
||||
column: 59
|
||||
fix:
|
||||
content: "from xml.etree.ElementTree import XML, Element, SubElement"
|
||||
location:
|
||||
row: 2
|
||||
column: 0
|
||||
end_location:
|
||||
row: 2
|
||||
column: 59
|
||||
- kind: RewriteCElementTree
|
||||
location:
|
||||
row: 3
|
||||
column: 7
|
||||
end_location:
|
||||
row: 3
|
||||
column: 35
|
||||
fix:
|
||||
content: xml.etree.ElementTree as ET
|
||||
location:
|
||||
row: 3
|
||||
column: 7
|
||||
end_location:
|
||||
row: 3
|
||||
column: 35
|
||||
- kind: RewriteCElementTree
|
||||
location:
|
||||
row: 6
|
||||
column: 0
|
||||
end_location:
|
||||
row: 6
|
||||
column: 44
|
||||
fix:
|
||||
content: from xml.etree.ElementTree import XML
|
||||
location:
|
||||
row: 6
|
||||
column: 0
|
||||
end_location:
|
||||
row: 6
|
||||
column: 44
|
||||
- kind: RewriteCElementTree
|
||||
location:
|
||||
row: 7
|
||||
column: 10
|
||||
end_location:
|
||||
row: 7
|
||||
column: 49
|
||||
fix:
|
||||
content: xml.etree.ElementTree as ET
|
||||
location:
|
||||
row: 7
|
||||
column: 10
|
||||
end_location:
|
||||
row: 7
|
||||
column: 49
|
||||
- kind: RewriteCElementTree
|
||||
location:
|
||||
row: 10
|
||||
column: 0
|
||||
end_location:
|
||||
row: 14
|
||||
column: 1
|
||||
fix:
|
||||
content: "from xml.etree.ElementTree import (\n XML,\n Element,\n SubElement,\n)"
|
||||
location:
|
||||
row: 10
|
||||
column: 0
|
||||
end_location:
|
||||
row: 14
|
||||
column: 1
|
||||
- kind: RewriteCElementTree
|
||||
location:
|
||||
row: 16
|
||||
column: 11
|
||||
end_location:
|
||||
row: 16
|
||||
column: 39
|
||||
fix:
|
||||
content: xml.etree.ElementTree as ET
|
||||
location:
|
||||
row: 16
|
||||
column: 11
|
||||
end_location:
|
||||
row: 16
|
||||
column: 39
|
||||
- kind: RewriteCElementTree
|
||||
location:
|
||||
row: 17
|
||||
column: 26
|
||||
end_location:
|
||||
row: 17
|
||||
column: 45
|
||||
fix:
|
||||
content: ElementTree as CET
|
||||
location:
|
||||
row: 17
|
||||
column: 26
|
||||
end_location:
|
||||
row: 17
|
||||
column: 45
|
||||
- kind: RewriteCElementTree
|
||||
location:
|
||||
row: 19
|
||||
column: 22
|
||||
end_location:
|
||||
row: 19
|
||||
column: 40
|
||||
fix:
|
||||
content: ElementTree as ET
|
||||
location:
|
||||
row: 19
|
||||
column: 22
|
||||
end_location:
|
||||
row: 19
|
||||
column: 40
|
||||
- kind: RewriteCElementTree
|
||||
location:
|
||||
row: 21
|
||||
column: 19
|
||||
end_location:
|
||||
row: 21
|
||||
column: 47
|
||||
fix:
|
||||
content: xml.etree.ElementTree as ET
|
||||
location:
|
||||
row: 21
|
||||
column: 19
|
||||
end_location:
|
||||
row: 21
|
||||
column: 47
|
||||
- kind: RewriteCElementTree
|
||||
location:
|
||||
row: 24
|
||||
column: 31
|
||||
end_location:
|
||||
row: 24
|
||||
column: 59
|
||||
fix:
|
||||
content: xml.etree.ElementTree as ET
|
||||
location:
|
||||
row: 24
|
||||
column: 31
|
||||
end_location:
|
||||
row: 24
|
||||
column: 59
|
||||
|
||||
@@ -1686,7 +1686,7 @@ pub fn keyword_argument_before_star_argument(args: &[Expr], keywords: &[Keyword]
|
||||
let mut checks = vec![];
|
||||
if let Some(arg) = args
|
||||
.iter()
|
||||
.find(|arg| matches!(arg.node, ExprKind::Starred { .. }))
|
||||
.rfind(|arg| matches!(arg.node, ExprKind::Starred { .. }))
|
||||
{
|
||||
for keyword in keywords {
|
||||
if keyword.location < arg.location {
|
||||
|
||||
@@ -5,19 +5,28 @@ expression: checks
|
||||
- kind:
|
||||
KeywordArgumentBeforeStarArgument: kw
|
||||
location:
|
||||
row: 12
|
||||
row: 13
|
||||
column: 2
|
||||
end_location:
|
||||
row: 12
|
||||
row: 13
|
||||
column: 6
|
||||
fix: ~
|
||||
- kind:
|
||||
KeywordArgumentBeforeStarArgument: kw
|
||||
location:
|
||||
row: 13
|
||||
row: 14
|
||||
column: 2
|
||||
end_location:
|
||||
row: 13
|
||||
row: 14
|
||||
column: 6
|
||||
fix: ~
|
||||
- kind:
|
||||
KeywordArgumentBeforeStarArgument: kw
|
||||
location:
|
||||
row: 15
|
||||
column: 6
|
||||
end_location:
|
||||
row: 15
|
||||
column: 10
|
||||
fix: ~
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/// Simple flags used to drive program behavior.
|
||||
use crate::autofix::fixer;
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Debug, Copy, Clone, Hash)]
|
||||
pub enum Autofix {
|
||||
Enabled,
|
||||
Disabled,
|
||||
@@ -20,13 +20,13 @@ impl From<bool> for Autofix {
|
||||
impl From<fixer::Mode> for Autofix {
|
||||
fn from(value: fixer::Mode) -> Self {
|
||||
match value {
|
||||
fixer::Mode::Generate | fixer::Mode::Apply => Autofix::Enabled,
|
||||
fixer::Mode::Generate | fixer::Mode::Diff | fixer::Mode::Apply => Autofix::Enabled,
|
||||
fixer::Mode::None => Autofix::Disabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Debug, Copy, Clone, Hash)]
|
||||
pub enum Noqa {
|
||||
Enabled,
|
||||
Disabled,
|
||||
@@ -42,7 +42,7 @@ impl From<bool> for Noqa {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Debug, Copy, Clone, Hash)]
|
||||
pub enum Cache {
|
||||
Enabled,
|
||||
Disabled,
|
||||
|
||||
@@ -138,7 +138,7 @@ impl Settings {
|
||||
}]
|
||||
.into_iter(),
|
||||
),
|
||||
format: config.format.unwrap_or(SerializationFormat::Text),
|
||||
format: config.format.unwrap_or_default(),
|
||||
force_exclude: config.force_exclude.unwrap_or(false),
|
||||
ignore_init_module_imports: config.ignore_init_module_imports.unwrap_or_default(),
|
||||
line_length: config.line_length.unwrap_or(88),
|
||||
|
||||
@@ -156,8 +156,9 @@ pub struct Options {
|
||||
)]
|
||||
/// The style in which violation messages should be formatted: `"text"`
|
||||
/// (default), `"grouped"` (group messages by file), `"json"`
|
||||
/// (machine-readable), `"junit"` (machine-readable XML), or `"github"`
|
||||
/// (GitHub Actions annotations).
|
||||
/// (machine-readable), `"junit"` (machine-readable XML), `"github"`
|
||||
/// (GitHub Actions annotations) or `"gitlab"`
|
||||
/// (GitLab CI code quality report).
|
||||
pub format: Option<SerializationFormat>,
|
||||
#[option(
|
||||
default = r#"false"#,
|
||||
@@ -338,7 +339,6 @@ pub struct Options {
|
||||
/// This setting will override even the `RUFF_CACHE_DIR` environment
|
||||
/// variable, if set.
|
||||
pub cache_dir: Option<String>,
|
||||
/// Plugins
|
||||
#[option_group]
|
||||
/// Options for the `flake8-annotations` plugin.
|
||||
pub flake8_annotations: Option<flake8_annotations::settings::Options>,
|
||||
|
||||
@@ -154,6 +154,7 @@ pub enum SerializationFormat {
|
||||
Junit,
|
||||
Grouped,
|
||||
Github,
|
||||
Gitlab,
|
||||
}
|
||||
|
||||
impl Default for SerializationFormat {
|
||||
@@ -163,6 +164,12 @@ impl Default for SerializationFormat {
|
||||
return Self::Github;
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(gitlab_ci) = env::var("GITLAB_CI") {
|
||||
if gitlab_ci == "true" {
|
||||
return Self::Gitlab;
|
||||
}
|
||||
}
|
||||
Self::Text
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user