Skip to content

Commit a378361

Browse files
authored
Merge pull request #71 from markstory/fix-regression
Fix regressions in cross linking
2 parents 6e244a1 + 820bf5a commit a378361

39 files changed

+3750
-131
lines changed

.github/workflows/ci.yml

Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@ jobs:
1010
runs-on: ubuntu-latest
1111
strategy:
1212
matrix:
13-
# Builds are failing with py3.9+ because sphinx templates are different.
14-
# python: ['3.7', '3.8', '3.9', '3.10', '3.11']
15-
python: ['3.7', '3.8']
13+
python: ['3.10', '3.11', '3.12']
1614
fail-fast: false
1715

1816
permissions:
@@ -33,33 +31,21 @@ jobs:
3331
run: |
3432
python -m pip install --upgrade pip
3533
pip install -r requirements.txt
36-
pip install -r test/requirements.txt
34+
pip install -r requirements-test.txt
3735
pip install .
3836
3937
- name: Build Unit Tests
4038
run: |
41-
cd test
42-
find . -name '*.html' -exec rm {} \;
43-
44-
sed -i 's~, "log\.md"~~' conf.py
45-
make html SPHINXOPTS='' 2>&1 | tee log.txt
46-
git restore conf.py
47-
48-
(cd _build/html && rm genindex.html index.html search.html php-modindex.html)
49-
(cd _build/html && find . -name '*.html' -exec sh -c 'xmllint {} --xpath '"'"'//div[@role="main"]'"'"' | xmllint --format - > ../../{}' \;)
50-
sed -i -r 's~.*/(test/)~\1~;t;d' log.txt
39+
cd test/unit
40+
make clean
41+
make html SPHINXOPTS='-W'
42+
make comparehtml
5143
5244
- name: Apply Coding Style
5345
if: matrix.python == '3.11'
5446
run: |
5547
pip install black
56-
python -m black .
57-
58-
- name: Diff Unit Tests Output and Coding Style
59-
run: |
60-
cd test
61-
rm -r _build
62-
git add . -N && git diff --exit-code
48+
python -m black --diff --check .
6349
6450
- name: Push Unit Tests Output
6551
if: failure() && github.repository_owner != 'markstory' && matrix.python == '3.11'
@@ -73,26 +59,22 @@ jobs:
7359
commit_user_email: bot@example.com
7460
commit_author: Bot <bot@example.com>
7561

