mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 02:38:25 +00:00
feat(W191): add indentation_contains_tabs (#3249)
This commit is contained in:
parent
d285f5c90a
commit
e8ba9c9e21
12 changed files with 591 additions and 4 deletions
145
crates/ruff/resources/test/fixtures/pycodestyle/W19.py
vendored
Normal file
145
crates/ruff/resources/test/fixtures/pycodestyle/W19.py
vendored
Normal file
|
@ -0,0 +1,145 @@
|
|||
#: W191
|
||||
if False:
|
||||
print # indented with 1 tab
|
||||
#:
|
||||
|
||||
|
||||
#: W191
|
||||
y = x == 2 \
|
||||
or x == 3
|
||||
#: E101 W191 W504
|
||||
if (
|
||||
x == (
|
||||
3
|
||||
) or
|
||||
y == 4):
|
||||
pass
|
||||
#: E101 W191
|
||||
if x == 2 \
|
||||
or y > 1 \
|
||||
or x == 3:
|
||||
pass
|
||||
#: E101 W191
|
||||
if x == 2 \
|
||||
or y > 1 \
|
||||
or x == 3:
|
||||
pass
|
||||
#:
|
||||
|
||||
#: E101 W191 W504
|
||||
if (foo == bar and
|
||||
baz == bop):
|
||||
pass
|
||||
#: E101 W191 W504
|
||||
if (
|
||||
foo == bar and
|
||||
baz == bop
|
||||
):
|
||||
pass
|
||||
#:
|
||||
|
||||
#: E101 E101 W191 W191
|
||||
if start[1] > end_col and not (
|
||||
over_indent == 4 and indent_next):
|
||||
return (0, "E121 continuation line over-"
|
||||
"indented for visual indent")
|
||||
#:
|
||||
|
||||
#: E101 W191
|
||||
|
||||
|
||||
def long_function_name(
|
||||
var_one, var_two, var_three,
|
||||
var_four):
|
||||
print(var_one)
|
||||
#: E101 W191 W504
|
||||
if ((row < 0 or self.moduleCount <= row or
|
||||
col < 0 or self.moduleCount <= col)):
|
||||
raise Exception("%s,%s - %s" % (row, col, self.moduleCount))
|
||||
#: E101 E101 E101 E101 W191 W191 W191 W191 W191 W191
|
||||
if bar:
|
||||
return (
|
||||
start, 'E121 lines starting with a '
|
||||
'closing bracket should be indented '
|
||||
"to match that of the opening "
|
||||
"bracket's line"
|
||||
)
|
||||
#
|
||||
#: E101 W191 W504
|
||||
# you want vertical alignment, so use a parens
|
||||
if ((foo.bar("baz") and
|
||||
foo.bar("bop")
|
||||
)):
|
||||
print "yes"
|
||||
#: E101 W191 W504
|
||||
# also ok, but starting to look like LISP
|
||||
if ((foo.bar("baz") and
|
||||
foo.bar("bop"))):
|
||||
print "yes"
|
||||
#: E101 W191 W504
|
||||
if (a == 2 or
|
||||
b == "abc def ghi"
|
||||
"jkl mno"):
|
||||
return True
|
||||
#: E101 W191 W504
|
||||
if (a == 2 or
|
||||
b == """abc def ghi
|
||||
jkl mno"""):
|
||||
return True
|
||||
#: W191:2:1 W191:3:1 E101:3:2
|
||||
if length > options.max_line_length:
|
||||
return options.max_line_length, \
|
||||
"E501 line too long (%d characters)" % length
|
||||
|
||||
|
||||
#
|
||||
#: E101 W191 W191 W504
|
||||
if os.path.exists(os.path.join(path, PEP8_BIN)):
|
||||
cmd = ([os.path.join(path, PEP8_BIN)] +
|
||||
self._pep8_options(targetfile))
|
||||
#: W191
|
||||
'''
|
||||
multiline string with tab in it'''
|
||||
#: E101 W191
|
||||
'''multiline string
|
||||
with tabs
|
||||
and spaces
|
||||
'''
|
||||
#: Okay
|
||||
'''sometimes, you just need to go nuts in a multiline string
|
||||
and allow all sorts of crap
|
||||
like mixed tabs and spaces
|
||||
|
||||
or trailing whitespace
|
||||
or long long long long long long long long long long long long long long long long long lines
|
||||
''' # nopep8
|
||||
#: Okay
|
||||
'''this one
|
||||
will get no warning
|
||||
even though the noqa comment is not immediately after the string
|
||||
''' + foo # noqa
|
||||
#
|
||||
#: E101 W191
|
||||
if foo is None and bar is "bop" and \
|
||||
blah == 'yeah':
|
||||
blah = 'yeahnah'
|
||||
|
||||
|
||||
#
|
||||
#: W191 W191 W191
|
||||
if True:
|
||||
foo(
|
||||
1,
|
||||
2)
|
||||
#: W191 W191 W191 W191 W191
|
||||
def test_keys(self):
|
||||
"""areas.json - All regions are accounted for."""
|
||||
expected = set([
|
||||
u'Norrbotten',
|
||||
u'V\xe4sterbotten',
|
||||
])
|
||||
#: W191
|
||||
x = [
|
||||
'abc'
|
||||
]
|
||||
#:
|
|
@ -8,8 +8,8 @@ use crate::rules::flake8_executable::rules::{
|
|||
shebang_missing, shebang_newline, shebang_not_executable, shebang_python, shebang_whitespace,
|
||||
};
|
||||
use crate::rules::pycodestyle::rules::{
|
||||
doc_line_too_long, line_too_long, mixed_spaces_and_tabs, no_newline_at_end_of_file,
|
||||
trailing_whitespace,
|
||||
doc_line_too_long, indentation_contains_tabs, line_too_long, mixed_spaces_and_tabs,
|
||||
no_newline_at_end_of_file, trailing_whitespace,
|
||||
};
|
||||
use crate::rules::pygrep_hooks::rules::{blanket_noqa, blanket_type_ignore};
|
||||
use crate::rules::pylint;
|
||||
|
@ -45,6 +45,7 @@ pub fn check_physical_lines(
|
|||
let enforce_trailing_whitespace = settings.rules.enabled(&Rule::TrailingWhitespace);
|
||||
let enforce_blank_line_contains_whitespace =
|
||||
settings.rules.enabled(&Rule::BlankLineContainsWhitespace);
|
||||
let enforce_indentation_contains_tabs = settings.rules.enabled(&Rule::IndentationContainsTabs);
|
||||
|
||||
let fix_unnecessary_coding_comment =
|
||||
autofix.into() && settings.rules.should_fix(&Rule::UTF8EncodingDeclaration);
|
||||
|
@ -149,6 +150,12 @@ pub fn check_physical_lines(
|
|||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
if enforce_indentation_contains_tabs {
|
||||
if let Some(diagnostic) = indentation_contains_tabs(index, line) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if enforce_no_newline_at_end_of_file {
|
||||
|
|
|
@ -74,6 +74,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
|
|||
(Pycodestyle, "E999") => Rule::SyntaxError,
|
||||
|
||||
// pycodestyle warnings
|
||||
(Pycodestyle, "W191") => Rule::IndentationContainsTabs,
|
||||
(Pycodestyle, "W291") => Rule::TrailingWhitespace,
|
||||
(Pycodestyle, "W292") => Rule::NoNewLineAtEndOfFile,
|
||||
(Pycodestyle, "W293") => Rule::BlankLineContainsWhitespace,
|
||||
|
|
|
@ -79,6 +79,7 @@ ruff_macros::register_rules!(
|
|||
rules::pycodestyle::rules::IOError,
|
||||
rules::pycodestyle::rules::SyntaxError,
|
||||
// pycodestyle warnings
|
||||
rules::pycodestyle::rules::IndentationContainsTabs,
|
||||
rules::pycodestyle::rules::TrailingWhitespace,
|
||||
rules::pycodestyle::rules::NoNewLineAtEndOfFile,
|
||||
rules::pycodestyle::rules::BlankLineContainsWhitespace,
|
||||
|
@ -803,6 +804,7 @@ impl Rule {
|
|||
| Rule::ShebangPython
|
||||
| Rule::ShebangWhitespace
|
||||
| Rule::TrailingWhitespace
|
||||
| Rule::IndentationContainsTabs
|
||||
| Rule::BlankLineContainsWhitespace => &LintSource::PhysicalLines,
|
||||
Rule::AmbiguousUnicodeCharacterComment
|
||||
| Rule::AmbiguousUnicodeCharacterDocstring
|
||||
|
|
|
@ -42,6 +42,7 @@ mod tests {
|
|||
#[test_case(Rule::NotInTest, Path::new("E713.py"))]
|
||||
#[test_case(Rule::NotIsTest, Path::new("E714.py"))]
|
||||
#[test_case(Rule::SyntaxError, Path::new("E999.py"))]
|
||||
#[test_case(Rule::IndentationContainsTabs, Path::new("W19.py"))]
|
||||
#[test_case(Rule::TrailingWhitespace, Path::new("W29.py"))]
|
||||
#[test_case(Rule::TrueFalseComparison, Path::new("E712.py"))]
|
||||
#[test_case(Rule::TypeComparison, Path::new("E721.py"))]
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
use ruff_macros::{define_violation, derive_message_formats};
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::ast::whitespace::leading_space;
|
||||
use crate::registry::Diagnostic;
|
||||
use crate::violation::Violation;
|
||||
|
||||
define_violation!(
|
||||
pub struct IndentationContainsTabs;
|
||||
);
|
||||
impl Violation for IndentationContainsTabs {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Indentation contains tabs")
|
||||
}
|
||||
}
|
||||
|
||||
/// W191
|
||||
pub fn indentation_contains_tabs(lineno: usize, line: &str) -> Option<Diagnostic> {
|
||||
let indent = leading_space(line);
|
||||
|
||||
if indent.contains('\t') {
|
||||
Some(Diagnostic::new(
|
||||
IndentationContainsTabs,
|
||||
Range::new(
|
||||
Location::new(lineno + 1, 0),
|
||||
Location::new(lineno + 1, indent.chars().count()),
|
||||
),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
|
@ -21,6 +21,8 @@ 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};
|
||||
pub use line_too_long::{line_too_long, LineTooLong};
|
||||
|
@ -58,6 +60,7 @@ mod errors;
|
|||
mod extraneous_whitespace;
|
||||
mod imports;
|
||||
mod indentation;
|
||||
mod indentation_contains_tabs;
|
||||
mod invalid_escape_sequence;
|
||||
mod lambda_assignment;
|
||||
mod line_too_long;
|
||||
|
|
|
@ -0,0 +1,385 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/pycodestyle/mod.rs
|
||||
expression: diagnostics
|
||||
---
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 3
|
||||
column: 0
|
||||
end_location:
|
||||
row: 3
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 9
|
||||
column: 0
|
||||
end_location:
|
||||
row: 9
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 16
|
||||
column: 0
|
||||
end_location:
|
||||
row: 16
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 21
|
||||
column: 0
|
||||
end_location:
|
||||
row: 21
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 26
|
||||
column: 0
|
||||
end_location:
|
||||
row: 26
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 32
|
||||
column: 0
|
||||
end_location:
|
||||
row: 32
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 38
|
||||
column: 0
|
||||
end_location:
|
||||
row: 38
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 44
|
||||
column: 0
|
||||
end_location:
|
||||
row: 44
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 45
|
||||
column: 0
|
||||
end_location:
|
||||
row: 45
|
||||
column: 9
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 54
|
||||
column: 0
|
||||
end_location:
|
||||
row: 54
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 58
|
||||
column: 0
|
||||
end_location:
|
||||
row: 58
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 61
|
||||
column: 0
|
||||
end_location:
|
||||
row: 61
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 62
|
||||
column: 0
|
||||
end_location:
|
||||
row: 62
|
||||
column: 5
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 63
|
||||
column: 0
|
||||
end_location:
|
||||
row: 63
|
||||
column: 5
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 64
|
||||
column: 0
|
||||
end_location:
|
||||
row: 64
|
||||
column: 5
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 65
|
||||
column: 0
|
||||
end_location:
|
||||
row: 65
|
||||
column: 5
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 66
|
||||
column: 0
|
||||
end_location:
|
||||
row: 66
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 73
|
||||
column: 0
|
||||
end_location:
|
||||
row: 73
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 78
|
||||
column: 0
|
||||
end_location:
|
||||
row: 78
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 83
|
||||
column: 0
|
||||
end_location:
|
||||
row: 83
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 88
|
||||
column: 0
|
||||
end_location:
|
||||
row: 88
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 91
|
||||
column: 0
|
||||
end_location:
|
||||
row: 91
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 92
|
||||
column: 0
|
||||
end_location:
|
||||
row: 92
|
||||
column: 5
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 98
|
||||
column: 0
|
||||
end_location:
|
||||
row: 98
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 99
|
||||
column: 0
|
||||
end_location:
|
||||
row: 99
|
||||
column: 8
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 102
|
||||
column: 0
|
||||
end_location:
|
||||
row: 102
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 105
|
||||
column: 0
|
||||
end_location:
|
||||
row: 105
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 110
|
||||
column: 0
|
||||
end_location:
|
||||
row: 110
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 125
|
||||
column: 0
|
||||
end_location:
|
||||
row: 125
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 131
|
||||
column: 0
|
||||
end_location:
|
||||
row: 131
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 132
|
||||
column: 0
|
||||
end_location:
|
||||
row: 132
|
||||
column: 2
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 133
|
||||
column: 0
|
||||
end_location:
|
||||
row: 133
|
||||
column: 2
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 136
|
||||
column: 0
|
||||
end_location:
|
||||
row: 136
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 137
|
||||
column: 0
|
||||
end_location:
|
||||
row: 137
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 138
|
||||
column: 0
|
||||
end_location:
|
||||
row: 138
|
||||
column: 2
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 139
|
||||
column: 0
|
||||
end_location:
|
||||
row: 139
|
||||
column: 2
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 140
|
||||
column: 0
|
||||
end_location:
|
||||
row: 140
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
IndentationContainsTabs: ~
|
||||
location:
|
||||
row: 143
|
||||
column: 0
|
||||
end_location:
|
||||
row: 143
|
||||
column: 1
|
||||
fix: ~
|
||||
parent: ~
|
||||
|
|
@ -56,6 +56,7 @@ mod tests {
|
|||
Rule::LineTooLong,
|
||||
Rule::UnusedImport,
|
||||
Rule::UnusedVariable,
|
||||
Rule::IndentationContainsTabs,
|
||||
]),
|
||||
)?;
|
||||
assert_yaml_snapshot!(diagnostics);
|
||||
|
|
|
@ -68,12 +68,12 @@ expression: diagnostics
|
|||
- kind:
|
||||
UnusedNOQA:
|
||||
codes:
|
||||
unknown:
|
||||
- W191
|
||||
unknown: []
|
||||
disabled:
|
||||
- F821
|
||||
unmatched:
|
||||
- F841
|
||||
- W191
|
||||
location:
|
||||
row: 19
|
||||
column: 11
|
||||
|
|
|
@ -470,6 +470,7 @@ mod tests {
|
|||
Rule::BlankLineContainsWhitespace,
|
||||
Rule::DocLineTooLong,
|
||||
Rule::InvalidEscapeSequence,
|
||||
Rule::IndentationContainsTabs,
|
||||
]);
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
|
@ -490,6 +491,7 @@ mod tests {
|
|||
Rule::BlankLineContainsWhitespace,
|
||||
Rule::DocLineTooLong,
|
||||
Rule::InvalidEscapeSequence,
|
||||
Rule::IndentationContainsTabs,
|
||||
]);
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
|
@ -526,6 +528,7 @@ mod tests {
|
|||
Rule::BlankLineContainsWhitespace,
|
||||
Rule::DocLineTooLong,
|
||||
Rule::InvalidEscapeSequence,
|
||||
Rule::IndentationContainsTabs,
|
||||
]);
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
|
@ -563,6 +566,7 @@ mod tests {
|
|||
Rule::BlankLineContainsWhitespace,
|
||||
Rule::DocLineTooLong,
|
||||
Rule::InvalidEscapeSequence,
|
||||
Rule::IndentationContainsTabs,
|
||||
]);
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
|
@ -582,6 +586,7 @@ mod tests {
|
|||
Rule::TrailingWhitespace,
|
||||
Rule::BlankLineContainsWhitespace,
|
||||
Rule::InvalidEscapeSequence,
|
||||
Rule::IndentationContainsTabs,
|
||||
]);
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
|
3
ruff.schema.json
generated
3
ruff.schema.json
generated
|
@ -2123,6 +2123,9 @@
|
|||
"UP036",
|
||||
"UP037",
|
||||
"W",
|
||||
"W1",
|
||||
"W19",
|
||||
"W191",
|
||||
"W2",
|
||||
"W29",
|
||||
"W291",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue