feat(e225,226,227,228): add rules (#3300)

This commit is contained in:
Carlos Gonçalves
2023-03-02 22:54:45 +00:00
committed by GitHub
parent 6f649d6579
commit 7e291e542d
11 changed files with 747 additions and 14 deletions

View File

@@ -9,9 +9,10 @@ use crate::ast::types::Range;
use crate::registry::{Diagnostic, Rule};
use crate::rules::pycodestyle::logical_lines::{iter_logical_lines, TokenFlags};
use crate::rules::pycodestyle::rules::{
extraneous_whitespace, indentation, missing_whitespace_after_keyword, space_around_operator,
whitespace_around_keywords, whitespace_around_named_parameter_equals,
whitespace_before_comment, whitespace_before_parameters,
extraneous_whitespace, indentation, missing_whitespace_after_keyword,
missing_whitespace_around_operator, space_around_operator, whitespace_around_keywords,
whitespace_around_named_parameter_equals, whitespace_before_comment,
whitespace_before_parameters,
};
use crate::settings::{flags, Settings};
use crate::source_code::{Locator, Stylist};
@@ -150,6 +151,17 @@ pub fn check_logical_lines(
});
}
}
for (location, kind) in missing_whitespace_around_operator(&line.tokens) {
if settings.rules.enabled(kind.rule()) {
diagnostics.push(Diagnostic {
kind,
location,
end_location: location,
fix: None,
parent: None,
});
}
}
}
if line.flags.contains(TokenFlags::BRACKET) {

View File

@@ -39,6 +39,14 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E224") => Rule::TabAfterOperator,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E225") => Rule::MissingWhitespaceAroundOperator,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E226") => Rule::MissingWhitespaceAroundArithmeticOperator,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E227") => Rule::MissingWhitespaceAroundBitwiseOrShiftOperator,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E228") => Rule::MissingWhitespaceAroundModuloOperator,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E251") => Rule::UnexpectedSpacesAroundKeywordParameterEquals,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E252") => Rule::MissingWhitespaceAroundParameterEquals,

View File

