Skip to content
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

Add fixers #561

Open
wants to merge 70 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
e0514c4
Add machine applicable diagnostic
chriscerie Oct 14, 2023
d617ee6
Test with pretty_assertions
chriscerie Oct 14, 2023
6684025
Remove autoformatted change
chriscerie Oct 14, 2023
3679fb0
Use if let
chriscerie Oct 14, 2023
bd9f42b
Fix unused variable with underscore
chriscerie Oct 14, 2023
5b31d46
Change fixed_code to option and fix self unused variable fix
chriscerie Oct 14, 2023
5451778
Fix unused_variable self cases where there is a whitespace
chriscerie Oct 14, 2023
cf88f73
Skip fix with unused self in method call
chriscerie Oct 14, 2023
7161458
Skip fixing unused variable when they have references
chriscerie Oct 14, 2023
679cd8c
Add fix option
chriscerie Oct 14, 2023
0560a86
Abort fix with changes in working directory
chriscerie Oct 14, 2023
5417142
Handle conflicting fixes
chriscerie Oct 14, 2023
c49e080
Fix unused variable false negative with fixer
chriscerie Oct 14, 2023
63ad3eb
Fix clippy
chriscerie Oct 14, 2023
1c519dc
Add compare_nan fixer
chriscerie Oct 14, 2023
47afa03
Add deprecated fixer
chriscerie Oct 14, 2023
2b90c54
Add fixer to paranthese lint
chriscerie Oct 15, 2023
cbf3a3e
Fix diff for parenthese
chriscerie Oct 15, 2023
ff5d606
Fix test
chriscerie Oct 15, 2023
c7d371b
Fix iterations
chriscerie Oct 15, 2023
0addbc8
Remove debugging print
chriscerie Oct 15, 2023
5f3a84b
Pretty print
chriscerie Oct 15, 2023
26c3c76
Update docs
chriscerie Oct 15, 2023
31bf18e
Update docs
chriscerie Oct 15, 2023
e145e82
Add vscode extension support
chriscerie Oct 15, 2023
c8df081
Remove comment
chriscerie Oct 15, 2023
5a52738
Add machine applicability
chriscerie Oct 15, 2023
c8d1919
Standardize try message for code suggestion
chriscerie Oct 15, 2023
4c10d3e
Remove diff code when there are no changes
chriscerie Oct 15, 2023
441679f
Update comment
chriscerie Oct 15, 2023
d2bc397
Add machine applicability to each diff line
chriscerie Oct 15, 2023
935dfbb
Bracket applicability
chriscerie Oct 15, 2023
3baa606
Add divide by zero fixer
chriscerie Oct 16, 2023
a94b9ac
Add unnecessary string escape fixer
chriscerie Oct 16, 2023
b395454
Add unscoped variables fixer
chriscerie Oct 16, 2023
c7e9156
Create better abstraction
chriscerie Oct 16, 2023
d5684b8
Remove unused variable
chriscerie Oct 16, 2023
ff082cf
Format ts
chriscerie Oct 16, 2023
a9ed88b
Move iteration logic to get_applied_suggestions_code
chriscerie Oct 18, 2023
ad168a3
Remove unnecessary file
chriscerie Oct 18, 2023
0987940
Use assert_str_eq
chriscerie Oct 18, 2023
0c6a30d
Change deprecated to maybeincorrect
chriscerie Oct 18, 2023
84f648b
Update deprecated
chriscerie Oct 18, 2023
efe4273
Add newline to expected diff
chriscerie Oct 18, 2023
7d1ae68
Remove newline
chriscerie Oct 18, 2023
22b2fee
Normalize diff string
chriscerie Oct 18, 2023
6749c77
Add unused self fix
chriscerie Oct 19, 2023
67b740a
Rename fixed_code to suggestion
chriscerie Oct 19, 2023
f6f0196
Update vscode diagnostics message
chriscerie Oct 19, 2023
0acb630
Move suggestion message to label
chriscerie Oct 19, 2023
a345f30
Change ` to better message
chriscerie Oct 19, 2023
0810cc6
Normalize assertion strings
chriscerie Oct 19, 2023
c10156b
Merge branch 'main' into fixes
chriscerie Oct 21, 2023
b3a29dc
Fix clippy error
chriscerie Oct 21, 2023
ba45535
Merge branch 'main' into fixes
chriscerie Oct 26, 2023
23aa26f
Test
chriscerie Nov 4, 2023
c23ba11
Test
chriscerie Nov 4, 2023
786de0b
Test
chriscerie Nov 4, 2023
4d26ab3
Test
chriscerie Nov 4, 2023
de9aeba
Merge branch 'main' into fixes
chriscerie Nov 8, 2023
a0d53df
Merge branch 'main' of https://github.com/Kampfkarren/selene
chriscerie Nov 9, 2023
6f3631b
Merge branch 'main' into fixes
chriscerie Nov 10, 2023
98176b5
Merge branch 'main' of https://github.com/Kampfkarren/selene
chriscerie Nov 11, 2023
6e4472c
Merge remote-tracking branch 'origin' into fixes
chriscerie Nov 11, 2023
6155eae
Update test
chriscerie Nov 11, 2023
feb4cf0
Remove file
chriscerie Nov 11, 2023
df80d2c
Remove change
chriscerie Nov 11, 2023
0055e2c
Flatten print format
chriscerie Nov 11, 2023
038e062
Fix pedantic clippy
chriscerie Nov 11, 2023
9538811
Add must use
chriscerie Nov 11, 2023
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Added `bit32.byteswap` to Luau standard library
- Added `buffer` library to Luau standard library
- Added `SharedTable` to Roblox standard library
- Added new [`--fix`](https://kampfkarren.github.io/selene/cli/usage.html#fix) flag, which will automatically apply lint suggestions.

### Changed
- Updated internal parser, which includes floor division (`//`), more correct parsing of string interpolation with double braces, and better parsing of `\z` escapes.
Expand Down
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 18 additions & 18 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
[workspace]
members = ["selene", "selene-lib"]
resolver = "2"
[workspace.package]
version = "0.25.0"
authors = ["Kampfkarren <kampfkarren@gmail.com>"]
edition = "2021"
homepage = "https://kampfkarren.github.io/selene/"
license = "MPL-2.0"
repository = "https://github.com/Kampfkarren/selene"
[workspace.dependencies]
full_moon = "0.19.0"
toml = "0.7.2"
# Do not update this without confirming profiling uses the same version of tracy-client as selene
profiling = "1.0.7"
[workspace]
members = ["selene", "selene-lib"]
resolver = "2"