76-
- name: Build Unit Tests with '-W' option
62+
- name: Build myst integration tests
7763
run: |
78-
cd test
64+
cd test/myst
7965
make html SPHINXOPTS='-W'
8066
81-
sed -i 's~, "log\.md"~~' conf.py
82-
! make html SPHINXOPTS='-W' || (echo 'Unexpected zero exit code'; false)
83-
git restore conf.py
84-
8567
- name: Build Unit Tests with toc show_parents=hide
8668
run: |
87-
cd test
69+
cd test/unit
8870
make html SPHINXOPTS='-W -D toc_object_entries_show_parents=hide'
8971
9072
- name: Build Unit Tests with toc show_parents=domain
9173
run: |
92-
cd test
74+
cd test/unit
9375
make html SPHINXOPTS='-W -D toc_object_entries_show_parents=domain'
9476
9577
- name: Build Unit Tests with toc show_parents=all
9678
run: |
97-
cd test
79+
cd test/unit
9880
make html SPHINXOPTS='-W -D toc_object_entries_show_parents=all'

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
dist/
22
build/
33
doc/_build
4-
test/_build
4+
test/*/_build
55
*.pyc
66
*.egg-info
77
.DS_Store

requirements-test.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
myst-parser

setup.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@
2828
"""
2929

3030
setup(
31-
name='sphinxcontrib-phpdomain',
32-
version='0.12.0',
33-
url='https://github.com/markstory/sphinxcontrib-phpdomain',
34-
download_url='http://pypi.python.org/pypi/sphinxcontrib-phpdomain',
35-
license='BSD',
36-
author='Mark Story',
37-
author_email='mark@mark-story.com',
38-
description='Sphinx extension to enable documenting PHP code',
31+
name="sphinxcontrib-phpdomain",
32+
version="0.12.0",
33+
url="https://github.com/markstory/sphinxcontrib-phpdomain",
34+
download_url="http://pypi.python.org/pypi/sphinxcontrib-phpdomain",
35+
license="BSD",
36+
author="Mark Story",
37+
author_email="mark@mark-story.com",
38+
description="Sphinx extension to enable documenting PHP code",
3939
long_description=long_desc,
4040
project_urls={
4141
"Documentation": "https://markstory.github.io/sphinxcontrib-phpdomain/",

sphinxcontrib/phpdomain.py

Lines changed: 105 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
"""
2-
Sphinx PHP domain.
3-
4-
The PHP domain. Based off of the rubydomain by SHIBUKAWA Yoshiki
2+
Sphinx PHP domain.
53
6-
:copyright: Copyright 2016 by Mark Story
7-
:license: BSD, see LICENSE for details.
4+
The PHP domain. Based off of the rubydomain by SHIBUKAWA Yoshiki
5+
6+
:copyright: Copyright 2016 by Mark Story
7+
:license: BSD, see LICENSE for details.
88
"""
9+
910
import re
1011
import inspect
1112

@@ -70,9 +71,9 @@ def throw_if_false(fromdocnode, value, message: str):
7071

7172
separators = {
7273
"global": None,
73-
"namespace": None,
74-
"function": None,
75-
"interface": None,
74+
"namespace": NS,
75+
"function": NS,
76+
"interface": NS,
7677
"class": None,
7778
"trait": None,
7879
"enum": None,
@@ -229,17 +230,20 @@ def handle_signature(self, sig, signode):
229230
)
230231
separator = separators[self.objtype]
231232

232-
if "::" in name_prefix:
233+
classname = self.env.temp_data.get("php:class")
234+
# Method declared as Class::methodName
235+
if not classname and "::" in name_prefix:
233236
classname = name_prefix.rstrip("::")
234-
else:
235-
classname = self.env.temp_data.get("php:class")
236237

237-
if self.objtype == "global":
238+
if self.objtype == "global" or self.objtype == "function":
239+
add_module = False
238240
namespace = None
239241
classname = None
240242
fullname = name
241243
else:
244+
add_module = True
242245
if name_prefix:
246+
classname = classname.rstrip("::")
243247
fullname = name_prefix + name
244248

245249
# Currently in a class, but not creating another class,
@@ -259,6 +263,14 @@ def handle_signature(self, sig, signode):
259263
classname = ""
260264
fullname = name
261265

266+
# A leading \ means the name is fully qualified
267+
# and should not inherit the current namespace.
268+
if fullname.startswith(NS) and namespace:
269+
add_module = False
270+
name = name[1:]
271+
fullname = fullname[1:]
272+
namespace = None
273+
262274
signode["namespace"] = namespace
263275
signode["class"] = self.class_name = classname
264276
signode["fullname"] = fullname
@@ -279,15 +291,21 @@ def handle_signature(self, sig, signode):
279291
name_prefix = namespace + NS + name_prefix
280292
signode += addnodes.desc_addname(name_prefix, name_prefix)
281293

282-
elif (
283-
namespace
284-
and not self.env.temp_data.get("php:in_class", False)
285-
and self.env.config.add_module_names
286-
):
287-
nodetext = namespace + NS
288-
signode += addnodes.desc_addname(nodetext, nodetext)
294+
elif add_module and self.env.config.add_module_names:
295+
if self.objtype == "global":
296+
nodetext = ""
297+
signode += addnodes.desc_addname(nodetext, nodetext)
298+
else:
299+
namespace = self.options.get(
300+
"namespace", self.env.temp_data.get("php:namespace")
301+
)
302+
303+
if namespace and not self.env.temp_data.get("php:in_class", False):
304+
nodetext = namespace + NS
305+
signode += addnodes.desc_addname(nodetext, nodetext)
289306

290307
signode += addnodes.desc_name(name, name)
308+
291309
if not arglist:
292310
if self.needs_arglist():
293311
# for callables, add an empty parameter list
@@ -603,26 +621,18 @@ class PhpXRefRole(XRefRole):
603621

604622
def process_link(self, env, refnode, has_explicit_title, title, target):
605623
if not has_explicit_title:
606-
# If the first char is '~' don't display the leading namespace & class.
607-
if target.startswith("~"): # only has a meaning for the title
608-
target = title[1:]
609-
if title.startswith("~"):
610-
title = title[1:]
611-
title = re.sub(r"^[\w\\]+::", "", title)
612-
613-
if title.startswith(NS):
614-
title = title[1:]
624+
if title.startswith("::"):
625+
title = title[2:]
626+
target = target.lstrip("~") # only has a meaning for the title
615627

616-
reftype = refnode.attributes["reftype"]
617-
if reftype == "global":
618-
namespace = None
619-
classname = None
620-
else:
621-
namespace = env.temp_data.get("php:namespace")
622-
classname = env.temp_data.get("php:class")
628+
# If the first char is ~ don't display the leading namespace & class.
629+
if title.startswith("~"):
630+
m = re.search(r"(?:.+[:]{2}|(?:.*?\\{1,2})+)?(.*)\Z", title)
631+
if m:
632+
title = m.group(1)
623633

624-
refnode["php:namespace"] = namespace
625-
refnode["php:class"] = classname
634+
refnode["php:namespace"] = env.temp_data.get("php:namespace")
635+
refnode["php:class"] = env.temp_data.get("php:class")
626636

627637
return title, target
628638

@@ -814,76 +824,79 @@ def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode):
814824
else:
815825
namespace = node.get("php:namespace")
816826
clsname = node.get("php:class")
817-
name, obj = self.find_obj(env, node, namespace, clsname, target, typ)
827+
searchorder = node.hasattr("refspecific") and 1 or 0
828+
name, obj = self.find_obj(
829+
env, node, namespace, clsname, target, typ, searchorder
830+
)
818831
if not obj:
819832
return None
820833
else:
821834
return make_refnode(builder, fromdocname, obj[0], name, contnode, name)
822835

823-
def find_obj(self, env, fromdocnode, namespace, classname, name, type):
836+
def find_obj(
837+
self, env, fromdocnode, namespace, classname, name, type, searchorder=0
838+
):
824839
"""
825840
Find a PHP object for "name", using the given namespace and classname.
826841
"""
827842
# strip parenthesis
828843
if name[-2:] == "()":
829844
name = name[:-2]
830845

846+
if not name:
847+
return None, None
848+
831849
objects = self.data["objects"]
832850

833-
if name.startswith(NS):
834-
absname = name[1:]
851+
newname = None
852+
if searchorder == 1:
853+
if (
854+
namespace
855+
and classname
856+
and namespace + NS + classname + "::" + name in objects
857+
):
858+
newname = namespace + NS + classname + "::" + name
859+
elif namespace and namespace + NS + name in objects:
860+
newname = namespace + NS + name
861+
elif namespace and namespace + NS + name in objects:
862+
newname = namespace + NS + name
863+
elif classname and classname + "::" + name in objects:
864+
newname = classname + "." + name
865+
elif classname and classname + "::$" + name in objects:
866+
newname = classname + "::$" + name
867+
elif name in objects:
868+
newname = name
835869
else:
836-
absname = (namespace + NS if namespace else "") + name
837-
838-
if absname not in objects and name in objects:
839-
# constants/functions can be namespaced, but allow fallback to global namespace the same way as PHP does
840-
name_type = objects[name][1]
841-
if (
842-
(name_type == "function" or name_type == "const")
843-
and NS not in name
844-
and "::" not in name
845-
):
846-
absname = name
847-
else:
848-
if namespace and name.startswith(namespace + NS):
849-
log_info(
850-
fromdocnode,
851-
f"Target {absname} not found - did you mean to write {name[len(namespace + NS):]}?",
852-
)
853-
else:
854-
log_info(
855-
fromdocnode,
856-
f"Target {absname} not found - did you mean to write {NS + name}?",
857-
)
858-
absname = name # fallback for BC, might be removed in the next major release
859-
860-
if absname in objects:
861-
return absname, objects[absname]
862-
863-
# PHP reserved keywords are never resolved using NS and ignore them when not defined
864-
if name not in [
865-
"array",
866-
"bool",
867-
"callable",
868-
"false",
869-
"float",
870-
"int",
871-
"iterable",
872-
"mixed",
873-
"never",
874-
"null",
875-
"object",
876-
"parent",
877-
"resource",
878-
"self",
879-
"static",
880-
"string",
881-
"true",
882-
"void",
883-
]:
884-
log_info(fromdocnode, f"Target {absname} not found")
885-
886-
return None, None
870+
if name in objects:
871+
newname = name
872+
elif classname and classname + "::" + name in objects:
873+
newname = classname + "::" + name
874+
elif classname and classname + "::$" + name in objects:
875+
newname = classname + "::$" + name
876+
elif namespace and namespace + NS + name in objects:
877+
newname = namespace + NS + name
878+
elif (
879+
namespace
880+
and classname
881+
and namespace + NS + classname + "::" + name in objects
882+
):
883+
newname = namespace + NS + classname + "::" + name
884+
elif (
885+
namespace
886+
and classname
887+
and namespace + NS + classname + "::$" + name in objects
888+
):
889+
newname = namespace + NS + classname + "::$" + name
890+
# special case: object methods
891+
elif (
892+
type in ("func", "meth")
893+
and "::" not in name
894+
and "object::" + name in objects
895+
):
896+
newname = "object::" + name
897+
if newname is None:
898+
return None, None
899+
return newname, objects[newname]
887900

888901
def get_objects(self):
889902
for ns, info in self.data["namespaces"].items():
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)