Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

js domain: Generate node_id in the right way #7210

Merged
merged 4 commits into from Feb 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES
Expand Up @@ -23,6 +23,10 @@ Incompatible changes
* Due to the scoping changes for :rst:dir:`productionlist` some uses of
:rst:role:`token` must be modified to include the scope which was previously
ignored.
* #6903: js domain: Internal data structure has changed. Both objects and
modules have node_id for cross reference
* #7210: js domain: Non intended behavior is removed such as ``parseInt_`` links
to ``.. js:function:: parseInt``

Deprecated
----------
Expand Down
118 changes: 73 additions & 45 deletions sphinx/domains/javascript.py
Expand Up @@ -28,12 +28,30 @@
from sphinx.util import logging
from sphinx.util.docfields import Field, GroupedField, TypedField
from sphinx.util.docutils import SphinxDirective
from sphinx.util.nodes import make_refnode
from sphinx.util.nodes import make_id, make_refnode


logger = logging.getLogger(__name__)


def make_old_jsmod_id(modname: str) -> str:
"""Generate old styled node_id for JS modules.

.. note:: Old Styled node_id was used until Sphinx-3.0.
This will be removed in Sphinx-5.0.
"""
return 'module-' + modname


def make_old_jsobj_id(fullname: str) -> str:
"""Generate old styled node_id for JS objects.

.. note:: Old Styled node_id was used until Sphinx-3.0.
This will be removed in Sphinx-5.0.
"""
return fullname.replace('$', '_S_')