[workspace.package]
version = "0.25.0"
authors = ["Kampfkarren <kampfkarren@gmail.com>"]
edition = "2021"
homepage = "https://kampfkarren.github.io/selene/"
license = "MPL-2.0"
repository = "https://github.com/Kampfkarren/selene"

[workspace.dependencies]
full_moon = "0.19.0"
toml = "0.7.2"

# Do not update this without confirming profiling uses the same version of tracy-client as selene
profiling = "1.0.7"
5 changes: 5 additions & 0 deletions docs/src/cli/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ USAGE:
FLAGS:
--allow-warnings Pass when only warnings occur
--no-exclude Ignore excludes defined in config
--fix Automatically fix applicable lint warnings
-h, --help Prints help information
-n, --no-summary Suppress summary information
-q, --quiet Display only the necessary information. Equivalent to --display-style="quiet"
Expand Down Expand Up @@ -76,6 +77,10 @@ Results:
0 parse errors
```

**--fix** *fix*

Automatically applies lint suggestions. Since this can be potentially destructive, it's not allowed when there are uncommitted changes. This safety mechanism can be ignored by including `--allow-dirty` to allow unstaged changes or `--allow-staged` to allow only staged changes.

**--num-threads** *num-threads*

Specifies the number of threads for selene to use. Defaults to however many cores your CPU has. If you type `selene --help`, you can see this number because it will show as the default for you.
Expand Down
1 change: 1 addition & 0 deletions selene-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ profiling.workspace = true
regex = "1.7.1"
serde = "1.0.152"
serde_yaml = "0.9.16"
similar = "2.3.0"
toml.workspace = true

[dev-dependencies]
Expand Down
4 changes: 2 additions & 2 deletions selene-lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,10 +229,10 @@ macro_rules! use_lints {
})
}

pub fn test_on(&self, ast: &Ast) -> Vec<CheckerDiagnostic> {
pub fn test_on(&self, ast: &Ast, code: &String) -> Vec<CheckerDiagnostic> {
let mut diagnostics = Vec::new();

let ast_context = AstContext::from_ast(ast);
let ast_context = AstContext::from_ast(ast, &code.to_string());

macro_rules! check_lint {
($name:ident) => {
Expand Down
8 changes: 7 additions & 1 deletion selene-lib/src/lint_filtering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::{
visit_nodes::{NodeVisitor, VisitorType},
},
lint_exists,
lints::{Diagnostic, Label, Severity},
lints::{Applicability, Diagnostic, Label, Severity},
CheckerDiagnostic, LintVariation,
};
use full_moon::{ast::Ast, node::Node, tokenizer::TokenType};
Expand Down Expand Up @@ -149,6 +149,8 @@ impl NodeVisitor for FilterVisitor {
trivia_start_position.bytes(),
trivia_end_position.bytes(),
)),
None,
Applicability::Unspecified,
))
}
}));
Expand Down Expand Up @@ -223,6 +225,8 @@ pub fn filter_diagnostics(
(first_code.0.bytes(), first_code.1.bytes()),
"global filter must be before this".to_owned(),
)],
None,
Applicability::Unspecified,
));

continue;
Expand All @@ -244,6 +248,8 @@ pub fn filter_diagnostics(
possibly_conflicting.comment_range,
"conflicts with this".to_owned(),
)],
None,
Applicability::Unspecified,
));
}
}
Expand Down
158 changes: 155 additions & 3 deletions selene-lib/src/lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,21 +89,66 @@ pub enum Severity {
Warning,
}

/// Indicates the confidence in the correctness of a suggestion
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Applicability {
/// The suggestion is definitely what the user intended, or maintains the exact meaning of the code
///
/// The suggestion is safe to be automatically applied
MachineApplicable,

/// The suggestion is probably what the user intended, but may not maintain the exact meaning of the code
///
/// The suggestion generates valid code when applied
MaybeIncorrect,

/// The suggestion is probably what the user intended, but may not maintain the exact meaning of the code
///
/// The suggestion does not generate valid code when applied
HasPlaceholders,

Unspecified,
}

#[derive(Debug)]
pub struct Diagnostic {
pub code: &'static str,
pub message: String,
pub notes: Vec<String>,
pub primary_label: Label,
pub secondary_labels: Vec<Label>,
pub suggestion: Option<String>,
pub applicability: Applicability,
}

impl Diagnostic {
pub fn new(code: &'static str, message: String, primary_label: Label) -> Self {
pub fn new(
code: &'static str,
message: String,
primary_label: Label,
suggestion: Option<String>,
applicability: Applicability,
) -> Self {
let mut label = primary_label;
if let Some(ref suggestion_str) = suggestion {
if label.message.is_none() {
label = Label::new_with_message(
label.range,
if suggestion_str.is_empty() {
"consider removing this".to_string()
} else {
format!("try: `{suggestion_str}`")
},
);
}
};

Self {
code,
message,
primary_label,
primary_label: label,
suggestion,
applicability,

notes: Vec::new(),
secondary_labels: Vec::new(),
Expand All @@ -116,13 +161,17 @@ impl Diagnostic {
primary_label: Label,
notes: Vec<String>,
secondary_labels: Vec<Label>,
suggestion: Option<String>,
applicability: Applicability,
) -> Self {
Self {
code,
message,
notes,
primary_label,
secondary_labels,
suggestion,
applicability,
}
}

Expand All @@ -147,9 +196,107 @@ impl Diagnostic {
}
}

#[must_use]
pub fn start_position(&self) -> u32 {
self.primary_label.range.0
}

#[must_use]
pub fn has_machine_applicable_fix(&self) -> bool {
self.suggestion.is_some() && self.applicability == Applicability::MachineApplicable
}

#[must_use]
pub fn has_maybe_incorrect_fix(&self) -> bool {
self.suggestion.is_some() && self.applicability == Applicability::MaybeIncorrect
}

/// After applying suggestions, calls `get_new_diagnostics` and reruns to ensure fixes didn't produce new errors
pub fn get_applied_suggestions_code<F>(
code: &str,
diagnostics: &[&Diagnostic],
get_new_diagnostics: F,
) -> String
where
F: Fn(&str) -> Vec<Diagnostic>,
{
let mut chosen_diagnostics = Self::get_independent_suggestions(diagnostics);
let mut owned_diagnostics_ref;
let mut owned_diagnostics;

let mut fixed_code = code.to_string();

while !chosen_diagnostics.is_empty() {
let mut bytes_offset = 0;

for diagnostic in chosen_diagnostics {
if let Some(suggestion) = &diagnostic.suggestion {
let (start, end) = diagnostic.primary_label.range;

// This conversion can theoretically overflow, but it's tied to string length so the user
// would face memory constraints of such large strings long before this is an issue
let start_with_offset = start as isize + bytes_offset;
let end_with_offset = end as isize + bytes_offset;

fixed_code = format!(
"{}{}{}",
// Conversion is safe as this algorithm guarantees range + offset will never be negative
&fixed_code[..start_with_offset as usize],
suggestion.as_str(),
&fixed_code[(end_with_offset as usize)..]
);

bytes_offset +=
suggestion.len() as isize - (end_with_offset - start_with_offset);
}
}

owned_diagnostics = get_new_diagnostics(&fixed_code);
owned_diagnostics_ref = owned_diagnostics.iter().collect::<Vec<_>>();
chosen_diagnostics = Self::get_independent_suggestions(&owned_diagnostics_ref);
}

fixed_code
}

/// * Chooses all disjoint suggestions
/// * If a suggestion is completely contained inside others, uses the innermost one and discards the outer ones
/// * If suggestions partially overlap, chooses the one that starts first and discard the other ones
fn get_independent_suggestions<'a>(diagnostics: &'a [&'a Diagnostic]) -> Vec<&'a Diagnostic> {
let mut sorted_diagnostics = diagnostics
.iter()
.filter(|&&diagnostic| {
diagnostic.has_machine_applicable_fix() || diagnostic.has_maybe_incorrect_fix()
})
.copied()
.collect::<Vec<_>>();
sorted_diagnostics.sort_by_key(|diagnostic| diagnostic.start_position());

let mut chosen_diagnostics = Vec::new();

let mut candidate = match sorted_diagnostics.first() {
Some(first) => *first,
None => return Vec::new(),
};

for &diagnostic in sorted_diagnostics.iter().skip(1) {
let this_range = diagnostic.primary_label.range;

if this_range.1 <= candidate.primary_label.range.1 {
// Completely contained inside
candidate = diagnostic;
} else if this_range.0 <= candidate.primary_label.range.1 {
// Partially overlapping
continue;
} else {
chosen_diagnostics.push(candidate);
candidate = diagnostic;
}
}
chosen_diagnostics.push(candidate);

chosen_diagnostics
}
}

#[derive(Debug, PartialEq, Eq)]
Expand Down Expand Up @@ -234,12 +381,17 @@ impl Context {
#[derive(Debug)]
pub struct AstContext {
pub scope_manager: ScopeManager,

// Converting ast -> code should probably come from the parser, but this should be ok as a temporary solution
pub code: String,
}

impl AstContext {
pub fn from_ast(ast: &Ast) -> Self {
#[must_use]
pub fn from_ast(ast: &Ast, code: &String) -> Self {
Self {
scope_manager: ScopeManager::new(ast),
code: code.to_string(),
}
}
}
12 changes: 7 additions & 5 deletions selene-lib/src/lints/almost_swapped.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,22 @@ impl Lint for AlmostSwappedLint {
.almost_swaps
.iter()
.map(|almost_swap| {
Diagnostic::new_complete(
Diagnostic::new(
"almost_swapped",
format!(
"this looks like you are trying to swap `{}` and `{}`",
(almost_swap.names.0),
(almost_swap.names.1),
),
Label::new(almost_swap.range),
vec![format!(
"try: `{name1}, {name2} = {name2}, {name1}`",
Some(format!(
"{name1}, {name2} = {name2}, {name1}",
name1 = almost_swap.names.0,
name2 = almost_swap.names.1,
)],
Vec::new(),
)),
// FIXME: Ideally `MaybeIncorrect`, but the end range for swapping `t[1]` and `t[2]`
// currently doesn't include the closing `]`
Applicability::HasPlaceholders,
)
})
.collect()
Expand Down
Loading
Loading