@@ -57,6 +57,14 @@ ruff_macros::register_rules!(
#[cfg(feature = "logical_lines")]
rules::pycodestyle::rules::MultipleSpacesBeforeKeyword,
#[cfg(feature = "logical_lines")]
rules::pycodestyle::rules::MissingWhitespaceAroundOperator,
#[cfg(feature = "logical_lines")]
rules::pycodestyle::rules::MissingWhitespaceAroundArithmeticOperator,
#[cfg(feature = "logical_lines")]
rules::pycodestyle::rules::MissingWhitespaceAroundBitwiseOrShiftOperator,
#[cfg(feature = "logical_lines")]
rules::pycodestyle::rules::MissingWhitespaceAroundModuloOperator,
#[cfg(feature = "logical_lines")]
rules::pycodestyle::rules::TabAfterKeyword,
#[cfg(feature = "logical_lines")]
rules::pycodestyle::rules::UnexpectedSpacesAroundKeywordParameterEquals,
@@ -837,12 +845,17 @@ impl Rule {
#[cfg(feature = "logical_lines")]
Rule::IndentationWithInvalidMultiple
| Rule::IndentationWithInvalidMultipleComment
| Rule::MissingWhitespaceAfterKeyword
| Rule::MissingWhitespaceAroundArithmeticOperator
| Rule::MissingWhitespaceAroundBitwiseOrShiftOperator
| Rule::MissingWhitespaceAroundModuloOperator
| Rule::MissingWhitespaceAroundOperator
| Rule::MissingWhitespaceAroundParameterEquals
| Rule::MultipleLeadingHashesForBlockComment
| Rule::MultipleSpacesAfterKeyword
| Rule::MultipleSpacesAfterOperator
| Rule::MultipleSpacesBeforeKeyword
| Rule::MultipleSpacesBeforeOperator
| Rule::MissingWhitespaceAfterKeyword
| Rule::NoIndentedBlock
| Rule::NoIndentedBlockComment
| Rule::NoSpaceAfterBlockComment
@@ -855,10 +868,9 @@ impl Rule {
| Rule::TooFewSpacesBeforeInlineComment
| Rule::UnexpectedIndentation
| Rule::UnexpectedIndentationComment
| Rule::UnexpectedSpacesAroundKeywordParameterEquals
| Rule::WhitespaceAfterOpenBracket
| Rule::WhitespaceBeforeCloseBracket
| Rule::UnexpectedSpacesAroundKeywordParameterEquals
| Rule::MissingWhitespaceAroundParameterEquals
| Rule::WhitespaceBeforeParameters
| Rule::WhitespaceBeforePunctuation => &LintSource::LogicalLines,
_ => &LintSource::Ast,

View File

@@ -157,6 +157,70 @@ pub fn is_op_token(token: &Tok) -> bool {
)
}
pub fn is_skip_comment_token(token: &Tok) -> bool {
matches!(
token,
Tok::Newline | Tok::Indent | Tok::Dedent | Tok::NonLogicalNewline | Tok::Comment { .. }
)
}
pub fn is_soft_keyword_token(token: &Tok) -> bool {
matches!(token, Tok::Match | Tok::Case)
}
pub fn is_arithmetic_token(token: &Tok) -> bool {
matches!(
token,
Tok::DoubleStar | Tok::Star | Tok::Plus | Tok::Minus | Tok::Slash | Tok::At
)
}
pub fn is_ws_optional_token(token: &Tok) -> bool {
is_arithmetic_token(token)
|| matches!(
token,
Tok::CircumFlex
| Tok::Amper
| Tok::Vbar
| Tok::LeftShift
| Tok::RightShift
| Tok::Percent
)
}
pub fn is_ws_needed_token(token: &Tok) -> bool {
matches!(
token,
Tok::DoubleStarEqual
| Tok::StarEqual
| Tok::SlashEqual
| Tok::DoubleSlashEqual
| Tok::PlusEqual
| Tok::MinusEqual
| Tok::NotEqual
| Tok::Less
| Tok::Greater
| Tok::PercentEqual
| Tok::CircumflexEqual
| Tok::AmperEqual
| Tok::VbarEqual
| Tok::EqEqual
| Tok::LessEqual
| Tok::GreaterEqual
| Tok::LeftShiftEqual
| Tok::RightShiftEqual
| Tok::Equal
| Tok::And
| Tok::Or
| Tok::In
| Tok::Is
| Tok::Rarrow
)
}
pub fn is_unary_token(token: &Tok) -> bool {
matches!(
token,
Tok::Plus | Tok::Minus | Tok::Star | Tok::DoubleStar | Tok::RightShift
)
}

View File

@@ -13,12 +13,13 @@ mod tests {
use insta::assert_yaml_snapshot;
use test_case::test_case;
use super::settings::Settings;
use crate::registry::Rule;
use crate::settings;
use crate::source_code::LineEnding;
use crate::test::test_path;
use super::settings::Settings;
#[test_case(Rule::AmbiguousClassName, Path::new("E742.py"))]
#[test_case(Rule::AmbiguousFunctionName, Path::new("E743.py"))]
#[test_case(Rule::AmbiguousVariableName, Path::new("E741.py"))]
@@ -95,6 +96,13 @@ mod tests {
#[test_case(Rule::TabAfterOperator, Path::new("E22.py"))]
#[test_case(Rule::TabBeforeKeyword, Path::new("E27.py"))]
#[test_case(Rule::TabBeforeOperator, Path::new("E22.py"))]
#[test_case(Rule::MissingWhitespaceAroundOperator, Path::new("E22.py"))]
#[test_case(Rule::MissingWhitespaceAroundArithmeticOperator, Path::new("E22.py"))]
#[test_case(
Rule::MissingWhitespaceAroundBitwiseOrShiftOperator,
Path::new("E22.py")
)]
#[test_case(Rule::MissingWhitespaceAroundModuloOperator, Path::new("E22.py"))]
#[test_case(Rule::TooFewSpacesBeforeInlineComment, Path::new("E26.py"))]
#[test_case(Rule::UnexpectedIndentation, Path::new("E11.py"))]
#[test_case(Rule::UnexpectedIndentationComment, Path::new("E11.py"))]

View File

@@ -0,0 +1,196 @@
#![allow(dead_code, unused_imports, unused_variables)]
use rustpython_parser::ast::Location;
use rustpython_parser::Tok;
use ruff_macros::{define_violation, derive_message_formats};
use crate::registry::DiagnosticKind;
use crate::rules::pycodestyle::helpers::{
is_arithmetic_token, is_keyword_token, is_op_token, is_singleton_token, is_skip_comment_token,
is_soft_keyword_token, is_unary_token, is_ws_needed_token, is_ws_optional_token,
};
use crate::violation::Violation;
// E225
define_violation!(
pub struct MissingWhitespaceAroundOperator;
);
impl Violation for MissingWhitespaceAroundOperator {
#[derive_message_formats]
fn message(&self) -> String {
format!("Missing whitespace around operator")
}
}
// E226
define_violation!(
pub struct MissingWhitespaceAroundArithmeticOperator;
);
impl Violation for MissingWhitespaceAroundArithmeticOperator {
#[derive_message_formats]
fn message(&self) -> String {
format!("Missing whitespace around arithmetic operator")
}
}
// E227
define_violation!(
pub struct MissingWhitespaceAroundBitwiseOrShiftOperator;
);
impl Violation for MissingWhitespaceAroundBitwiseOrShiftOperator {
#[derive_message_formats]
fn message(&self) -> String {
format!("Missing whitespace around bitwise or shift operator")
}
}
// E228
define_violation!(
pub struct MissingWhitespaceAroundModuloOperator;
);
impl Violation for MissingWhitespaceAroundModuloOperator {
#[derive_message_formats]
fn message(&self) -> String {
format!("Missing whitespace around modulo operator")
}
}
/// E225, E226, E227, E228
#[cfg(feature = "logical_lines")]
#[allow(clippy::if_same_then_else)]
pub fn missing_whitespace_around_operator(
tokens: &[(Location, &Tok, Location)],
) -> Vec<(Location, DiagnosticKind)> {
let mut diagnostics = vec![];
let mut needs_space_main: Option<bool> = Some(false);
let mut needs_space_aux: Option<bool> = None;
let mut prev_end_aux: Option<&Location> = None;
let mut parens = 0;
let mut prev_type: Option<&Tok> = None;
let mut prev_end: Option<&Location> = None;
for (start, token, end) in tokens {
if is_skip_comment_token(token) {
continue;
}
if **token == Tok::Lpar || **token == Tok::Lambda {
parens += 1;
} else if **token == Tok::Rpar {
parens -= 1;
}
let needs_space = (needs_space_main.is_some() && needs_space_main.unwrap())
|| needs_space_aux.is_some()
|| prev_end_aux.is_some();
if needs_space {
if Some(start) != prev_end {
if !(needs_space_main.is_some() && needs_space_main.unwrap())
&& (needs_space_aux.is_none() || !needs_space_aux.unwrap())
{
diagnostics.push((
*(prev_end_aux.unwrap()),
MissingWhitespaceAroundOperator.into(),
));
}
needs_space_main = Some(false);
needs_space_aux = None;
prev_end_aux = None;
} else if **token == Tok::Greater
&& (prev_type == Some(&Tok::Less) || prev_type == Some(&Tok::Minus))
{
// Tolerate the "<>" operator, even if running Python 3
// Deal with Python 3's annotated return value "->"
} else if prev_type == Some(&Tok::Slash)
&& (**token == Tok::Comma || **token == Tok::Rpar || **token == Tok::Colon)
|| (prev_type == Some(&Tok::Rpar) && **token == Tok::Colon)
{
// Tolerate the "/" operator in function definition
// For more info see PEP570
} else {
if (needs_space_main.is_some() && needs_space_main.unwrap())
|| (needs_space_aux.is_some() && needs_space_aux.unwrap())
{
diagnostics
.push((*(prev_end.unwrap()), MissingWhitespaceAroundOperator.into()));
} else if prev_type != Some(&Tok::DoubleStar) {
if prev_type == Some(&Tok::Percent) {
diagnostics.push((
*(prev_end_aux.unwrap()),
MissingWhitespaceAroundModuloOperator.into(),
));
} else if !is_arithmetic_token(prev_type.unwrap()) {
diagnostics.push((
*(prev_end_aux.unwrap()),
MissingWhitespaceAroundBitwiseOrShiftOperator.into(),
));
} else {
diagnostics.push((
*(prev_end_aux.unwrap()),
MissingWhitespaceAroundArithmeticOperator.into(),
));
}
}
needs_space_main = Some(false);
needs_space_aux = None;
prev_end_aux = None;
}
} else if (is_op_token(token) || matches!(token, Tok::Name { .. })) && prev_end.is_some() {
if **token == Tok::Equal && parens > 0 {
// Allow keyword args or defaults: foo(bar=None).
} else if is_ws_needed_token(token) {
needs_space_main = Some(true);
needs_space_aux = None;
prev_end_aux = None;
} else if is_unary_token(token) {
// Check if the operator is used as a binary operator
// Allow unary operators: -123, -x, +1.
// Allow argument unpacking: foo(*args, **kwargs)
if (prev_type.is_some()
&& is_op_token(prev_type.unwrap())
&& (prev_type == Some(&Tok::Rpar)
|| prev_type == Some(&Tok::Rsqb)
|| prev_type == Some(&Tok::Rbrace)))
|| (!is_op_token(prev_type.unwrap()) && !is_keyword_token(prev_type.unwrap()))
&& (!is_soft_keyword_token(prev_type.unwrap()))
{
needs_space_main = None;
needs_space_aux = None;
prev_end_aux = None;
}
} else if is_ws_optional_token(token) {
needs_space_main = None;
needs_space_aux = None;
prev_end_aux = None;
}
if needs_space_main.is_none() {
// Surrounding space is optional, but ensure that
// trailing space matches opening space
needs_space_main = None;
prev_end_aux = prev_end;
needs_space_aux = Some(Some(start) != prev_end_aux);
} else if needs_space_main.is_some()
&& needs_space_main.unwrap()
&& Some(start) == prev_end_aux
{
// A needed opening space was not found
diagnostics.push((*(prev_end.unwrap()), MissingWhitespaceAroundOperator.into()));
needs_space_main = Some(false);
needs_space_aux = None;
prev_end_aux = None;
}
}
prev_type = Some(*token);
prev_end = Some(end);
}
diagnostics
}
#[cfg(not(feature = "logical_lines"))]
pub fn missing_whitespace_around_operator(
_tokens: &[(Location, &Tok, Location)],
) -> Vec<(Location, DiagnosticKind)> {
vec![]
}

View File

@@ -21,7 +21,6 @@ pub use indentation::{
NoIndentedBlock, NoIndentedBlockComment, OverIndented, UnexpectedIndentation,
UnexpectedIndentationComment,
};
pub use indentation_contains_tabs::{indentation_contains_tabs, IndentationContainsTabs};
pub use invalid_escape_sequence::{invalid_escape_sequence, InvalidEscapeSequence};
pub use lambda_assignment::{lambda_assignment, LambdaAssignment};
@@ -30,6 +29,11 @@ pub use literal_comparisons::{literal_comparisons, NoneComparison, TrueFalseComp
pub use missing_whitespace_after_keyword::{
missing_whitespace_after_keyword, MissingWhitespaceAfterKeyword,
};
pub use missing_whitespace_around_operator::{
missing_whitespace_around_operator, MissingWhitespaceAroundArithmeticOperator,
MissingWhitespaceAroundBitwiseOrShiftOperator, MissingWhitespaceAroundModuloOperator,
MissingWhitespaceAroundOperator,
};
pub use mixed_spaces_and_tabs::{mixed_spaces_and_tabs, MixedSpacesAndTabs};
pub use no_newline_at_end_of_file::{no_newline_at_end_of_file, NoNewLineAtEndOfFile};
pub use not_tests::{not_tests, NotInTest, NotIsTest};
@@ -45,16 +49,14 @@ pub use whitespace_around_keywords::{
whitespace_around_keywords, MultipleSpacesAfterKeyword, MultipleSpacesBeforeKeyword,
TabAfterKeyword, TabBeforeKeyword,
};
pub use whitespace_before_comment::{
whitespace_before_comment, MultipleLeadingHashesForBlockComment, NoSpaceAfterBlockComment,
NoSpaceAfterInlineComment, TooFewSpacesBeforeInlineComment,
};
pub use whitespace_around_named_parameter_equals::{
whitespace_around_named_parameter_equals, MissingWhitespaceAroundParameterEquals,
UnexpectedSpacesAroundKeywordParameterEquals,
};
pub use whitespace_before_comment::{
whitespace_before_comment, MultipleLeadingHashesForBlockComment, NoSpaceAfterBlockComment,
NoSpaceAfterInlineComment, TooFewSpacesBeforeInlineComment,
};
pub use whitespace_before_parameters::{whitespace_before_parameters, WhitespaceBeforeParameters};
mod ambiguous_class_name;
@@ -73,6 +75,7 @@ mod lambda_assignment;
mod line_too_long;
mod literal_comparisons;
mod missing_whitespace_after_keyword;
mod missing_whitespace_around_operator;
mod mixed_spaces_and_tabs;
mod no_newline_at_end_of_file;
mod not_tests;

View File

@@ -0,0 +1,215 @@
---
source: crates/ruff/src/rules/pycodestyle/mod.rs
expression: diagnostics
---
- kind:
MissingWhitespaceAroundOperator: ~
location:
row: 54
column: 12
end_location:
row: 54
column: 12
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundOperator: ~
location:
row: 58
column: 3
end_location:
row: 58
column: 3
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundOperator: ~
location:
row: 60
column: 7
end_location:
row: 60
column: 7
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundOperator: ~
location:
row: 62
column: 11
end_location:
row: 62
column: 11
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundOperator: ~
location:
row: 64
column: 9
end_location:
row: 64
column: 9
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundOperator: ~
location:
row: 66
column: 8
end_location:
row: 66
column: 8
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundOperator: ~
location:
row: 68
column: 14
end_location:
row: 68
column: 14
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundOperator: ~
location:
row: 70
column: 11
end_location:
row: 70
column: 11
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundOperator: ~
location:
row: 72
column: 14
end_location:
row: 72
column: 14
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundOperator: ~
location:
row: 74
column: 11
end_location:
row: 74
column: 11
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundOperator: ~
location:
row: 76
column: 2
end_location:
row: 76
column: 2
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundOperator: ~
location:
row: 76
column: 3
end_location:
row: 76
column: 3
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundOperator: ~
location:
row: 78
column: 2
end_location:
row: 78
column: 2
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundOperator: ~
location:
row: 78
column: 5
end_location:
row: 78
column: 5
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundOperator: ~
location:
row: 88
column: 7
end_location:
row: 88
column: 7
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundOperator: ~
location:
row: 90
column: 5
end_location:
row: 90
column: 5
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundOperator: ~
location:
row: 92
column: 2
end_location:
row: 92
column: 2
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundOperator: ~
location:
row: 94
column: 3
end_location:
row: 94
column: 3
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundOperator: ~
location:
row: 98
column: 8
end_location:
row: 98
column: 8
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundOperator: ~
location:
row: 100
column: 6
end_location:
row: 100
column: 6
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundOperator: ~
location:
row: 154
column: 12
end_location:
row: 154
column: 12
fix: ~
parent: ~

View File

@@ -0,0 +1,125 @@
---
source: crates/ruff/src/rules/pycodestyle/mod.rs
expression: diagnostics
---
- kind:
MissingWhitespaceAroundArithmeticOperator: ~
location:
row: 92
column: 3
end_location:
row: 92
column: 3
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundArithmeticOperator: ~
location:
row: 94
column: 4
end_location:
row: 94
column: 4
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundArithmeticOperator: ~
location:
row: 96
column: 4
end_location:
row: 96
column: 4
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundArithmeticOperator: ~
location:
row: 98
column: 10
end_location:
row: 98
column: 10
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundArithmeticOperator: ~
location:
row: 100
column: 10
end_location:
row: 100
column: 10
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundArithmeticOperator: ~
location:
row: 106
column: 6
end_location:
row: 106
column: 6
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundArithmeticOperator: ~
location:
row: 106
column: 14
end_location:
row: 106
column: 14
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundArithmeticOperator: ~
location:
row: 110
column: 5
end_location:
row: 110
column: 5
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundArithmeticOperator: ~
location:
row: 112
column: 5
end_location:
row: 112
column: 5
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundArithmeticOperator: ~
location:
row: 114
column: 10
end_location:
row: 114
column: 10
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundArithmeticOperator: ~
location:
row: 114
column: 16
end_location:
row: 114
column: 16
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundArithmeticOperator: ~
location:
row: 116
column: 11
end_location:
row: 116
column: 11
fix: ~
parent: ~

View File

@@ -0,0 +1,55 @@
---
source: crates/ruff/src/rules/pycodestyle/mod.rs
expression: diagnostics
---
- kind:
MissingWhitespaceAroundBitwiseOrShiftOperator: ~
location:
row: 121
column: 11
end_location:
row: 121
column: 11
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundBitwiseOrShiftOperator: ~
location:
row: 123
column: 11
end_location:
row: 123
column: 11
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundBitwiseOrShiftOperator: ~
location:
row: 125
column: 5
end_location:
row: 125
column: 5
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundBitwiseOrShiftOperator: ~
location:
row: 127
column: 5
end_location:
row: 127
column: 5
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundBitwiseOrShiftOperator: ~
location:
row: 129
column: 5
end_location:
row: 129
column: 5
fix: ~
parent: ~

View File

@@ -0,0 +1,35 @@
---
source: crates/ruff/src/rules/pycodestyle/mod.rs
expression: diagnostics
---
- kind:
MissingWhitespaceAroundModuloOperator: ~
location:
row: 131
column: 5
end_location:
row: 131
column: 5
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundModuloOperator: ~
location:
row: 133
column: 9
end_location:
row: 133
column: 9
fix: ~
parent: ~
- kind:
MissingWhitespaceAroundModuloOperator: ~
location:
row: 135
column: 25
end_location:
row: 135
column: 25
fix: ~
parent: ~