Repository: sphinx-doc/sphinx

Issue:
Sphinx does not warn when glossary term appears twice
**Describe the bug**
Sphinx does not warn when glossary term appears twice

**To Reproduce**
Build the following document.
```
.. glossary::

   foo
       description

.. glossary::

   foo
       description
```

Then it succeeded without any errors. And ```:term:`foo` ``` refers last "foo" entry.

**Expected behavior**
Got warned for duplicated term.

**Your project**
None

**Screenshots**
None

**Environment info**
- OS: Mac
- Python version: 3.7.3
- Sphinx version: 2.2.0+
- Sphinx extensions:  None
- Extra tools: None

**Additional context**
None

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/sphinx-doc/sphinx/pull/6560?src=pr&el=h1) Report
> Merging [#6560](https://codecov.io/gh/sphinx-doc/sphinx/pull/6560?src=pr&el=desc) into [master](https://codecov.io/gh/sphinx-doc/sphinx/commit/618cc26c6463b1ad29a1568bfd123b016fc870c4?src=pr&el=desc) will **decrease** coverage by `0.04%`.
> The diff coverage is `87.5%`.

[![Impacted file tree graph](https://codecov.io/gh/sphinx-doc/sphinx/pull/6560/graphs/tree.svg?width=650&token=VFlFSELbkw&height=150&src=pr)](https://codecov.io/gh/sphinx-doc/sphinx/pull/6560?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #6560      +/-   ##
==========================================
- Coverage   84.41%   84.36%   -0.05%     
==========================================
  Files         266      264       -2     
  Lines       40448    40279     -169     
  Branches     5931     5920      -11     
==========================================
- Hits        34144    33982     -162     
+ Misses       4997     4991       -6     
+ Partials     1307     1306       -1
```


| [Impacted Files](https://codecov.io/gh/sphinx-doc/sphinx/pull/6560?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [tests/test\_domain\_std.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/6560/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9kb21haW5fc3RkLnB5) | `96.26% <100%> (-0.11%)` | :arrow_down: |
| [sphinx/domains/std.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/6560/diff?src=pr&el=tree#diff-c3BoaW54L2RvbWFpbnMvc3RkLnB5) | `85.49% <84.61%> (-0.2%)` | :arrow_down: |
| [sphinx/util/images.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/6560/diff?src=pr&el=tree#diff-c3BoaW54L3V0aWwvaW1hZ2VzLnB5) | `66.17% <0%> (-4.14%)` | :arrow_down: |
| [sphinx/project.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/6560/diff?src=pr&el=tree#diff-c3BoaW54L3Byb2plY3QucHk=) | `91.11% <0%> (-4.13%)` | :arrow_down: |
| [sphinx/deprecation.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/6560/diff?src=pr&el=tree#diff-c3BoaW54L2RlcHJlY2F0aW9uLnB5) | `59.09% <0%> (-2.82%)` | :arrow_down: |
| [sphinx/transforms/post\_transforms/\_\_init\_\_.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/6560/diff?src=pr&el=tree#diff-c3BoaW54L3RyYW5zZm9ybXMvcG9zdF90cmFuc2Zvcm1zL19faW5pdF9fLnB5) | `69.81% <0%> (-1.36%)` | :arrow_down: |
| [sphinx/transforms/compact\_bullet\_list.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/6560/diff?src=pr&el=tree#diff-c3BoaW54L3RyYW5zZm9ybXMvY29tcGFjdF9idWxsZXRfbGlzdC5weQ==) | `81.63% <0%> (-1.06%)` | :arrow_down: |
| [sphinx/util/\_\_init\_\_.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/6560/diff?src=pr&el=tree#diff-c3BoaW54L3V0aWwvX19pbml0X18ucHk=) | `72.96% <0%> (-0.75%)` | :arrow_down: |
| [sphinx/transforms/post\_transforms/images.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/6560/diff?src=pr&el=tree#diff-c3BoaW54L3RyYW5zZm9ybXMvcG9zdF90cmFuc2Zvcm1zL2ltYWdlcy5weQ==) | `64.05% <0%> (-0.7%)` | :arrow_down: |
| [sphinx/transforms/post\_transforms/code.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/6560/diff?src=pr&el=tree#diff-c3BoaW54L3RyYW5zZm9ybXMvcG9zdF90cmFuc2Zvcm1zL2NvZGUucHk=) | `94.52% <0%> (-0.55%)` | :arrow_down: |
| ... and [53 more](https://codecov.io/gh/sphinx-doc/sphinx/pull/6560/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/sphinx-doc/sphinx/pull/6560?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/sphinx-doc/sphinx/pull/6560?src=pr&el=footer). Last update [618cc26...9ecb5d8](https://codecov.io/gh/sphinx-doc/sphinx/pull/6560?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/CHANGES b/CHANGES
index e8c0561ab..df590535e 100644
--- a/CHANGES
+++ b/CHANGES
@@ -20,6 +20,8 @@ Incompatible changes
 Deprecated
 ----------
 
+* ``sphinx.domains.std.StandardDomain.add_object()``
+
 Features added
 --------------
 
@@ -27,6 +29,8 @@ Features added
   old stub file
 * #5923: autodoc: ``:inherited-members:`` option takes a name of anchestor class
   not to document inherited members of the class and uppers
+* #6558: glossary: emit a warning for duplicated glossary entry
+* #6558: std domain: emit a warning for duplicated generic objects
 
 Bugs fixed
 ----------
@@ -1971,7 +1975,7 @@ Features removed
 
 * ``termsep`` node
 * defindex.html template
-* LDML format support in `today`, `today_fmt` and `html_last_updated_fmt`
+* LDML format support in ``today``, ``today_fmt`` and ``html_last_updated_fmt``
 * ``:inline:`` option for the directives of sphinx.ext.graphviz extension
 * sphinx.ext.pngmath extension
 * ``sphinx.util.compat.make_admonition()``
@@ -4775,7 +4779,7 @@ Features added
 
   - Added a "nitpicky" mode that emits warnings for all missing
     references.  It is activated by the :option:`sphinx-build -n` command-line
-    switch or the `nitpicky` config value.
+    switch or the :confval:`nitpicky` config value.
   - Added ``latexpdf`` target in quickstart Makefile.
 
 * Markup:
diff --git a/doc/conf.py b/doc/conf.py
index 16594f038..aa513edf8 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -146,6 +146,9 @@ def setup(app):
     app.add_object_type('confval', 'confval',
                         objname='configuration value',
                         indextemplate='pair: %s; configuration value')
+    app.add_object_type('setuptools-confval', 'setuptools-confval',
+                        objname='setuptools configuration value',
+                        indextemplate='pair: %s; setuptools configuration value')
     fdesc = GroupedField('parameter', label='Parameters',
                          names=['param'], can_collapse=True)
     app.add_object_type('event', 'event', 'pair: %s; event', parse_event,
diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst
index 6159ae4f7..52dcddfdb 100644
--- a/doc/extdev/deprecated.rst
+++ b/doc/extdev/deprecated.rst
@@ -26,6 +26,11 @@ The following is a list of deprecated interfaces.
      - (will be) Removed
      - Alternatives
 
+   * - ``sphinx.domains.std.StandardDomain.add_object()``
+     - 3.0
+     - 5.0
+     - ``sphinx.domains.std.StandardDomain.note_object()``
+
    * - ``sphinx.environment.BuildEnvironment.indexentries``
      - 2.4
      - 4.0
diff --git a/doc/usage/advanced/setuptools.rst b/doc/usage/advanced/setuptools.rst
index 10cc6a77d..f4dfb7f66 100644
--- a/doc/usage/advanced/setuptools.rst
+++ b/doc/usage/advanced/setuptools.rst
@@ -57,7 +57,7 @@ Once configured, call this by calling the relevant command on ``setup.py``::
 Options for setuptools integration
 ----------------------------------
 
-.. confval:: fresh-env
+.. setuptools-confval:: fresh-env
 
    A boolean that determines whether the saved environment should be discarded
    on build. Default is false.
@@ -68,7 +68,7 @@ Options for setuptools integration
 
       $ python setup.py build_sphinx -E
 
-.. confval:: all-files
+.. setuptools-confval:: all-files
 
    A boolean that determines whether all files should be built from scratch.
    Default is false.
@@ -79,7 +79,7 @@ Options for setuptools integration
 
       $ python setup.py build_sphinx -a
 
-.. confval:: source-dir
+.. setuptools-confval:: source-dir
 
    The target source directory. This can be relative to the ``setup.py`` or
    ``setup.cfg`` file, or it can be absolute.  It defaults to ``./doc`` or
@@ -92,12 +92,12 @@ Options for setuptools integration
 
       $ python setup.py build_sphinx -s $SOURCE_DIR
 
-.. confval:: build-dir
+.. setuptools-confval:: build-dir
 
    The target build directory. This can be relative to the ``setup.py`` or
    ``setup.cfg`` file, or it can be absolute. Default is ``./build/sphinx``.
 
-.. confval:: config-dir
+.. setuptools-confval:: config-dir
 
    Location of the configuration directory. This can be relative to the
    ``setup.py`` or ``setup.cfg`` file, or it can be absolute. Default is to use
@@ -111,7 +111,7 @@ Options for setuptools integration
 
    .. versionadded:: 1.0
 
-.. confval:: builder
+.. setuptools-confval:: builder
 
    The builder or list of builders to use. Default is ``html``.
 
@@ -124,7 +124,7 @@ Options for setuptools integration
    .. versionchanged:: 1.6
       This can now be a comma- or space-separated list of builders
 
-.. confval:: warning-is-error
+.. setuptools-confval:: warning-is-error
 
    A boolean that ensures Sphinx warnings will result in a failed build.
    Default is false.
@@ -137,32 +137,32 @@ Options for setuptools integration
 
    .. versionadded:: 1.5
 
-.. confval:: project
+.. setuptools-confval:: project
 
    The documented project's name. Default is ``''``.
 
    .. versionadded:: 1.0
 
-.. confval:: version
+.. setuptools-confval:: version
 
    The short X.Y version. Default is ``''``.
 
    .. versionadded:: 1.0
 
-.. confval:: release
+.. setuptools-confval:: release
 
    The full version, including alpha/beta/rc tags. Default is ``''``.
 
    .. versionadded:: 1.0
 
-.. confval:: today
+.. setuptools-confval:: today
 
    How to format the current date, used as the replacement for ``|today|``.
    Default is ``''``.
 
    .. versionadded:: 1.0
 
-.. confval:: link-index
+.. setuptools-confval:: link-index
 
    A boolean that ensures index.html will be linked to the master doc. Default
    is false.
@@ -175,13 +175,13 @@ Options for setuptools integration
 
    .. versionadded:: 1.0
 
-.. confval:: copyright
+.. setuptools-confval:: copyright
 
    The copyright string. Default is ``''``.
 
    .. versionadded:: 1.3
 
-.. confval:: nitpicky
+.. setuptools-confval:: nitpicky
 
    Run in nit-picky mode.  Currently, this generates warnings for all missing
    references.  See the config value :confval:`nitpick_ignore` for a way to
@@ -189,7 +189,7 @@ Options for setuptools integration
 
    .. versionadded:: 1.8
 
-.. confval:: pdb
+.. setuptools-confval:: pdb
 
    A boolean to configure ``pdb`` on exception. Default is false.
 
diff --git a/sphinx/domains/std.py b/sphinx/domains/std.py
index 10ca58c5b..dc35b1c2b 100644
--- a/sphinx/domains/std.py
+++ b/sphinx/domains/std.py
@@ -22,7 +22,7 @@
 
 from sphinx import addnodes
 from sphinx.addnodes import desc_signature, pending_xref
-from sphinx.deprecation import RemovedInSphinx40Warning
+from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning
 from sphinx.directives import ObjectDescription
 from sphinx.domains import Domain, ObjType
 from sphinx.locale import _, __
@@ -81,7 +81,8 @@ def add_target_and_index(self, name: str, sig: str, signode: desc_signature) ->
                                               targetname, '', None))
 
         std = cast(StandardDomain, self.env.get_domain('std'))
-        std.add_object(self.objtype, name, self.env.docname, targetname)
+        std.note_object(self.objtype, name, targetname,
+                        location=(self.env.docname, self.lineno))
 
 
 class EnvVar(GenericObject):
@@ -143,7 +144,7 @@ def run(self) -> List[Node]:
             _, name = self.name.split(':', 1)
 
         std = cast(StandardDomain, self.env.get_domain('std'))
-        std.add_object(name, fullname, self.env.docname, targetname)
+        std.note_object(name, fullname, targetname, location=(self.env.docname, self.lineno))
 
         return ret
 
@@ -261,7 +262,7 @@ def make_glossary_term(env: "BuildEnvironment", textnodes: Iterable[Node], index
     gloss_entries.add(new_id)
 
     std = cast(StandardDomain, env.get_domain('std'))
-    std.add_object('term', termtext.lower(), env.docname, new_id)
+    std.note_object('term', termtext.lower(), new_id, location=(env.docname, lineno))
 
     # add an index entry too
     indexnode = addnodes.index()
@@ -443,7 +444,8 @@ def run(self) -> List[Node]:
                 if idname not in self.state.document.ids:
                     subnode['ids'].append(idname)
                 self.state.document.note_implicit_target(subnode, subnode)
-                domain.add_object('token', subnode['tokenname'], self.env.docname, idname)
+                domain.note_object('token', subnode['tokenname'], idname,
+                                   location=(self.env.docname, self.lineno))
             subnode.extend(token_xrefs(tokens))
             node.append(subnode)
         return [node]
@@ -539,6 +541,23 @@ def __init__(self, env: "BuildEnvironment") -> None:
     def objects(self) -> Dict[Tuple[str, str], Tuple[str, str]]:
         return self.data.setdefault('objects', {})  # (objtype, name) -> docname, labelid
 
+    def note_object(self, objtype: str, name: str, labelid: str, location: Any = None
+                    ) -> None:
+        """Note a generic object for cross reference.
+
+        .. versionadded:: 3.0
+        """
+        if (objtype, name) in self.objects:
+            docname = self.objects[objtype, name][0]
+            logger.warning(__('duplicate %s description of %s, other instance in %s'),
+                           objtype, name, docname, location=location)
+        self.objects[objtype, name] = (self.env.docname, labelid)
+
+    def add_object(self, objtype: str, name: str, docname: str, labelid: str) -> None:
+        warnings.warn('StandardDomain.add_object() is deprecated.',
+                      RemovedInSphinx50Warning)
+        self.objects[objtype, name] = (docname, labelid)
+
     @property
     def progoptions(self) -> Dict[Tuple[str, str], Tuple[str, str]]:
         return self.data.setdefault('progoptions', {})  # (program, name) -> docname, labelid
@@ -620,9 +639,6 @@ def process_doc(self, env: "BuildEnvironment", docname: str, document: nodes.doc
                     continue
             self.labels[name] = docname, labelid, sectname
 
-    def add_object(self, objtype: str, name: str, docname: str, labelid: str) -> None:
-        self.objects[objtype, name] = (docname, labelid)
-
     def add_program_option(self, program: str, name: str, docname: str, labelid: str) -> None:
         self.progoptions[program, name] = (docname, labelid)
 


Test patch:
diff --git a/tests/test_domain_std.py b/tests/test_domain_std.py
index c7a7b496a..db1346bcf 100644
--- a/tests/test_domain_std.py
+++ b/tests/test_domain_std.py
@@ -172,6 +172,15 @@ def test_glossary_warning(app, status, warning):
     assert ("case3.rst:4: WARNING: glossary term must be preceded by empty line"
             in warning.getvalue())
 
+    # duplicated terms
+    text = (".. glossary::\n"
+            "\n"
+            "   term-case4\n"
+            "   term-case4\n")
+    restructuredtext.parse(app, text, "case4")
+    assert ("case4.txt:3: WARNING: duplicate term description of term-case4, "
+            "other instance in case4" in warning.getvalue())
+
 
 def test_glossary_comment(app):
     text = (".. glossary::\n"