diff --git a/.gitignore b/.gitignore
index bfa441d1..c1ef3afc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,7 +5,7 @@ __pycache__/
parser/snooty.py
*.dist/
node_modules/
-.coverage
+.coverage*
htmlcov/
.venv/
dist/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9576528a..fda6f6c5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
+- Add validation for links under the `doc` role (DOCSP-6190).
+
- Add support for the following reStructuredText constructs:
- `datalakeconf` rstobject
diff --git a/snooty/parser.py b/snooty/parser.py
index 9ce85686..4c60a23a 100644
--- a/snooty/parser.py
+++ b/snooty/parser.py
@@ -250,6 +250,9 @@ def dispatch_visit(self, node: docutils.nodes.Node) -> None:
doc["label"] = node["label"]
if "target" in node:
doc["target"] = node["target"]
+
+ if doc["name"] == "doc":
+ self.validate_doc_role(node)
elif node_name == "target":
doc["type"] = "target"
doc["ids"] = node["ids"]
@@ -402,7 +405,7 @@ def handle_directive(
):
pass
else:
- msg = f'"{name}" could not open "{argument_text}: No such file exists"'
+ msg = f'"{name}" could not open "{argument_text}": No such file exists'
self.diagnostics.append(Diagnostic.error(msg, util.get_line(node)))
if options:
@@ -411,6 +414,17 @@ def handle_directive(
doc["children"] = []
return True
+ def validate_doc_role(self, node: docutils.nodes.Node) -> None:
+ """Validate target for doc role"""
+ target = PurePath(node["target"]).with_suffix(".txt")
+ fileid, target_path = util.reroot_path(target, self.docpath, self.source_path)
+
+ if not target_path.is_file():
+ msg = (
+ f'"{node["name"]}" could not open "{target_path}": No such file exists'
+ )
+ self.diagnostics.append(Diagnostic.error(msg, util.get_line(node)))
+
def add_static_asset(self, path: Path, upload: bool) -> StaticAsset:
fileid, path = util.reroot_path(path, self.docpath, self.source_path)
static_asset = StaticAsset.load(fileid, path, upload)
diff --git a/snooty/test_parser.py b/snooty/test_parser.py
index 2f4a89e7..0b7d1401 100644
--- a/snooty/test_parser.py
+++ b/snooty/test_parser.py
@@ -393,6 +393,91 @@ def test_roles() -> None:
)
+def test_doc_role() -> None:
+ project_root = ROOT_PATH.joinpath("test_project")
+ path = project_root.joinpath(Path("source/test.rst")).resolve()
+ project_config = ProjectConfig(project_root, "")
+ parser = rstparser.Parser(project_config, JSONVisitor)
+
+ # Test bad text
+ page, diagnostics = parse_rst(
+ parser,
+ path,
+ """
+* :doc:`Testing it `
+* :doc:`Testing this `
+* :doc:`Testing that <./fake-text>`
+* :doc:`fake-text`
+* :doc:`/fake-text`
+* :doc:`./fake-text`
+""",
+ )
+ page.finish(diagnostics)
+ assert len(diagnostics) == 6
+
+ # Test valid text
+ page, diagnostics = parse_rst(
+ parser,
+ path,
+ """
+* :doc:`Testing this `
+* :doc:`Testing that <./../source/index>`
+* :doc:`index`
+* :doc:`/index`
+* :doc:`./../source/index`
+* :doc:`/index/`
+""",
+ )
+ page.finish(diagnostics)
+ print(ast_to_testing_string(page.ast))
+ assert diagnostics == []
+ assert ast_to_testing_string(page.ast) == "".join(
+ (
+ "",
+ "",
+ "",
+ "",
+ '',
+ "",
+ "",
+ "",
+ "",
+ "",
+ '',
+ "",
+ "",
+ "",
+ "",
+ "",
+ '',
+ "",
+ "",
+ "",
+ "",
+ '',
+ "",
+ "",
+ "",
+ "",
+ '',
+ "",
+ "",
+ "",
+ "",
+ '',
+ "",
+ "",
+ "",
+ "
",
+ "",
+ )
+ )
+
+
def test_accidental_indentation() -> None:
path = ROOT_PATH.joinpath(Path("test.rst"))
project_config = ProjectConfig(ROOT_PATH, "", source="./")