mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-03 07:04:37 +00:00
[flake8-use-pathlib
] Expand PTH201
to check all PurePath
subclasses (#19440)
## Summary Fixes #19437
This commit is contained in:
parent
6a2d358d7a
commit
b3a26a50ad
5 changed files with 464 additions and 2 deletions
|
@ -1,4 +1,4 @@
|
||||||
from pathlib import Path, PurePath
|
from pathlib import Path, PurePath, PosixPath, PurePosixPath, WindowsPath, PureWindowsPath
|
||||||
from pathlib import Path as pth
|
from pathlib import Path as pth
|
||||||
|
|
||||||
|
|
||||||
|
@ -68,3 +68,11 @@ Path(".", "folder")
|
||||||
PurePath(".", "folder")
|
PurePath(".", "folder")
|
||||||
|
|
||||||
Path()
|
Path()
|
||||||
|
|
||||||
|
from importlib.metadata import PackagePath
|
||||||
|
|
||||||
|
_ = PosixPath(".")
|
||||||
|
_ = PurePosixPath(".")
|
||||||
|
_ = WindowsPath(".")
|
||||||
|
_ = PureWindowsPath(".")
|
||||||
|
_ = PackagePath(".")
|
||||||
|
|
|
@ -20,6 +20,35 @@ pub(crate) fn is_pathlib_path_call(checker: &Checker, expr: &Expr) -> bool {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if the given segments represent a pathlib Path subclass or `PackagePath` with preview mode support.
|
||||||
|
/// In stable mode, only checks for `Path` and `PurePath`. In preview mode, also checks for
|
||||||
|
/// `PosixPath`, `PurePosixPath`, `WindowsPath`, `PureWindowsPath`, and `PackagePath`.
|
||||||
|
pub(crate) fn is_pure_path_subclass_with_preview(
|
||||||
|
checker: &crate::checkers::ast::Checker,
|
||||||
|
segments: &[&str],
|
||||||
|
) -> bool {
|
||||||
|
let is_core_pathlib = matches!(segments, ["pathlib", "Path" | "PurePath"]);
|
||||||
|
|
||||||
|
if is_core_pathlib {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if checker.settings().preview.is_enabled() {
|
||||||
|
let is_expanded_pathlib = matches!(
|
||||||
|
segments,
|
||||||
|
[
|
||||||
|
"pathlib",
|
||||||
|
"PosixPath" | "PurePosixPath" | "WindowsPath" | "PureWindowsPath"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
let is_packagepath = matches!(segments, ["importlib", "metadata", "PackagePath"]);
|
||||||
|
|
||||||
|
return is_expanded_pathlib || is_packagepath;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/// We check functions that take only 1 argument, this does not apply to functions
|
/// We check functions that take only 1 argument, this does not apply to functions
|
||||||
/// with `dir_fd` argument, because `dir_fd` is not supported by pathlib,
|
/// with `dir_fd` argument, because `dir_fd` is not supported by pathlib,
|
||||||
/// so check if it's set to non-default values
|
/// so check if it's set to non-default values
|
||||||
|
|
|
@ -123,6 +123,7 @@ mod tests {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test_case(Rule::PathConstructorCurrentDirectory, Path::new("PTH201.py"))]
|
||||||
#[test_case(Rule::OsPathGetsize, Path::new("PTH202.py"))]
|
#[test_case(Rule::OsPathGetsize, Path::new("PTH202.py"))]
|
||||||
#[test_case(Rule::OsPathGetsize, Path::new("PTH202_2.py"))]
|
#[test_case(Rule::OsPathGetsize, Path::new("PTH202_2.py"))]
|
||||||
#[test_case(Rule::OsPathGetatime, Path::new("PTH203.py"))]
|
#[test_case(Rule::OsPathGetatime, Path::new("PTH203.py"))]
|
||||||
|
|
|
@ -9,6 +9,7 @@ use ruff_text_size::{Ranged, TextRange};
|
||||||
|
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
use crate::fix::edits::{Parentheses, remove_argument};
|
use crate::fix::edits::{Parentheses, remove_argument};
|
||||||
|
use crate::rules::flake8_use_pathlib::helpers::is_pure_path_subclass_with_preview;
|
||||||
use crate::{AlwaysFixableViolation, Applicability, Edit, Fix};
|
use crate::{AlwaysFixableViolation, Applicability, Edit, Fix};
|
||||||
|
|
||||||
/// ## What it does
|
/// ## What it does
|
||||||
|
@ -69,7 +70,7 @@ pub(crate) fn path_constructor_current_directory(
|
||||||
|
|
||||||
let arguments = &call.arguments;
|
let arguments = &call.arguments;
|
||||||
|
|
||||||
if !matches!(segments, ["pathlib", "Path" | "PurePath"]) {
|
if !is_pure_path_subclass_with_preview(checker, segments) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,423 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
|
||||||
|
assertion_line: 144
|
||||||
|
---
|
||||||
|
PTH201.py:6:10: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
||||||
|
|
|
||||||
|
5 | # match
|
||||||
|
6 | _ = Path(".")
|
||||||
|
| ^^^ PTH201
|
||||||
|
7 | _ = pth(".")
|
||||||
|
8 | _ = PurePath(".")
|
||||||
|
|
|
||||||
|
= help: Remove the current directory argument
|
||||||
|
|
||||||
|
ℹ Safe fix
|
||||||
|
3 3 |
|
||||||
|
4 4 |
|
||||||
|
5 5 | # match
|
||||||
|
6 |-_ = Path(".")
|
||||||
|
6 |+_ = Path()
|
||||||
|
7 7 | _ = pth(".")
|
||||||
|
8 8 | _ = PurePath(".")
|
||||||
|
9 9 | _ = Path("")
|
||||||
|
|
||||||
|
PTH201.py:7:9: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
||||||
|
|
|
||||||
|
5 | # match
|
||||||
|
6 | _ = Path(".")
|
||||||
|
7 | _ = pth(".")
|
||||||
|
| ^^^ PTH201
|
||||||
|
8 | _ = PurePath(".")
|
||||||
|
9 | _ = Path("")
|
||||||
|
|
|
||||||
|
= help: Remove the current directory argument
|
||||||
|
|
||||||
|
ℹ Safe fix
|
||||||
|
4 4 |
|
||||||
|
5 5 | # match
|
||||||
|
6 6 | _ = Path(".")
|
||||||
|
7 |-_ = pth(".")
|
||||||
|
7 |+_ = pth()
|
||||||
|
8 8 | _ = PurePath(".")
|
||||||
|
9 9 | _ = Path("")
|
||||||
|
10 10 |
|
||||||
|
|
||||||
|
PTH201.py:8:14: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
||||||
|
|
|
||||||
|
6 | _ = Path(".")
|
||||||
|
7 | _ = pth(".")
|
||||||
|
8 | _ = PurePath(".")
|
||||||
|
| ^^^ PTH201
|
||||||
|
9 | _ = Path("")
|
||||||
|
|
|
||||||
|
= help: Remove the current directory argument
|
||||||
|
|
||||||
|
ℹ Safe fix
|
||||||
|
5 5 | # match
|
||||||
|
6 6 | _ = Path(".")
|
||||||
|
7 7 | _ = pth(".")
|
||||||
|
8 |-_ = PurePath(".")
|
||||||
|
8 |+_ = PurePath()
|
||||||
|
9 9 | _ = Path("")
|
||||||
|
10 10 |
|
||||||
|
11 11 | Path('', )
|
||||||
|
|
||||||
|
PTH201.py:9:10: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
||||||
|
|
|
||||||
|
7 | _ = pth(".")
|
||||||
|
8 | _ = PurePath(".")
|
||||||
|
9 | _ = Path("")
|
||||||
|
| ^^ PTH201
|
||||||
|
10 |
|
||||||
|
11 | Path('', )
|
||||||
|
|
|
||||||
|
= help: Remove the current directory argument
|
||||||
|
|
||||||
|
ℹ Safe fix
|
||||||
|
6 6 | _ = Path(".")
|
||||||
|
7 7 | _ = pth(".")
|
||||||
|
8 8 | _ = PurePath(".")
|
||||||
|
9 |-_ = Path("")
|
||||||
|
9 |+_ = Path()
|
||||||
|
10 10 |
|
||||||
|
11 11 | Path('', )
|
||||||
|
12 12 |
|
||||||
|
|
||||||
|
PTH201.py:11:6: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
||||||
|
|
|
||||||
|
9 | _ = Path("")
|
||||||
|
10 |
|
||||||
|
11 | Path('', )
|
||||||
|
| ^^ PTH201
|
||||||
|
12 |
|
||||||
|
13 | Path(
|
||||||
|
|
|
||||||
|
= help: Remove the current directory argument
|
||||||
|
|
||||||
|
ℹ Safe fix
|
||||||
|
8 8 | _ = PurePath(".")
|
||||||
|
9 9 | _ = Path("")
|
||||||
|
10 10 |
|
||||||
|
11 |-Path('', )
|
||||||
|
11 |+Path()
|
||||||
|
12 12 |
|
||||||
|
13 13 | Path(
|
||||||
|
14 14 | '',
|
||||||
|
|
||||||
|
PTH201.py:14:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
||||||
|
|
|
||||||
|
13 | Path(
|
||||||
|
14 | '',
|
||||||
|
| ^^ PTH201
|
||||||
|
15 | )
|
||||||
|
|
|
||||||
|
= help: Remove the current directory argument
|
||||||
|
|
||||||
|
ℹ Safe fix
|
||||||
|
10 10 |
|
||||||
|
11 11 | Path('', )
|
||||||
|
12 12 |
|
||||||
|
13 |-Path(
|
||||||
|
14 |- '',
|
||||||
|
15 |-)
|
||||||
|
13 |+Path()
|
||||||
|
16 14 |
|
||||||
|
17 15 | Path( # Comment before argument
|
||||||
|
18 16 | '',
|
||||||
|
|
||||||
|
PTH201.py:18:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
||||||
|
|
|
||||||
|
17 | Path( # Comment before argument
|
||||||
|
18 | '',
|
||||||
|
| ^^ PTH201
|
||||||
|
19 | )
|
||||||
|
|
|
||||||
|
= help: Remove the current directory argument
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
14 14 | '',
|
||||||
|
15 15 | )
|
||||||
|
16 16 |
|
||||||
|
17 |-Path( # Comment before argument
|
||||||
|
18 |- '',
|
||||||
|
19 |-)
|
||||||
|
17 |+Path()
|
||||||
|
20 18 |
|
||||||
|
21 19 | Path(
|
||||||
|
22 20 | '', # EOL comment
|
||||||
|
|
||||||
|
PTH201.py:22:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
||||||
|
|
|
||||||
|
21 | Path(
|
||||||
|
22 | '', # EOL comment
|
||||||
|
| ^^ PTH201
|
||||||
|
23 | )
|
||||||
|
|
|
||||||
|
= help: Remove the current directory argument
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
18 18 | '',
|
||||||
|
19 19 | )
|
||||||
|
20 20 |
|
||||||
|
21 |-Path(
|
||||||
|
22 |- '', # EOL comment
|
||||||
|
23 |-)
|
||||||
|
21 |+Path()
|
||||||
|
24 22 |
|
||||||
|
25 23 | Path(
|
||||||
|
26 24 | '' # Comment in the middle of implicitly concatenated string
|
||||||
|
|
||||||
|
PTH201.py:26:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
||||||
|
|
|
||||||
|
25 | Path(
|
||||||
|
26 | / '' # Comment in the middle of implicitly concatenated string
|
||||||
|
27 | | ".",
|
||||||
|
| |_______^ PTH201
|
||||||
|
28 | )
|
||||||
|
|
|
||||||
|
= help: Remove the current directory argument
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
22 22 | '', # EOL comment
|
||||||
|
23 23 | )
|
||||||
|
24 24 |
|
||||||
|
25 |-Path(
|
||||||
|
26 |- '' # Comment in the middle of implicitly concatenated string
|
||||||
|
27 |- ".",
|
||||||
|
28 |-)
|
||||||
|
25 |+Path()
|
||||||
|
29 26 |
|
||||||
|
30 27 | Path(
|
||||||
|
31 28 | '' # Comment before comma
|
||||||
|
|
||||||
|
PTH201.py:31:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
||||||
|
|
|
||||||
|
30 | Path(
|
||||||
|
31 | '' # Comment before comma
|
||||||
|
| ^^ PTH201
|
||||||
|
32 | ,
|
||||||
|
33 | )
|
||||||
|
|
|
||||||
|
= help: Remove the current directory argument
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
27 27 | ".",
|
||||||
|
28 28 | )
|
||||||
|
29 29 |
|
||||||
|
30 |-Path(
|
||||||
|
31 |- '' # Comment before comma
|
||||||
|
32 |- ,
|
||||||
|
33 |-)
|
||||||
|
30 |+Path()
|
||||||
|
34 31 |
|
||||||
|
35 32 | Path(
|
||||||
|
36 33 | '',
|
||||||
|
|
||||||
|
PTH201.py:36:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
||||||
|
|
|
||||||
|
35 | Path(
|
||||||
|
36 | '',
|
||||||
|
| ^^ PTH201
|
||||||
|
37 | ) / "bare"
|
||||||
|
|
|
||||||
|
= help: Remove the current directory argument
|
||||||
|
|
||||||
|
ℹ Safe fix
|
||||||
|
33 33 | )
|
||||||
|
34 34 |
|
||||||
|
35 35 | Path(
|
||||||
|
36 |- '',
|
||||||
|
37 |-) / "bare"
|
||||||
|
36 |+ "bare",
|
||||||
|
37 |+)
|
||||||
|
38 38 |
|
||||||
|
39 39 | Path( # Comment before argument
|
||||||
|
40 40 | '',
|
||||||
|
|
||||||
|
PTH201.py:40:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
||||||
|
|
|
||||||
|
39 | Path( # Comment before argument
|
||||||
|
40 | '',
|
||||||
|
| ^^ PTH201
|
||||||
|
41 | ) / ("parenthesized")
|
||||||
|
|
|
||||||
|
= help: Remove the current directory argument
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
37 37 | ) / "bare"
|
||||||
|
38 38 |
|
||||||
|
39 39 | Path( # Comment before argument
|
||||||
|
40 |- '',
|
||||||
|
41 |-) / ("parenthesized")
|
||||||
|
40 |+ ("parenthesized"),
|
||||||
|
41 |+)
|
||||||
|
42 42 |
|
||||||
|
43 43 | Path(
|
||||||
|
44 44 | '', # EOL comment
|
||||||
|
|
||||||
|
PTH201.py:44:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
||||||
|
|
|
||||||
|
43 | Path(
|
||||||
|
44 | '', # EOL comment
|
||||||
|
| ^^ PTH201
|
||||||
|
45 | ) / ( ("double parenthesized" ) )
|
||||||
|
|
|
||||||
|
= help: Remove the current directory argument
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
41 41 | ) / ("parenthesized")
|
||||||
|
42 42 |
|
||||||
|
43 43 | Path(
|
||||||
|
44 |- '', # EOL comment
|
||||||
|
45 |-) / ( ("double parenthesized" ) )
|
||||||
|
44 |+ ( ("double parenthesized" ) ), # EOL comment
|
||||||
|
45 |+)
|
||||||
|
46 46 |
|
||||||
|
47 47 | ( Path(
|
||||||
|
48 48 | '' # Comment in the middle of implicitly concatenated string
|
||||||
|
|
||||||
|
PTH201.py:48:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
||||||
|
|
|
||||||
|
47 | ( Path(
|
||||||
|
48 | / '' # Comment in the middle of implicitly concatenated string
|
||||||
|
49 | | ".",
|
||||||
|
| |_______^ PTH201
|
||||||
|
50 | ) )/ (("parenthesized path call")
|
||||||
|
51 | # Comment between closing parentheses
|
||||||
|
|
|
||||||
|
= help: Remove the current directory argument
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
44 44 | '', # EOL comment
|
||||||
|
45 45 | ) / ( ("double parenthesized" ) )
|
||||||
|
46 46 |
|
||||||
|
47 |-( Path(
|
||||||
|
48 |- '' # Comment in the middle of implicitly concatenated string
|
||||||
|
49 |- ".",
|
||||||
|
50 |-) )/ (("parenthesized path call")
|
||||||
|
47 |+Path(
|
||||||
|
48 |+ (("parenthesized path call")
|
||||||
|
51 49 | # Comment between closing parentheses
|
||||||
|
50 |+),
|
||||||
|
52 51 | )
|
||||||
|
53 52 |
|
||||||
|
54 53 | Path(
|
||||||
|
|
||||||
|
PTH201.py:55:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
||||||
|
|
|
||||||
|
54 | Path(
|
||||||
|
55 | '' # Comment before comma
|
||||||
|
| ^^ PTH201
|
||||||
|
56 | ,
|
||||||
|
57 | ) / "multiple" / (
|
||||||
|
|
|
||||||
|
= help: Remove the current directory argument
|
||||||
|
|
||||||
|
ℹ Unsafe fix
|
||||||
|
52 52 | )
|
||||||
|
53 53 |
|
||||||
|
54 54 | Path(
|
||||||
|
55 |- '' # Comment before comma
|
||||||
|
55 |+ "multiple" # Comment before comma
|
||||||
|
56 56 | ,
|
||||||
|
57 |-) / "multiple" / (
|
||||||
|
57 |+) / (
|
||||||
|
58 58 | "frag" # Comment
|
||||||
|
59 59 | 'ment'
|
||||||
|
60 60 | )
|
||||||
|
|
||||||
|
PTH201.py:74:15: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
||||||
|
|
|
||||||
|
72 | from importlib.metadata import PackagePath
|
||||||
|
73 |
|
||||||
|
74 | _ = PosixPath(".")
|
||||||
|
| ^^^ PTH201
|
||||||
|
75 | _ = PurePosixPath(".")
|
||||||
|
76 | _ = WindowsPath(".")
|
||||||
|
|
|
||||||
|
= help: Remove the current directory argument
|
||||||
|
|
||||||
|
ℹ Safe fix
|
||||||
|
71 71 |
|
||||||
|
72 72 | from importlib.metadata import PackagePath
|
||||||
|
73 73 |
|
||||||
|
74 |-_ = PosixPath(".")
|
||||||
|
74 |+_ = PosixPath()
|
||||||
|
75 75 | _ = PurePosixPath(".")
|
||||||
|
76 76 | _ = WindowsPath(".")
|
||||||
|
77 77 | _ = PureWindowsPath(".")
|
||||||
|
|
||||||
|
PTH201.py:75:19: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
||||||
|
|
|
||||||
|
74 | _ = PosixPath(".")
|
||||||
|
75 | _ = PurePosixPath(".")
|
||||||
|
| ^^^ PTH201
|
||||||
|
76 | _ = WindowsPath(".")
|
||||||
|
77 | _ = PureWindowsPath(".")
|
||||||
|
|
|
||||||
|
= help: Remove the current directory argument
|
||||||
|
|
||||||
|
ℹ Safe fix
|
||||||
|
72 72 | from importlib.metadata import PackagePath
|
||||||
|
73 73 |
|
||||||
|
74 74 | _ = PosixPath(".")
|
||||||
|
75 |-_ = PurePosixPath(".")
|
||||||
|
75 |+_ = PurePosixPath()
|
||||||
|
76 76 | _ = WindowsPath(".")
|
||||||
|
77 77 | _ = PureWindowsPath(".")
|
||||||
|
78 78 | _ = PackagePath(".")
|
||||||
|
|
||||||
|
PTH201.py:76:17: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
||||||
|
|
|
||||||
|
74 | _ = PosixPath(".")
|
||||||
|
75 | _ = PurePosixPath(".")
|
||||||
|
76 | _ = WindowsPath(".")
|
||||||
|
| ^^^ PTH201
|
||||||
|
77 | _ = PureWindowsPath(".")
|
||||||
|
78 | _ = PackagePath(".")
|
||||||
|
|
|
||||||
|
= help: Remove the current directory argument
|
||||||
|
|
||||||
|
ℹ Safe fix
|
||||||
|
73 73 |
|
||||||
|
74 74 | _ = PosixPath(".")
|
||||||
|
75 75 | _ = PurePosixPath(".")
|
||||||
|
76 |-_ = WindowsPath(".")
|
||||||
|
76 |+_ = WindowsPath()
|
||||||
|
77 77 | _ = PureWindowsPath(".")
|
||||||
|
78 78 | _ = PackagePath(".")
|
||||||
|
|
||||||
|
PTH201.py:77:21: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
||||||
|
|
|
||||||
|
75 | _ = PurePosixPath(".")
|
||||||
|
76 | _ = WindowsPath(".")
|
||||||
|
77 | _ = PureWindowsPath(".")
|
||||||
|
| ^^^ PTH201
|
||||||
|
78 | _ = PackagePath(".")
|
||||||
|
|
|
||||||
|
= help: Remove the current directory argument
|
||||||
|
|
||||||
|
ℹ Safe fix
|
||||||
|
74 74 | _ = PosixPath(".")
|
||||||
|
75 75 | _ = PurePosixPath(".")
|
||||||
|
76 76 | _ = WindowsPath(".")
|
||||||
|
77 |-_ = PureWindowsPath(".")
|
||||||
|
77 |+_ = PureWindowsPath()
|
||||||
|
78 78 | _ = PackagePath(".")
|
||||||
|
|
||||||
|
PTH201.py:78:17: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
||||||
|
|
|
||||||
|
76 | _ = WindowsPath(".")
|
||||||
|
77 | _ = PureWindowsPath(".")
|
||||||
|
78 | _ = PackagePath(".")
|
||||||
|
| ^^^ PTH201
|
||||||
|
|
|
||||||
|
= help: Remove the current directory argument
|
||||||
|
|
||||||
|
ℹ Safe fix
|
||||||
|
75 75 | _ = PurePosixPath(".")
|
||||||
|
76 76 | _ = WindowsPath(".")
|
||||||
|
77 77 | _ = PureWindowsPath(".")
|
||||||
|
78 |-_ = PackagePath(".")
|
||||||
|
78 |+_ = PackagePath()
|
Loading…
Add table
Add a link
Reference in a new issue