class JSObject(ObjectDescription):
"""
Description of a JavaScript object.
Expand Down Expand Up @@ -106,19 +124,23 @@ def add_target_and_index(self, name_obj: Tuple[str, str], sig: str,
signode: desc_signature) -> None:
mod_name = self.env.ref_context.get('js:module')
fullname = (mod_name + '.' if mod_name else '') + name_obj[0]
if fullname not in self.state.document.ids:
signode['names'].append(fullname)
tk0miya marked this conversation as resolved.
Show resolved Hide resolved
signode['ids'].append(fullname.replace('$', '_S_'))
tk0miya marked this conversation as resolved.
Show resolved Hide resolved
self.state.document.note_explicit_target(signode)
node_id = make_id(self.env, self.state.document, '', fullname)
signode['ids'].append(node_id)

domain = cast(JavaScriptDomain, self.env.get_domain('js'))
domain.note_object(fullname, self.objtype, location=signode)
# Assign old styled node_id not to break old hyperlinks (if possible)
# Note: Will be removed in Sphinx-5.0 (RemovedInSphinx50Warning)
old_node_id = make_old_jsobj_id(fullname)
if old_node_id not in self.state.document.ids and old_node_id not in signode['ids']:
signode['ids'].append(old_node_id)

self.state.document.note_explicit_target(signode)

domain = cast(JavaScriptDomain, self.env.get_domain('js'))
domain.note_object(fullname, self.objtype, node_id, location=signode)

indextext = self.get_index_text(mod_name, name_obj)
if indextext:
self.indexnode['entries'].append(('single', indextext,
fullname.replace('$', '_S_'),
'', None))
self.indexnode['entries'].append(('single', indextext, node_id, '', None))

def get_index_text(self, objectname: str, name_obj: Tuple[str, str]) -> str:
name, obj = name_obj
Expand Down Expand Up @@ -249,18 +271,25 @@ def run(self) -> List[Node]:
if not noindex:
domain = cast(JavaScriptDomain, self.env.get_domain('js'))

domain.note_module(mod_name)
node_id = make_id(self.env, self.state.document, 'module', mod_name)
domain.note_module(mod_name, node_id)
# Make a duplicate entry in 'objects' to facilitate searching for
# the module in JavaScriptDomain.find_obj()
domain.note_object(mod_name, 'module', location=(self.env.docname, self.lineno))
domain.note_object(mod_name, 'module', node_id,
location=(self.env.docname, self.lineno))

target = nodes.target('', '', ids=[node_id], ismod=True)

# Assign old styled node_id not to break old hyperlinks (if possible)
# Note: Will be removed in Sphinx-5.0 (RemovedInSphinx50Warning)
old_node_id = make_old_jsmod_id(mod_name)
if old_node_id not in self.state.document.ids and old_node_id not in target['ids']:
target['ids'].append(old_node_id)

targetnode = nodes.target('', '', ids=['module-' + mod_name],
ismod=True)
self.state.document.note_explicit_target(targetnode)
ret.append(targetnode)
self.state.document.note_explicit_target(target)
ret.append(target)
indextext = _('%s (module)') % mod_name
inode = addnodes.index(entries=[('single', indextext,
'module-' + mod_name, '', None)])
inode = addnodes.index(entries=[('single', indextext, node_id, '', None)])
ret.append(inode)
return ret

Expand Down Expand Up @@ -315,47 +344,48 @@ class JavaScriptDomain(Domain):
'mod': JSXRefRole(),
}
initial_data = {
'objects': {}, # fullname -> docname, objtype
'modules': {}, # modname -> docname
'objects': {}, # fullname -> docname, node_id, objtype
'modules': {}, # modname -> docname, node_id
} # type: Dict[str, Dict[str, Tuple[str, str]]]

@property
def objects(self) -> Dict[str, Tuple[str, str]]:
return self.data.setdefault('objects', {}) # fullname -> docname, objtype
def objects(self) -> Dict[str, Tuple[str, str, str]]:
return self.data.setdefault('objects', {}) # fullname -> docname, node_id, objtype

def note_object(self, fullname: str, objtype: str, location: Any = None) -> None:
def note_object(self, fullname: str, objtype: str, node_id: str,
location: Any = None) -> None:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method takes an additional argument node_id. It would be a breaking change...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought other APIs in JS domain are also changed. And I don't have good idea to keep compatibility. So I'm going with this implementation.

if fullname in self.objects:
docname = self.objects[fullname][0]
logger.warning(__('duplicate object description of %s, other instance in %s'),
fullname, docname, location=location)
self.objects[fullname] = (self.env.docname, objtype)
logger.warning(__('duplicate %s description of %s, other %s in %s'),
objtype, fullname, objtype, docname, location=location)
self.objects[fullname] = (self.env.docname, node_id, objtype)

@property
def modules(self) -> Dict[str, str]:
return self.data.setdefault('modules', {}) # modname -> docname
def modules(self) -> Dict[str, Tuple[str, str]]:
return self.data.setdefault('modules', {}) # modname -> docname, node_id

def note_module(self, modname: str) -> None:
self.modules[modname] = self.env.docname
def note_module(self, modname: str, node_id: str) -> None:
self.modules[modname] = (self.env.docname, node_id)

def clear_doc(self, docname: str) -> None:
for fullname, (pkg_docname, _l) in list(self.objects.items()):
for fullname, (pkg_docname, node_id, _l) in list(self.objects.items()):
if pkg_docname == docname:
del self.objects[fullname]
for modname, pkg_docname in list(self.modules.items()):
for modname, (pkg_docname, node_id) in list(self.modules.items()):
if pkg_docname == docname:
del self.modules[modname]

def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None:
# XXX check duplicates
for fullname, (fn, objtype) in otherdata['objects'].items():
for fullname, (fn, node_id, objtype) in otherdata['objects'].items():
if fn in docnames:
self.objects[fullname] = (fn, objtype)
for mod_name, pkg_docname in otherdata['modules'].items():
self.objects[fullname] = (fn, node_id, objtype)
for mod_name, (pkg_docname, node_id) in otherdata['modules'].items():
if pkg_docname in docnames:
self.modules[mod_name] = pkg_docname
self.modules[mod_name] = (pkg_docname, node_id)

def find_obj(self, env: BuildEnvironment, mod_name: str, prefix: str, name: str,
typ: str, searchorder: int = 0) -> Tuple[str, Tuple[str, str]]:
typ: str, searchorder: int = 0) -> Tuple[str, Tuple[str, str, str]]:
if name[-2:] == '()':
name = name[:-2]

Expand Down Expand Up @@ -387,8 +417,7 @@ def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder
name, obj = self.find_obj(env, mod_name, prefix, target, typ, searchorder)
if not obj:
return None
return make_refnode(builder, fromdocname, obj[0],
name.replace('$', '_S_'), contnode, name)
return make_refnode(builder, fromdocname, obj[0], obj[1], contnode, name)

def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder,
target: str, node: pending_xref, contnode: Element
Expand All @@ -398,13 +427,12 @@ def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Bui
name, obj = self.find_obj(env, mod_name, prefix, target, None, 1)
if not obj:
return []
return [('js:' + self.role_for_objtype(obj[1]),
make_refnode(builder, fromdocname, obj[0],
name.replace('$', '_S_'), contnode, name))]
return [('js:' + self.role_for_objtype(obj[2]),
make_refnode(builder, fromdocname, obj[0], obj[1], contnode, name))]

def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]:
for refname, (docname, type) in list(self.objects.items()):
yield refname, refname, type, docname, refname.replace('$', '_S_'), 1
for refname, (docname, node_id, typ) in list(self.objects.items()):
yield refname, refname, typ, docname, node_id, 1

def get_full_qualified_name(self, node: Element) -> str:
modname = node.get('js:module')
Expand All @@ -421,7 +449,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:

return {
'version': 'builtin',
'env_version': 1,
'env_version': 2,
'parallel_read_safe': True,
'parallel_write_safe': True,
}
6 changes: 5 additions & 1 deletion sphinx/util/nodes.py
Expand Up @@ -443,14 +443,18 @@ def make_id(env: "BuildEnvironment", document: nodes.document,
if prefix:
idformat = prefix + "-%s"
else:
idformat = document.settings.id_prefix + "%s"
idformat = (document.settings.id_prefix or "id") + "%s"

# try to generate node_id by *term*
if prefix and term:
node_id = nodes.make_id(idformat % term)
if node_id == prefix:
# *term* is not good to generate a node_id.
node_id = None
elif term:
node_id = nodes.make_id(term)
if node_id == '':
node_id = None # fallback to None

while node_id is None or node_id in document.ids:
node_id = idformat % env.new_serialno(prefix)
Expand Down
57 changes: 32 additions & 25 deletions tests/test_domain_js.py
Expand Up @@ -91,22 +91,22 @@ def test_domain_js_objects(app, status, warning):
assert 'module_b.submodule' in modules
assert 'module_b.submodule' in objects

assert objects['module_a.submodule.ModTopLevel'] == ('module', 'class')
assert objects['module_a.submodule.ModTopLevel.mod_child_1'] == ('module', 'method')
assert objects['module_a.submodule.ModTopLevel.mod_child_2'] == ('module', 'method')
assert objects['module_b.submodule.ModTopLevel'] == ('module', 'class')

assert objects['TopLevel'] == ('roles', 'class')
assert objects['top_level'] == ('roles', 'function')
assert objects['NestedParentA'] == ('roles', 'class')
assert objects['NestedParentA.child_1'] == ('roles', 'function')
assert objects['NestedParentA.any_child'] == ('roles', 'function')
assert objects['NestedParentA.NestedChildA'] == ('roles', 'class')
assert objects['NestedParentA.NestedChildA.subchild_1'] == ('roles', 'function')
assert objects['NestedParentA.NestedChildA.subchild_2'] == ('roles', 'function')
assert objects['NestedParentA.child_2'] == ('roles', 'function')
assert objects['NestedParentB'] == ('roles', 'class')
assert objects['NestedParentB.child_1'] == ('roles', 'function')
assert objects['module_a.submodule.ModTopLevel'][2] == 'class'
assert objects['module_a.submodule.ModTopLevel.mod_child_1'][2] == 'method'
assert objects['module_a.submodule.ModTopLevel.mod_child_2'][2] == 'method'
assert objects['module_b.submodule.ModTopLevel'][2] == 'class'

assert objects['TopLevel'][2] == 'class'
assert objects['top_level'][2] == 'function'
assert objects['NestedParentA'][2] == 'class'
assert objects['NestedParentA.child_1'][2] == 'function'
assert objects['NestedParentA.any_child'][2] == 'function'
assert objects['NestedParentA.NestedChildA'][2] == 'class'
assert objects['NestedParentA.NestedChildA.subchild_1'][2] == 'function'
assert objects['NestedParentA.NestedChildA.subchild_2'][2] == 'function'
assert objects['NestedParentA.child_2'][2] == 'function'
assert objects['NestedParentB'][2] == 'class'
assert objects['NestedParentB.child_1'][2] == 'function'


@pytest.mark.sphinx('dummy', testroot='domain-js')
Expand All @@ -120,21 +120,28 @@ def find_obj(mod_name, prefix, obj_name, obj_type, searchmode=0):

assert (find_obj(None, None, 'NONEXISTANT', 'class') == (None, None))
assert (find_obj(None, None, 'NestedParentA', 'class') ==
('NestedParentA', ('roles', 'class')))
('NestedParentA', ('roles', 'nestedparenta', 'class')))
assert (find_obj(None, None, 'NestedParentA.NestedChildA', 'class') ==
('NestedParentA.NestedChildA', ('roles', 'class')))
('NestedParentA.NestedChildA',
('roles', 'nestedparenta-nestedchilda', 'class')))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good example for new_id. It is regulated to small case and invalid characters are converted to hyphens.

assert (find_obj(None, 'NestedParentA', 'NestedChildA', 'class') ==
('NestedParentA.NestedChildA', ('roles', 'class')))
('NestedParentA.NestedChildA',
('roles', 'nestedparenta-nestedchilda', 'class')))
assert (find_obj(None, None, 'NestedParentA.NestedChildA.subchild_1', 'func') ==
('NestedParentA.NestedChildA.subchild_1', ('roles', 'function')))
('NestedParentA.NestedChildA.subchild_1',
('roles', 'nestedparenta-nestedchilda-subchild-1', 'function')))
assert (find_obj(None, 'NestedParentA', 'NestedChildA.subchild_1', 'func') ==
('NestedParentA.NestedChildA.subchild_1', ('roles', 'function')))
('NestedParentA.NestedChildA.subchild_1',
('roles', 'nestedparenta-nestedchilda-subchild-1', 'function')))
assert (find_obj(None, 'NestedParentA.NestedChildA', 'subchild_1', 'func') ==
('NestedParentA.NestedChildA.subchild_1', ('roles', 'function')))
('NestedParentA.NestedChildA.subchild_1',
('roles', 'nestedparenta-nestedchilda-subchild-1', 'function')))
assert (find_obj('module_a.submodule', 'ModTopLevel', 'mod_child_2', 'meth') ==
('module_a.submodule.ModTopLevel.mod_child_2', ('module', 'method')))
('module_a.submodule.ModTopLevel.mod_child_2',
('module', 'module-a-submodule-modtoplevel-mod-child-2', 'method')))
assert (find_obj('module_b.submodule', 'ModTopLevel', 'module_a.submodule', 'mod') ==
('module_a.submodule', ('module', 'module')))
('module_a.submodule',
('module', 'module-module-a-submodule', 'module')))


def test_get_full_qualified_name():
Expand Down Expand Up @@ -198,7 +205,7 @@ def test_js_class(app):
[desc_parameterlist, ()])],
[desc_content, ()])]))
assert_node(doctree[0], addnodes.index,
entries=[("single", "Application() (class)", "Application", "", None)])
entries=[("single", "Application() (class)", "application", "", None)])
assert_node(doctree[1], addnodes.desc, domain="js", objtype="class", noindex=False)


Expand Down