Repository: mozilla/sphinx-js (language: python)

Issue:
Introduce an intermediate representation
Currently, we program everything directly against JSDoc's JSON output. Even the TypeScript stuff first converts to JSDoc. But JSDoc's JSON is unspecified, idiosyncratic (i.e. https://github.com/mozilla/sphinx-js/blob/2158457eeeafc0baa7aa86033dbe3d9e314223f6/sphinx_js/doclets.py#L31-L35), and not always the clearest thing in the world to read at callsites.

Let's instead write against an intermediate representation that we control.

Advantages:
1. We can fix any changes JSDoc makes in one place instead of all over the codebase.
2. We can support information that JSDoc doesn't, as for TypeScript or other frontend languages.
3. Our TypeScript module should become a lot more maintainable.
4. Maybe tests will become easier to write.

What bits of info will we need to support in the IR?
* filename
* file path
* longname (to do: define what this is)
* line number (for error reporting)
* params, with their types, names, default values, descriptions, and whether they're variadic
* exceptions a function can raise
* function return type and description
* examples for functions and classes
* whether funcs or classes are deprecated
* see-alsos
* class comments and class-constructor comments (Should constructor comments be special? Probably; I think we merge them currently.)
* members of a class
  * what kind of thing they are (fuction, typedef (currently a hack, I believe, around not having a proper representation of typedefs—we just shove them through the fuction renderer))
  * access (public, private, protected)
* properties (Is this a TS thing?)

That's from my reading of the JS-related source along with a peek at the (handy) docs at the top of typedoc.py about what data are used. Anybody see anything else? The next step is to gin up a class-based API that supports all this.


Discussion / hints:
Sounds good so far. We should strive to make this as generic as possible so that we can add new features more easily in the future.

Types are kind of a pain. They need to be nestable and have properties. Perhaps something like
```
functionTypes = [
 {
    name: 'Promise',
    flags: {
        readonly: true,
    },
    children: [
        {
            name: 'TypeA',
        },
   ],
},
...
]
```

There are also keywords like`extends`, and defaults for generic types, and probably more that I haven't encountered yet. I wonder what's the best way to maintain this, as ts will probably add additional syntax features in the future. Typedoc has a [big collection of tests with sources and output json](https://github.com/TypeStrong/typedoc/tree/master/src/test/converter), maybe we can use that.

> properties (Is this a TS thing?)

It is, so far, but it's also a stage3 proposal for JS itself: https://github.com/tc39/proposal-class-fields


>  I wonder what's the best way to maintain this, as ts will probably add additional syntax features in the future

I had assumed we'd use a class-based object model, with methods behind which we could hide common bits of processing. Then we can always keep adding methods and fields as TS (for example) grows or we grow to support more of its features. I'd definitely represent something as hairy as a type as some kind of method-having object, perhaps containing even more thereof.
Here's a first-draft sketch of a class-based API for the IR. Commentary welcome. The main new thing is the rough division into classes. Spelling doesn't count yet. These are the must-have properties. They may be augmented by or even hidden behind computed convenience properties later.

```
Node:
    .name (the short name of a member, regardless of whether it's a class of function or typedef or param)
    .longname (our namepath-like things)
    .is_documented
    .description
    .filename with extension
    .lineno
    .deprecated
    .examples
    .memberof  # Or maybe Class.members suffices.
    .see_also
    .properties

Anything that can be a class member (function, class, attr, anything else?):
    .access (private, public, protected)

Class or Function:  # Whether this ends up being a common superclass is an implementation detail, but the point is that both functions and classes have these fields.
    .params (classes in TS are type-parametrizable)

Function(Node):
    .exceptions
    .returns

Class(Node):
    (.classdesc can go into Node.description)
    Maybe _sphinxjs_doclets_by_class turns into .members.
    Maybe will want a .constructor.

Typedef(Node?):
    .type

Param(Node):  # Does this subclass Node? It has a name, description, filename, lineno. Probably.
    .defaultvalue
    .is_variadic
    .type # Could be an array of types. Types could even be parametrized by other types, but we probably don't care for IR purposes: let whichever analyzer boil them down to strings.

Unneeded in and of themselves:
    .meta.path (just to implement doclet_full_path)
    .kind (function vs. typedef. Can be represented by node class.)
```

Fix patch:
diff --git a/.gitignore b/.gitignore
index 1e259d64..1b269500 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,8 @@
 /node_modules
 /tests/source/docs/_build
 /tests/test_build/source/_build
+/tests/test_build/source/docs/_build
+/tests/test_build_ts/source/docs/_build
 sphinx_js.egg-info/
 # Python 3
 */__pycache__/*
diff --git a/README.rst b/README.rst
index 6adedd7c..9effe248 100644
--- a/README.rst
+++ b/README.rst
@@ -9,14 +9,20 @@ When you write a JavaScript library, how do you explain it to people? If it's a
 
 sphinx-js lets you use the industry-leading `Sphinx <http://sphinx-doc.org/>`_ documentation tool with JS projects. It provides a handful of directives, patterned after the Python-centric `autodoc <www.sphinx-doc.org/en/latest/ext/autodoc.html>`_ ones, for pulling JSDoc-formatted documentation into reStructuredText pages. And, because you can keep using JSDoc in your code, you remain compatible with the rest of your JS tooling, like Google's Closure Compiler.
 
+sphinx-js also works with TypeScript, using the TypeDoc tool in place of JSDoc and emitting all the type information you would expect.
+
 Setup
 =====
 
-1. Install JSDoc using npm. ``jsdoc`` must be on your ``$PATH``, so you might want to install it globally::
+1. Install JSDoc (or TypeDoc if you're writing TypeScript). The tool must be on your ``$PATH``, so you might want to install it globally::
 
         npm install -g jsdoc
 
-   We work with jsdoc 3.6.3 and quite possibly other versions.
+   ...or... ::
+
+        npm install -g typedoc
+
+   We work with JSDoc 3.6.3 and quite possibly other versions. TypeDoc 0.15.0 is known to work.
 
 2. Install sphinx-js, which will pull in Sphinx itself as a dependency::
 
@@ -55,9 +61,10 @@ Setup
 
         extensions = ['sphinx_js']
 
-5. If your JS source code is anywhere but at the root of your project, add ``js_source_path = '../somewhere/else'`` on a line by itself in conf.py. The root of your JS source tree should be where that setting points, relative to the conf.py file. (The default, ``../``, works well when there is a ``docs`` folder at the root of your project and your source code lives directly inside the root.)
-6. If you have special jsdoc configuration, add ``jsdoc_config_path = '../conf.json'`` (for example) to conf.py as well.
-7. If you're documenting only JS and no other languages, you can set your "primary domain" to JS in conf.py::
+5. If you want to document TypeScript, add ``js_language = 'typescript'`` to conf.py as well.
+6. If your JS source code is anywhere but at the root of your project, add ``js_source_path = '../somewhere/else'`` on a line by itself in conf.py. The root of your JS source tree should be where that setting points, relative to the conf.py file. (The default, ``../``, works well when there is a ``docs`` folder at the root of your project and your source code lives directly inside the root.)
+7. If you have special JSDoc or TypeDoc configuration, add ``jsdoc_config_path = '../conf.json'`` (for example) to conf.py as well.
+8. If you're documenting only JS/TS and no other languages, you can set your "primary domain" to JS in conf.py::
 
         primary_domain = 'js'
 
@@ -181,7 +188,7 @@ You can add private members by saying... ::
        :members:
        :private-members:
 
-Privacy is determined by JSDoc ``@private`` tags.
+Privacy is determined by JSDoc ``@private`` tags or TypeScript's ``private`` keyword.
 
 Exclude certain members by name with ``:exclude-members:``::
 
@@ -264,7 +271,7 @@ For conciseness, you can use any unique suffix, as long as it consists of comple
 
 Things to note:
 
-* We use simple file paths rather than JSDoc's ``module:`` prefix.
+* We use simple file paths rather than JSDoc's ``module:`` prefix or TypeDoc's ``external:`` or ``module:`` ones.
 * We use simple backslash escaping exclusively rather than switching escaping schemes halfway through the path; JSDoc itself `is headed that way as well <https://github.com/jsdoc3/jsdoc/issues/876>`_. The characters that need to be escaped are ``#.~(/``, though you do not need to escape the dots in a leading ``./`` or ``../``. A really horrible path might be... ::
 
     some/path\ with\ spaces/file.topLevelObject#instanceMember.staticMember\(with\(parens
@@ -280,14 +287,14 @@ Saving Keystrokes By Setting The Primary Domain
 
 To save some keystrokes, you can set ``primary_domain = 'js'`` in conf.py and then say (for example) ``autofunction`` rather than ``js:autofunction``.
 
-TypeScript support
-------------------
+TypeScript: Getting Superclass and Interface Links To Work
+----------------------------------------------------------
 
-There is experimental TypeScript support in sphinx-js. Enable it by setting the config variable ``js_language = 'typescript'``. Then, instead of installing JSDoc, install TypeDoc (version 0.15.0 is known to work)::
+To have a class link to its superclasses and implemented interfaces, you'll need to document the superclass (or interface) somewhere using ``js:autoclass`` or ``js:class`` and use the class's full (but dotted) path when you do::
 
-    npm install -g typedoc
+    .. js:autoclass:: someFile.SomeClass
 
-The main difference you'll notice is additional **type** fields in function documentation.
+Unfortunately, Sphinx's ``~`` syntax doesn't work in these spots, so users will see the full paths in the documentation.
 
 Configuration Reference
 -----------------------
@@ -296,16 +303,16 @@ Configuration Reference
   Use 'javascript' or 'typescript' depending on the language you use. The default is 'javascript'.
 
 ``js_source_path``
-  A list of directories to scan (non-recursively) for JS files, relative to Sphinx's conf.py file. Can be a string instead if there is only one. If there is more than one, ``root_for_relative_js_paths`` must be specified as well.
+  A list of directories to scan (non-recursively) for JS or TS source files, relative to Sphinx's conf.py file. Can be a string instead if there is only one. If there is more than one, ``root_for_relative_js_paths`` must be specified as well. Defaults to '../'.
 
 ``jsdoc_config_path``
-  A conf.py-relative path to a jsdoc or typedoc config file, which is useful if you want to specify your own jsdoc options, like recursion and custom filename matching.
+  A conf.py-relative path to a JSDoc config file, which is useful if you want to specify your own JSDoc options, like recursion and custom filename matching. If using TypeDoc, you can also point to a ``tsconfig.json`` file.
 
 ``root_for_relative_js_paths``
   Relative JS entity paths are resolved relative to this path. Defaults to ``js_source_path`` if it is only one item.
 
 ``jsdoc_cache``
-  Path to a file where jsdoc output will be cached. If omitted, jsdoc will be run every time Sphinx is. If you have a large number of source files, it may be beneficial to configure this value. But be careful: the cache is not automatically flushed if your source code changes; you must delete it manually.
+  Path to a file where JSDoc output will be cached. If omitted, JSDoc will be run every time Sphinx is. If you have a large number of source files, it may help to configure this value. But be careful: the cache is not automatically flushed if your source code changes; you must delete it manually.
 
 Example
 =======
@@ -320,7 +327,7 @@ A good example using most of sphinx-js's functionality is the Fathom documentati
 
 Then put the version of sphinx-js you want in ``docs/requirements.txt``. For example... ::
 
-    sphinx-js==3.0
+    sphinx-js==3.1
 
 Or, if you prefer, the Fathom repo carries a `Travis CI configuration <https://github.com/mozilla/fathom/blob/92304b8ad4768e90c167c3d93f9865771f5a6d80/.travis.yml#L41>`_ and a `deployment script <https://github.com/mozilla/fathom/blob/92304b8ad4768e90c167c3d93f9865771f5a6d80/tooling/travis-deploy-docs>`_ for building docs with sphinx-js and publishing them to GitHub Pages. Feel free to borrow them.
 
@@ -333,7 +340,7 @@ Caveats
 Tests
 =====
 
-Run the tests using tox, which will also install jsdoc and typedoc at pinned versions::
+Run the tests using tox, which will also install JSDoc and TypeDoc at pinned versions::
 
     pip install tox
     tox
@@ -341,6 +348,20 @@ Run the tests using tox, which will also install jsdoc and typedoc at pinned ver
 Version History
 ===============
 
+3.1
+  * Re-architect language analysis. There is now a well-documented intermediate representation between JSDoc- and TypeDoc-emitted JSON and the renderers. This should make it much faster to merge PRs.
+  * Rewrite much of the TypeScript analysis engine so it feeds into the new IR.
+
+    * TypeScript analysis used to crash if your codebase contained any overloaded functions. This no longer happens; we now arbitrarily use only the first function signature of each overloaded function.
+    * Add support for static properties on TS classes.
+    * Support variadic args in TS.
+    * Support intersection types (``foo & bar``) in TS.
+    * Remove the "exported from" module links from classes and interfaces. Functions never had them. Let's see if we miss them.
+    * Pathnames for TypeScript objects no longer spuriously use ``~`` after the filename path segment; now they use ``.`` as in JS.
+    * More generally, TS pathnames are now just like JS ones. There is no more ``external:`` prefix in front of filenames or ``module:`` in front of namespace names.
+  * No longer show args in the arg list that are utterly uninformative, lacking both description and type info.
+  * Class attributes are now listed before methods unless manally ordered with ``:members:``.
+
 3.0.1
   * Don't crash when encountering a ``../`` prefix on an object path. This can happen behind the scenes when ``root_for_relative_js_paths`` is set inward of the JS code.
 
@@ -354,14 +375,14 @@ Version History
   * Display generic TypeScript types properly. Make fields come before methods. (Paul Grau)
   * Combine constructor and class documentation at the top TypeScript classes. (Sebastian Weigand)
   * Switch to pytest as the testrunner. (Sebastian Weigand)
-  * Add optional caching of jsdoc output, for large codebases. (Patrick Browne)
+  * Add optional caching of JSDoc output, for large codebases. (Patrick Browne)
   * Fix the display of union types in TypeScript. (Sebastian Weigand)
   * Fix parsing breakage that began in typedoc 0.14.0. (Paul Grau)
   * Fix a data-intake crash with TypeScript. (Cristiano Santos)
 
 2.7.1
   * Fix a crash that would happen sometimes with UTF-8 on Windows. #67.
-  * Always use conf.py's dir for jsdoc's working dir. #78. (Thomas Khyn)
+  * Always use conf.py's dir for JSDoc's working dir. #78. (Thomas Khyn)
 
 2.7
   * Add experimental TypeScript support. (Wim Yedema)
@@ -374,7 +395,7 @@ Version History
 2.5
   * Use documented ``@params`` to help fill out the formal param list for a
     function. This keeps us from missing params that use destructuring. (flozz)
-  * Improve error reporting when jsdoc is missing.
+  * Improve error reporting when JSDoc is missing.
   * Add extracted default values to generated formal param lists. (flozz and erikrose)
 
 2.4
@@ -385,7 +406,7 @@ Version History
   * Fix build-time crash when using recommonmark (for Markdown support) under Sphinx >=1.7.1. (jamrizzi)
 
 2.3.1
-  * Find the jsdoc command on Windows, where it has a different name. Then
+  * Find the ``jsdoc`` command on Windows, where it has a different name. Then
     patch up process communication so it doesn't hang.
 
 2.3
@@ -394,14 +415,14 @@ Version History
 2.2
   * Add ``autofunction`` support for ``@callback`` tags. (krassowski)
   * Add experimental ``autofunction`` support for ``@typedef`` tags. (krassowski)
-  * Add a nice error message for when jsdoc can't find any JS files.
+  * Add a nice error message for when JSDoc can't find any JS files.
   * Pin six more tightly so ``python_2_unicode_compatible`` is sure to be around.
 
 2.1
-  * Allow multiple folders in ``js_source_path``. This is useful for gradually migrating large projects, one folder at a time, to jsdoc. Introduce ``root_for_relative_js_paths`` to keep relative paths unambiguous in the face of multiple source paths.
+  * Allow multiple folders in ``js_source_path``. This is useful for gradually migrating large projects, one folder at a time, to JSDoc. Introduce ``root_for_relative_js_paths`` to keep relative paths unambiguous in the face of multiple source paths.
   * Aggregate PathTaken errors, and report them all at once. This means you don't have to run JSDoc repeatedly while cleaning up large projects.
   * Fix a bytes-vs-strings issue that crashed on versions of Python 3 before 3.6. (jhkennedy)
-  * Tolerate JS files that have filename extensions other than ".js". Before, when combined with custom jsdoc configuration that ingested such files, incorrect object pathnames were generated, which led to spurious "No JSDoc documentation was found for object ..." errors.
+  * Tolerate JS files that have filename extensions other than ".js". Before, when combined with custom JSDoc configuration that ingested such files, incorrect object pathnames were generated, which led to spurious "No JSDoc documentation was found for object ..." errors.
 
 2.0.1
   * Fix spurious syntax errors while loading large JSDoc output by writing it to a temp file first. (jhkennedy)
diff --git a/setup.py b/setup.py
index 5501afca..cc0be32e 100644
--- a/setup.py
+++ b/setup.py
@@ -16,7 +16,7 @@
     url='https://github.com/mozilla/sphinx-js',
     include_package_data=True,
     install_requires=['docutils', 'Jinja2>2.0,<3.0', 'parsimonious>=0.7.0,<0.8.0', 'Sphinx>=3.0.0'],
-    python_requires='>=3.6',
+    python_requires='>=3.7',
     classifiers=[
         'Intended Audience :: Developers',
         'Natural Language :: English',
diff --git a/sphinx_js/__init__.py b/sphinx_js/__init__.py
index 1382c9cd..8749fc11 100644
--- a/sphinx_js/__init__.py
+++ b/sphinx_js/__init__.py
@@ -1,14 +1,19 @@
+from os.path import join, normpath
+
+from sphinx.errors import SphinxError
+
 from .directives import (auto_class_directive_bound_to_app,
                          auto_function_directive_bound_to_app,
                          auto_attribute_directive_bound_to_app)
-from .doclets import gather_doclets
+from .jsdoc import Analyzer as JsAnalyzer
+from .typedoc import Analyzer as TsAnalyzer
 
 
 def setup(app):
     # I believe this is the best place to run jsdoc. I was tempted to use
     # app.add_source_parser(), but I think the kind of source it's referring to
     # is RSTs.
-    app.connect('builder-inited', gather_doclets)
+    app.connect('builder-inited', analyze)
 
     app.connect('env-before-read-docs', read_all_docs)
 
@@ -34,6 +39,48 @@ def setup(app):
     app.add_config_value('root_for_relative_js_paths', None, 'env')
 
 
+def analyze(app):
+    """Run JSDoc or another analysis tool across a whole codebase, and squirrel
+    away its results in a language-specific Analyzer."""
+    # Normalize config values:
+    source_paths = [app.config.js_source_path] if isinstance(app.config.js_source_path, str) else app.config.js_source_path
+    abs_source_paths = [normpath(join(app.confdir, path)) for path in source_paths]
+    root_for_relative_paths = root_or_fallback(
+        normpath(join(app.confdir, app.config.root_for_relative_js_paths)) if app.config.root_for_relative_js_paths else None,
+        abs_source_paths)
+
+    # Pick analyzer:
+    try:
+        analyzer = {'javascript': JsAnalyzer,
+                    'typescript': TsAnalyzer}[app.config.js_language]
+    except KeyError:
+        raise SphinxError('Unsupported value of js_language in config: %s' % app.config.js_language)
+
+    # Analyze source code:
+    app._sphinxjs_analyzer = analyzer.from_disk(abs_source_paths,
+                                                app,
+                                                root_for_relative_paths)
+
+
+def root_or_fallback(root_for_relative_paths, abs_source_paths):
+    """Return the path that relative JS entity paths in the docs are relative to.
+
+    Fall back to the sole JS source path if the setting is unspecified.
+
+    :arg root_for_relative_paths: The absolute-ized root_for_relative_js_paths
+        setting. None if the user hasn't specified it.
+    :arg abs_source_paths: Absolute paths of dirs to scan for JS code
+
+    """
+    if root_for_relative_paths:
+        return root_for_relative_paths
+    else:
+        if len(abs_source_paths) > 1:
+            raise SphinxError('Since more than one js_source_path is specified in conf.py, root_for_relative_js_paths must also be specified. This allows paths beginning with ./ or ../ to be unambiguous.')
+        else:
+            return abs_source_paths[0]
+
+
 def read_all_docs(app, env, doc_names):
     """Add all found docs to the to-be-read list, because we have no way of
     telling which ones reference JS code that might have changed.
diff --git a/sphinx_js/analyzer_utils.py b/sphinx_js/analyzer_utils.py
new file mode 100644
index 00000000..6a4ab2e4
--- /dev/null
+++ b/sphinx_js/analyzer_utils.py
@@ -0,0 +1,72 @@
+"""Conveniences shared among analyzers"""
+
+from functools import wraps
+from json import dump, load
+import os
+
+
+def program_name_on_this_platform(program):
+    """Return the name of the executable file on the current platform, given a
+    command name with no extension."""
+    return program + '.cmd' if os.name == 'nt' else program
+
+
+class Command(object):
+    def __init__(self, program):
+        self.program = program_name_on_this_platform(program)
+        self.args = []
+
+    def add(self, *args):
+        self.args.extend(args)
+
+    def make(self):
+        return [self.program] + self.args
+
+
+def cache_to_file(get_filename):
+    """Return a decorator that will cache the result of ``get_filename()`` to a
+    file
+
+    :arg get_filename: A function which receives the original arguments of the
+        decorated function
+    """
+    def decorator(fn):
+        @wraps(fn)
+        def decorated(*args, **kwargs):
+            filename = get_filename(*args, **kwargs)
+            if filename and os.path.isfile(filename):
+                with open(filename, encoding='utf-8') as f:
+                    return load(f)
+            res = fn(*args, **kwargs)
+            if filename:
+                with open(filename, 'w', encoding='utf-8') as f:
+                    dump(res, f, indent=2)
+            return res
+        return decorated
+    return decorator
+
+
+def is_explicitly_rooted(path):
+    """Return whether a relative path is explicitly rooted relative to the
+    cwd, rather than starting off immediately with a file or folder name.
+
+    It's nice to have paths start with "./" (or "../", "../../", etc.) so, if a
+    user is that explicit, we still find the path in the suffix tree.
+
+    """
+    return path.startswith(('../', './')) or path in ('..', '.')
+
+
+def dotted_path(segments):
+    """Convert a JS object path (``['dir/', 'file/', 'class#',
+    'instanceMethod']``) to a dotted style that Sphinx will better index.
+
+    Strip off any leading relative-dir segments (./ or ../) because they lead
+    to invalid paths like ".....foo". Ultimately, we should thread ``base_dir``
+    into this and construct a full path based on that.
+
+    """
+    segments_without_separators = [s[:-1] for s in segments[:-1]
+                                   if s not in ['./', '../']]
+    segments_without_separators.append(segments[-1])
+    return '.'.join(segments_without_separators)
diff --git a/sphinx_js/doclets.py b/sphinx_js/doclets.py
deleted file mode 100644
index ef6c1707..00000000
--- a/sphinx_js/doclets.py
+++ /dev/null
@@ -1,231 +0,0 @@
-from codecs import getreader, getwriter
-from collections import defaultdict
-from errno import ENOENT
-from functools import wraps
-from json import load, dump
-import os
-from os.path import join, normpath, relpath, splitext, sep
-import subprocess
-from tempfile import TemporaryFile, NamedTemporaryFile
-
-from sphinx.errors import SphinxError
-
-from .parsers import path_and_formal_params, PathVisitor
-from .suffix_tree import PathTaken, SuffixTree
-from .typedoc import parse_typedoc
-
-
-def gather_doclets(app):
-    """Run JSDoc or another analysis tool across a whole codebase, and squirrel
-    away its results in jsdoc doclet format."""
-    source_paths = [app.config.js_source_path] if isinstance(app.config.js_source_path, str) else app.config.js_source_path
-    abs_source_paths = [normpath(join(app.confdir, path)) for path in source_paths]
-
-    root_for_relative_paths = root_or_fallback(
-        normpath(join(app.confdir, app.config.root_for_relative_js_paths)) if app.config.root_for_relative_js_paths else None,
-        abs_source_paths)
-
-    analyze = analyzer_for(app.config.js_language)
-    doclets = analyze(abs_source_paths, app)
-    # 2 doclets are made for classes, and they are largely redundant: one for
-    # the class itself and another for the constructor. However, the
-    # constructor one gets merged into the class one and is intentionally
-    # marked as undocumented, even if it isn't. See
-    # https://github.com/jsdoc3/jsdoc/issues/1129.
-    doclets = [d for d in doclets if d.get('comment')
-                                     and not d.get('undocumented')]
-
-    # Build table for lookup by name, which most directives use:
-    app._sphinxjs_doclets_by_path = SuffixTree()
-    conflicts = []
-    for d in doclets:
-        try:
-            app._sphinxjs_doclets_by_path.add(
-                doclet_full_path(d, root_for_relative_paths),
-                d)
-        except PathTaken as conflict:
-            conflicts.append(conflict.segments)
-    if conflicts:
-        raise PathsTaken(conflicts)
-
-    # Build lookup table for autoclass's :members: option. This will also
-    # pick up members of functions (inner variables), but it will instantly
-    # filter almost all of them back out again because they're undocumented.
-    # We index these by unambiguous full path. Then, when looking them up by
-    # arbitrary name segment, we disambiguate that first by running it through
-    # the suffix tree above. Expect trouble due to jsdoc's habit of calling
-    # things (like ES6 class methods) "<anonymous>" in the memberof field, even
-    # though they have names. This will lead to multiple methods having each
-    # other's members. But if you don't have same-named inner functions or
-    # inner variables that are documented, you shouldn't have trouble.
-    app._sphinxjs_doclets_by_class = defaultdict(lambda: [])
-    for d in doclets:
-        of = d.get('memberof')
-        if of:  # speed optimization
-            segments = doclet_full_path(d, root_for_relative_paths, longname_field='memberof')
-            app._sphinxjs_doclets_by_class[tuple(segments)].append(d)
-
-
-def program_name_on_this_platform(program):
-    """Return the name of the executable file on the current platform, given a
-    command name with no extension."""
-    return program + '.cmd' if os.name == 'nt' else program
-
-
-class Command(object):
-    def __init__(self, program):
-        self.program = program_name_on_this_platform(program)
-        self.args = []
-
-    def add(self, *args):
-        self.args.extend(args)
-
-    def make(self):
-        return [self.program] + self.args
-
-
-def cache_to_file(get_filename):
-    """Return a decorator that will cache the result of ``get_filename()`` to a
-    file
-
-    :arg get_filename: A function which receives the original arguments of the
-        decorated function
-    """
-    def decorator(fn):
-        @wraps(fn)
-        def decorated(*args, **kwargs):
-            filename = get_filename(*args, **kwargs)
-            if filename and os.path.isfile(filename):
-                with open(filename, encoding='utf-8') as f:
-                    return load(f)
-            res = fn(*args, **kwargs)
-            if filename:
-                with open(filename, 'w', encoding='utf-8') as f:
-                    dump(res, f, indent=2)
-            return res
-        return decorated
-    return decorator
-
-
-@cache_to_file(lambda abs_source_paths, app: getattr(app.config, 'jsdoc_cache', None))
-def analyze_jsdoc(abs_source_paths, app):
-    command = Command('jsdoc')
-    command.add('-X', *abs_source_paths)
-    if app.config.jsdoc_config_path:
-        command.add('-c', normpath(join(app.confdir, app.config.jsdoc_config_path)))
-
-    # Use a temporary file to handle large output volume. JSDoc defaults to
-    # utf8-encoded output.
-    with getwriter('utf-8')(TemporaryFile(mode='w+b')) as temp:
-        try:
-            p = subprocess.Popen(command.make(), cwd=app.confdir, stdout=temp)
-        except OSError as exc:
-            if exc.errno == ENOENT:
-                raise SphinxError('%s was not found. Install it using "npm install -g jsdoc".' % command.program)
-            else:
-                raise
-        p.wait()
-        # Once output is finished, move back to beginning of file and load it:
-        temp.seek(0)
-        try:
-            return load(getreader('utf-8')(temp))
-        except ValueError:
-            raise SphinxError('jsdoc found no JS files in the directories %s. Make sure js_source_path is set correctly in conf.py. It is also possible (though unlikely) that jsdoc emitted invalid JSON.' % abs_source_paths)
-
-
-def analyze_typescript(abs_source_paths, app):
-    command = Command('typedoc')
-    if app.config.jsdoc_config_path:
-        command.add('--tsconfig', normpath(join(app.confdir, app.config.jsdoc_config_path)))
-
-    with NamedTemporaryFile(mode='w+b') as temp:
-        command.add('--json', temp.name, *abs_source_paths)
-        try:
-            subprocess.call(command.make())
-        except OSError as exc:
-            if exc.errno == ENOENT:
-                raise SphinxError('%s was not found. Install it using "npm install -g typedoc".' % command.program)
-            else:
-                raise
-        # typedoc emits a valid JSON file even if it finds no TS files in the dir:
-        return parse_typedoc(getreader('utf-8')(temp))
-
-
-ANALYZERS = {'javascript': analyze_jsdoc,
-             'typescript': analyze_typescript}
-
-
-def analyzer_for(language):
-    """Return a callable that spits out JSDoc-style doclets from some language:
-    JS, TypeScript, or other."""
-    try:
-        return ANALYZERS[language]
-    except KeyError:
-        raise SphinxError('Unsupported value of js_language in config: %s' % language)
-
-
-def root_or_fallback(root_for_relative_paths, abs_source_paths):
-    """Return the path that relative JS entity paths in the docs are relative to.
-
-    Fall back to the sole JS source path if the setting is unspecified.
-
-    :arg root_for_relative_paths: The absolute-ized root_for_relative_js_paths
-        setting. None if the user hasn't specified it.
-    :arg abs_source_paths: Absolute paths of dirs to scan for JS code
-
-    """
-    if root_for_relative_paths:
-        return root_for_relative_paths
-    else:
-        if len(abs_source_paths) > 1:
-            raise SphinxError('Since more than one js_source_path is specified in conf.py, root_for_relative_js_paths must also be specified. This allows paths beginning with ./ or ../ to be unambiguous.')
-        else:
-            return abs_source_paths[0]
-
-
-def doclet_full_path(d, base_dir, longname_field='longname'):
-    """Return the full, unambiguous list of path segments that points to an
-    entity described by a doclet.
-
-    Example: ``['./', 'dir/', 'dir/', 'file/', 'object.', 'object#', 'object']``
-
-    :arg d: The doclet
-    :arg base_dir: Absolutized value of the root_for_relative_js_paths option
-    :arg longname_field: The field to look in at the top level of the doclet
-        for the long name of the object to emit a path to
-    """
-    meta = d['meta']
-    rel = relpath(meta['path'], base_dir)
-    rel = '/'.join(rel.split(sep))
-    if not rel.startswith(('../', './')) and rel not in ('..', '.'):
-        # It just starts right out with the name of a folder in the base_dir.
-        rooted_rel = './%s' % rel
-    else:
-        rooted_rel = rel
-
-    # Building up a string and then parsing it back down again is probably
-    # not the fastest approach, but it means knowledge of path format is in
-    # one place: the parser.
-    path = '%s/%s.%s' % (rooted_rel,
-                         splitext(meta['filename'])[0],
-                         d[longname_field])
-    return PathVisitor().visit(
-        path_and_formal_params['path'].parse(path))
-
-
-class PathsTaken(Exception):
-    """One or more JS objects had the same paths.
-
-    Rolls up multiple PathTaken exceptions for mass reporting.
-
-    """
-    def __init__(self, conflicts):
-        # List of paths, each given as a list of segments:
-        self.conflicts = conflicts
-
-    def __str__(self):
-        return ('Your JS code contains multiple documented objects at each of '
-                "these paths:\n\n  %s\n\nWe won't know which one you're "
-                'talking about. Using JSDoc tags like @class might help you '
-                'differentiate them.' %
-                '\n  '.join(''.join(c) for c in self.conflicts))
diff --git a/sphinx_js/ir.py b/sphinx_js/ir.py
new file mode 100644
index 00000000..4e283195
--- /dev/null
+++ b/sphinx_js/ir.py
@@ -0,0 +1,226 @@
+"""Intermediate representation that JS and TypeScript are transformed to for
+use by the rest of sphinx-js
+
+This results from my former inability to review any but the most trivial
+TypeScript PRs due to jsdoc's JSON output format being undocumented, often
+surprising, and occasionally changing.
+
+This IR is not intended to be a lossless representation of either jsdoc's or
+typedoc's output. Nor is it meant to generalize to other uses like static
+analysis. Ideally, it exists to provide the minimum information necessary to
+render our Sphinx templates about JS and TS entities. Any expansion or
+generalization of the IR should be driven by needs of those templates and the
+(minimal) logic around them. The complexity of doing otherwise has no payback.
+
+I was conflicted about introducing an additional representation, since a
+multiplicity of representations incurs conversion complexity costs at a
+superlinear rate. However, I think it is essential complexity here. The
+potentially simpler approach would have been to let the RST template vars
+required by our handful of directives be the IR. However, we still would have
+wanted to factor out formatting like the joining of types with "|" and the
+unwrapping of comments, making another representation inevitable. Therefore,
+let's at least have a well-documented one and one slightly more likely to
+survive template changes.
+
+"""
+from dataclasses import dataclass, InitVar
+from typing import Any, List, NewType, Optional, Union
+
+from .analyzer_utils import dotted_path
+
+
+#: Human-readable type of a value. None if we don't know the type.
+Type = NewType('Type', Optional[str])
+# In the far future, we may take full control of our RST templates rather than
+# using the js-domain directives provided by Sphinx. This would give us the
+# freedom to link type names in formal param lists and param description lists
+# to their definitions. To do this, we could replace the string-based Type with
+# a class-based Type which internally preserves the structure of the type
+# (simple for JS, fancy for TS) and can, on request, render it out as either
+# text or link-having RST.
+
+#: Pathname, full or not, to an object:
+ReStructuredText = NewType('ReStructuredText', str)
+
+
+class Pathname:
+    """A partial or full path to a language entity.
+
+    Example: ``['./', 'dir/', 'dir/', 'file.', 'object.', 'object#', 'object']``
+
+    """
+    def __init__(self, segments):
+        self.segments = segments
+
+    def __str__(self):
+        return ''.join(self.segments)
+
+    def __repr__(self):
+        return '<Pathname(%r)>' % self.segments
+
+    def __eq__(self, other):
+        return isinstance(other, self.__class__) and self.segments == other.segments
+
+    def dotted(self):
+        return dotted_path(self.segments)
+
+
+class _NoDefault:
+    """A conspicuous no-default value that will show up in templates to help
+    troubleshoot code paths that grab ``Param.default`` without checking
+    ``Param.has_default`` first."""
+    def __repr__(self):
+        return '<no default value>'
+NO_DEFAULT = _NoDefault()
+
+
+@dataclass
+class _Member:
+    """An IR object that is a member of another, as a method is a member of a
+    class or interface"""
+    #: Whether this member is required to be provided by a subclass of a class
+    #: or implementor of an interface
+    is_abstract: bool
+    #: Whether this member is optional in the TypeScript sense of being allowed
+    #: on but not required of an object to conform to a type
+    is_optional: bool
+    #: Whether this member can be accessed on the container itself rather than
+    #: just on instances of it
+    is_static: bool
+    #: Is a private member of a class or, at least in JSDoc, a @namespace:
+    is_private: bool
+
+
+@dataclass
+class Param:
+    """A parameter of either a function or (in the case of TS, which has
+    classes parametrized by type) a class."""
+    name: str
+    description: ReStructuredText = ''
+    has_default: bool = False
+    is_variadic: bool = False
+    type: Type = None
+    #: Return the default value of this parameter, string-formatted so it can
+    #: be immediately suffixed to an equal sign in a formal param list. For
+    #: example, the number 6 becomes the string "6" to create ``foo=6``. If
+    #: has_default=True, this must be set.
+    default: InitVar[Any] = NO_DEFAULT  # noqa: flake8 thinks this is a "def".
+
+    def __post_init__(self, default):
+        if self.has_default and default is NO_DEFAULT:
+            raise ValueError('Tried to construct a Param with has_default=True but without `default` specified.')
+        self.default = default
+
+
+@dataclass
+class Exc:
+    """One kind of exception that can be raised by a function"""
+    #: The type of exception can have
+    type: Type
+    description: ReStructuredText
+
+
+@dataclass
+class Return:
+    """One kind of thing a function can return"""
+    #: The type this kind of return value can have
+    type: Type
+    description: ReStructuredText
+
+
+@dataclass
+class TopLevel:
+    """A language object with an independent existence
+
+    A TopLevel entity is a potentially strong entity in the database sense; one
+    of these can exist on its own and not merely as a datum attached to another
+    entity. For example, Returns do not qualify, since they cannot exist
+    without a parent Function. And, though a given Attribute may be attached to
+    a Class, Attributes can also exist top-level in a module.
+
+    These are also complex entities: the sorts of thing with the potential to
+    include the kinds of subentities referenced by the fields defined herein.
+
+    """
+    #: The short name of the object, regardless of whether it's a class or
+    #: function or typedef or param.
+    #:
+    #: This is usually the same as the last item of path.segments but not
+    #: always. For example, in JSDoc Attributes defined with @property, name is
+    #: defined but path is empty. This was a shortcut and could be corrected at
+    #: some point. If it is, we can stop storing name as a separate field. Also
+    #: TypeScript constructors are named "new WhateverClass". They should
+    #: instead be called "constructor".
+    name: str
+    #: The namepath-like unambiguous identifier of the object, e.g. ``['./',
+    #: 'dir/', 'dir/', 'file/', 'object.', 'object#', 'object']``
+    path: Pathname
+    #: The basename of the file the object is from, e.g. "foo.js"
+    filename: str
+    #: The human-readable description of the entity or '' if absent
+    description: ReStructuredText
+    #: Line number where the object (exluding any prefixing comment) begins
+    line: int
+    #: Explanation of the deprecation (which implies True) or True or False
+    deprecated: Union[ReStructuredText, bool]
+    #: List of preformatted textual examples
+    examples: List[str]
+    #: List of paths to also refer the reader to
+    see_alsos: List[str]
+    #: Explicitly documented sub-properties of the object, a la jsdoc's @properties
+    properties: List['Attribute']
+    #: None if not exported for use by outside code. Otherwise, the Sphinx
+    #: dotted path to the module it is exported from, e.g. 'foo.bar'
+    exported_from: Optional[Pathname]
+
+
+@dataclass
+class Attribute(TopLevel, _Member):
+    """A property of an object
+
+    These are called attributes to match up with Sphinx's autoattribute
+    directive which is used to display them.
+
+    """
+    #: The type this property's value can have
+    type: Type
+
+
+@dataclass
+class Function(TopLevel, _Member):
+    params: List[Param]
+    exceptions: List[Exc]  # noqa: Linter is buggy.
+    returns: List[Return]
+
+
+@dataclass
+class _MembersAndSupers:
+    """An IR object that can contain members and extend other types"""
+    #: Class members, concretized ahead of time for simplicity. (Otherwise,
+    #: we'd have to pass the doclets_by_class map in and keep it around, along
+    #: with a callable that would create the member IRs from it on demand.)
+    #: Does not include the default constructor.
+    members: List[Union[Function, Attribute]]
+    #: Objects this one extends: for example, superclasses of a class or
+    #: superinterfaces of an interface
+    supers: List[Pathname]
+
+
+@dataclass
+class Interface(TopLevel, _MembersAndSupers):
+    """An interface, a la TypeScript"""
+
+
+@dataclass
+class Class(TopLevel, _MembersAndSupers):
+    #: The default constructor for this class. Absent if the constructor is
+    #: inherited.
+    constructor: Optional[Function]
+    #: Whether this is an abstract class
+    is_abstract: bool
+    #: Interfaces this class implements
+    interfaces: List[Pathname]
+    # There's room here for additional fields like @example on the class doclet
+    # itself. These are supported and extracted by jsdoc, but they end up in an
+    # `undocumented: True` doclet and so are presently filtered out. But we do
+    # have the space to include them someday.
diff --git a/sphinx_js/jsdoc.py b/sphinx_js/jsdoc.py
new file mode 100644
index 00000000..a3d293b2
--- /dev/null
+++ b/sphinx_js/jsdoc.py
@@ -0,0 +1,374 @@
+"""JavaScript analyzer
+
+Analyzers run jsdoc or typedoc or whatever, squirrel away their output, and
+then lazily constitute IR objects as requested.
+
+"""
+from codecs import getreader, getwriter
+from collections import defaultdict
+from errno import ENOENT
+from json import load, dumps
+from os.path import join, normpath, relpath, splitext, sep
+from re import sub
+import subprocess
+from tempfile import TemporaryFile
+
+from sphinx.errors import SphinxError
+
+from .analyzer_utils import cache_to_file, Command, is_explicitly_rooted
+from .ir import Attribute, Class, Exc, Function, NO_DEFAULT, Param, Pathname, Return
+from .parsers import path_and_formal_params, PathVisitor
+from .suffix_tree import SuffixTree
+
+
+class Analyzer:
+    """A runner of a langauge-specific static analysis tool and translator of
+    the results to our IR
+
+    """
+    def __init__(self, json, base_dir):
+        """Index and squirrel away the JSON for later lazy conversion to IR
+        objects.
+
+        :arg json: The loaded JSON output from jsdoc
+        :arg base_dir: Resolve paths in the JSON relative to this directory.
+            This must be an absolute pathname.
+
+        """
+        self._base_dir = base_dir
+        # 2 doclets are made for classes, and they are largely redundant: one
+        # for the class itself and another for the constructor. However, the
+        # constructor one gets merged into the class one and is intentionally
+        # marked as undocumented, even if it isn't. See
+        # https://github.com/jsdoc3/jsdoc/issues/1129.
+        doclets = [doclet for doclet in json if doclet.get('comment') and
+                                                not doclet.get('undocumented')]
+
+        # Build table for lookup by name, which most directives use:
+        self._doclets_by_path = SuffixTree()
+        self._doclets_by_path.add_many((full_path_segments(d, base_dir), d)
+                                       for d in doclets)
+
+        # Build lookup table for autoclass's :members: option. This will also
+        # pick up members of functions (inner variables), but it will instantly
+        # filter almost all of them back out again because they're
+        # undocumented. We index these by unambiguous full path. Then, when
+        # looking them up by arbitrary name segment, we disambiguate that first
+        # by running it through the suffix tree above. Expect trouble due to
+        # jsdoc's habit of calling things (like ES6 class methods)
+        # "<anonymous>" in the memberof field, even though they have names.
+        # This will lead to multiple methods having each other's members. But
+        # if you don't have same-named inner functions or inner variables that
+        # are documented, you shouldn't have trouble.
+        self._doclets_by_class = defaultdict(lambda: [])
+        for d in doclets:
+            of = d.get('memberof')
+            if of:  # speed optimization
+                segments = full_path_segments(d, base_dir, longname_field='memberof')
+                self._doclets_by_class[tuple(segments)].append(d)
+
+    @classmethod
+    def from_disk(cls, abs_source_paths, app, base_dir):
+        json = jsdoc_output(getattr(app.config, 'jsdoc_cache', None),
+                            abs_source_paths,
+                            base_dir,
+                            app.confdir,
+                            getattr(app.config, 'jsdoc_config_path', None))
+        return cls(json, base_dir)
+
+    def get_object(self, path_suffix, as_type):
+        """Return the IR object with the given path suffix.
+
+        If helpful, use the ``as_type`` hint, which identifies which autodoc
+        directive the user called.
+
+        """
+        # Design note: Originally, I had planned to eagerly convert all the
+        # doclets to the IR. But it's hard to tell unambiguously what kind
+        # each doclet is, at least in the case of jsdoc. If instead we lazily
+        # convert each doclet as it's referenced by an autodoc directive, we
+        # can use the hint we previously did: the user saying "this is a
+        # function (by using autofunction on it)", "this is a class", etc.
+        # Additionally, being lazy lets us avoid converting unused doclets
+        # altogether.
+        try:
+            doclet_as_whatever = {
+                'function': self._doclet_as_function,
+                'class': self._doclet_as_class,
+                'attribute': self._doclet_as_attribute}[as_type]
+        except KeyError:
+            raise NotImplementedError('Unknown autodoc directive: auto%s' % as_type)
+
+        doclet, full_path = self._doclets_by_path.get_with_path(path_suffix)
+        return doclet_as_whatever(doclet, full_path)
+
+    def _doclet_as_class(self, doclet, full_path):
+        # This is an instance method so it can get at the base dir.
+        members = []
+        for member_doclet in self._doclets_by_class[tuple(full_path)]:
+            kind = member_doclet.get('kind')
+            member_full_path = full_path_segments(member_doclet, self._base_dir)
+            # Typedefs should still fit into function-shaped holes:
+            doclet_as_whatever = self._doclet_as_function if (kind == 'function' or kind == 'typedef') else self._doclet_as_attribute
+            member = doclet_as_whatever(member_doclet, member_full_path)
+            members.append(member)
+        return Class(
+            description=doclet.get('classdesc', ''),
+            supers=[],  # Could implement for JS later.
+            exported_from=None,  # Could implement for JS later.
+            is_abstract=False,
+            interfaces=[],
+            # Right now, a class generates several doclets, all but one of
+            # which are marked as undocumented. In the one that's left, most of
+            # the fields are about the default constructor:
+            constructor=self._doclet_as_function(doclet, full_path),
+            members=members,
+            **top_level_properties(doclet, full_path))
+
+    @staticmethod
+    def _doclet_as_function(doclet, full_path):
+        return Function(
+            description=unwrapped_description(doclet),
+            exported_from=None,
+            is_abstract=False,
+            is_optional=False,
+            is_static=False,
+            is_private=is_private(doclet),
+            exceptions=exceptions_to_ir(doclet.get('exceptions', [])),
+            returns=returns_to_ir(doclet.get('returns', [])),
+            params=params_to_ir(doclet),
+            **top_level_properties(doclet, full_path))
+
+    @staticmethod
+    def _doclet_as_attribute(doclet, full_path):
+        return Attribute(
+            description=unwrapped_description(doclet),
+            exported_from=None,
+            is_abstract=False,
+            is_optional=False,
+            is_static=False,
+            is_private=is_private(doclet),
+            type=get_type(doclet),
+            **top_level_properties(doclet, full_path)
+        )
+
+
+def is_private(doclet):
+    return doclet.get('access') == 'private'
+
+
+def full_path_segments(d, base_dir, longname_field='longname'):
+    """Return the full, unambiguous list of path segments that points to an
+    entity described by a doclet.
+
+    Example: ``['./', 'dir/', 'dir/', 'file.', 'object.', 'object#', 'object']``
+
+    :arg d: The doclet
+    :arg base_dir: Absolutized value of the root_for_relative_js_paths option
+    :arg longname_field: The field to look in at the top level of the doclet
+        for the long name of the object to emit a path to
+    """
+    meta = d['meta']
+    rel = relpath(meta['path'], base_dir)
+    rel = '/'.join(rel.split(sep))
+    rooted_rel = rel if is_explicitly_rooted(rel) else './%s' % rel
+
+    # Building up a string and then parsing it back down again is probably
+    # not the fastest approach, but it means knowledge of path format is in
+    # one place: the parser.
+    path = '%s/%s.%s' % (rooted_rel,
+                         splitext(meta['filename'])[0],
+                         d[longname_field])
+    return PathVisitor().visit(
+        path_and_formal_params['path'].parse(path))
+
+
+@cache_to_file(lambda cache, *args: cache)
+def jsdoc_output(cache, abs_source_paths, base_dir, sphinx_conf_dir, config_path=None):
+    command = Command('jsdoc')
+    command.add('-X', *abs_source_paths)
+    if config_path:
+        command.add('-c', normpath(join(sphinx_conf_dir, config_path)))
+
+    # Use a temporary file to handle large output volume. JSDoc defaults to
+    # utf8-encoded output.
+    with getwriter('utf-8')(TemporaryFile(mode='w+b')) as temp:
+        try:
+            p = subprocess.Popen(command.make(), cwd=sphinx_conf_dir, stdout=temp)
+        except OSError as exc:
+            if exc.errno == ENOENT:
+                raise SphinxError('%s was not found. Install it using "npm install -g jsdoc".' % command.program)
+            else:
+                raise
+        p.wait()
+        # Once output is finished, move back to beginning of file and load it:
+        temp.seek(0)
+        try:
+            return load(getreader('utf-8')(temp))
+        except ValueError:
+            raise SphinxError('jsdoc found no JS files in the directories %s. Make sure js_source_path is set correctly in conf.py. It is also possible (though unlikely) that jsdoc emitted invalid JSON.' % abs_source_paths)
+
+
+def format_default_according_to_type_hints(value, declared_types, first_type_is_string):
+    """Return the default value for a param, formatted as a string
+    ready to be used in a formal parameter list.
+
+    JSDoc is a mess at extracting default values. It can unambiguously
+    extract only a few simple types from the function signature, and
+    ambiguity is even more rife when extracting from doclets. So we use
+    any declared types to resolve the ambiguity.
+
+    :arg value: The extracted value, which may be of the right or wrong type
+    :arg declared_types: A list of types declared in the doclet for
+        this param. For example ``{string|number}`` would yield ['string',
+        'number'].
+    :arg first_type_is_string: Whether the first declared type for this param
+        is string, which we use as a signal that any string-typed default value
+        in the JSON is legitimately string-typed rather than some arrow
+        function or something just encased in quotes because they couldn't
+        think what else to do. Thus, if you want your ambiguously documented
+        default like ``@param {string|Array} [foo=[]]`` to be treated as a
+        string, make sure "string" comes first.
+
+    """
+    if isinstance(value, str):  # JSDoc threw it to us as a string in the JSON.
+        if declared_types and not first_type_is_string:
+            # It's a spurious string, like ``() => 5`` or a variable name.
+            # Let it through verbatim.
+            return value
+        else:
+            # It's a real string.
+            return dumps(value)  # Escape any contained quotes.
+    else:  # It came in as a non-string.
+        if first_type_is_string:
+            # It came in as an int, null, or bool, and we have to
+            # convert it back to a string.
+            return '"%s"' % (dumps(value),)
+        else:
+            # It's fine as the type it is.
+            return dumps(value)
+
+
+def unwrapped_description(obj):
+    return sub(r'[ \t]*[\r\n]+[ \t]*', ' ', obj.get('description', ''))  # TODO: Don't unwrap unless totally unindented. Maybe this would let us support OLs and ULs in param descriptions.
+
+
+def get_type(props):
+    """Given an arbitrary object from a jsdoc-emitted JSON file, go get the
+    ``type`` property, and return the textual rendering of the type, possibly a
+    union like ``Foo | Bar``, or None if we don't know the type."""
+    names = props.get('type', {}).get('names', [])
+    return ' | '.join(names) if names else None
+
+
+def top_level_properties(doclet, full_path):
+    """Extract information common to complex entities, and return it as a dict.
+
+    Specifically, pull out the information needed to parametrize TopLevel's
+    constructor.
+
+    """
+    return dict(
+        name=doclet['name'],
+        path=Pathname(full_path),
+        filename=doclet['meta']['filename'],
+        # description's source varies depending on whether the doclet is a
+        #    class, so it gets filled out elsewhere.
+        line=doclet['meta']['lineno'],
+        deprecated=doclet.get('deprecated', False),
+        examples=doclet.get('examples', []),
+        see_alsos=doclet.get('see', []),
+        properties=properties_to_ir(doclet.get('properties', [])))
+
+
+def properties_to_ir(properties):
+    """Turn jsdoc-emitted properties JSON into a list of Properties."""
+    return [Attribute(type=get_type(p),
+                      name=p['name'],
+                      # We can get away with setting null values for these
+                      # because we never use them for anything:
+                      path=Pathname([]),
+                      filename='',
+                      description=unwrapped_description(p),
+                      line=0,
+                      deprecated=False,
+                      examples=[],
+                      see_alsos=[],
+                      properties=[],
+                      exported_from=None,
+                      is_abstract=False,
+                      is_optional=False,
+                      is_static=False,
+                      is_private=False)
+            for p in properties]
+
+
+def first_type_is_string(type):
+    type_names = type.get('names', [])
+    return type_names and type_names[0] == 'string'
+
+
+def params_to_ir(doclet):
+    """Extract the parameters of a function or class, and return a list of
+    Param instances.
+
+    Formal param fallback philosophy:
+
+    1. If the user puts a formal param list in the RST explicitly, use that.
+    2. Else, if they've @param'd anything, show just those args. This gives the
+       user full control from the code, so they can use autoclass without
+       having to manually write each function signature in the RST.
+    3. Else, extract a formal param list from the meta field, which will lack
+       descriptions.
+
+    Param list:
+
+    * Don't show anything without a description or at least a type. It adds
+      nothing.
+
+    Our extraction to IR thus follows our formal param philosophy, and the
+    renderer caps it off by checking for descriptions and types while building
+    the param+description list.
+
+    :arg doclet: A JSDoc doclet representing a function or class
+
+    """
+    ret = []
+
+    # First, go through the explicitly documented params:
+    for p in doclet.get('params', []):
+        type = get_type(p)
+        default = p.get('defaultvalue', NO_DEFAULT)
+        formatted_default = (
+            'dummy' if default is NO_DEFAULT else
+            format_default_according_to_type_hints(
+                default,
+                type,
+                first_type_is_string(p.get('type', {}))))
+        ret.append(Param(
+            name=p['name'],
+            description=p.get('description', ''),
+            has_default=default is not NO_DEFAULT,
+            default=formatted_default,
+            is_variadic=p.get('variable', False),
+            type=get_type(p)))
+
+    # Use params from JS code if there are no documented @params.
+    if not ret:
+        ret = [Param(name=p) for p in
+               doclet['meta']['code'].get('paramnames', [])]
+
+    return ret
+
+
+def exceptions_to_ir(exceptions):
+    """Turn jsdoc's JSON-formatted exceptions into a list of Exceptions."""
+    return [Exc(type=get_type(e),
+                description=unwrapped_description(e))
+            for e in exceptions]
+
+
+def returns_to_ir(returns):
+    return [Return(type=get_type(r),
+                   description=unwrapped_description(r))
+            for r in returns]
diff --git a/sphinx_js/renderers.py b/sphinx_js/renderers.py
index 254fd4c5..a0639e3c 100644
--- a/sphinx_js/renderers.py
+++ b/sphinx_js/renderers.py
@@ -1,6 +1,3 @@
-from json import dumps
-from re import sub
-
 from docutils.parsers.rst import Parser as RstParser
 from docutils.statemachine import StringList
 from docutils.utils import new_document
@@ -8,6 +5,8 @@
 from sphinx.errors import SphinxError
 from sphinx.util import rst
 
+from .analyzer_utils import dotted_path
+from .ir import Class, Function, Interface, Pathname
 from .parsers import PathVisitor
 from .suffix_tree import SuffixAmbiguous, SuffixNotFound
 
@@ -64,18 +63,17 @@ def rst_nodes(self):
 
         """
         try:
-            # Get the relevant documentation together:
-            doclet, full_path = self._app._sphinxjs_doclets_by_path.get_with_path(self._partial_path)
+            obj = self._app._sphinxjs_analyzer.get_object(
+                self._partial_path, self._renderer_type)
         except SuffixNotFound as exc:
             raise SphinxError('No JSDoc documentation was found for object "%s" or any path ending with that.'
                               % ''.join(exc.segments))
         except SuffixAmbiguous as exc:
-            raise SphinxError('More than one JS object matches the path suffix "%s". Candidate paths have these segments in front: %s'
+            raise SphinxError('More than one object matches the path suffix "%s". Candidate paths have these segments in front: %s'
                               % (''.join(exc.segments), exc.next_possible_keys))
         else:
             rst = self.rst(self._partial_path,
-                           full_path,
-                           doclet,
+                           obj,
                            use_short_name='short-name' in self._options)
 
             # Parse the RST into docutils nodes with a fresh doc, and return
@@ -83,95 +81,45 @@ def rst_nodes(self):
             #
             # Not sure if passing the settings from the "real" doc is the right
             # thing to do here:
-            meta = doclet['meta']
-            doc = new_document('%s:%s(%s)' % (meta['filename'],
-                                              doclet['longname'],
-                                              meta['lineno']),
+            doc = new_document('%s:%s(%s)' % (obj.filename,
+                                              obj.path,
+                                              obj.line),
                                settings=self._directive.state.document.settings)
             RstParser().parse(rst, doc)
             return doc.children
         return []
 
-    def rst(self, partial_path, full_path, doclet, use_short_name=False):
-        """Return rendered RST about an entity with the given name and doclet."""
-        dotted_name = partial_path[-1] if use_short_name else _dotted_path(partial_path)
+    def rst(self, partial_path, obj, use_short_name=False):
+        """Return rendered RST about an entity with the given name and IR
+        object."""
+        dotted_name = partial_path[-1] if use_short_name else dotted_path(partial_path)
 
         # Render to RST using Jinja:
         env = Environment(loader=PackageLoader('sphinx_js', 'templates'))
         template = env.get_template(self._template)
-        return template.render(**self._template_vars(dotted_name, full_path, doclet))
+        return template.render(**self._template_vars(dotted_name, obj))
 
-    def _formal_params(self, doclet):
+    def _formal_params(self, obj):
         """Return the JS function or class params, looking first to any
         explicit params written into the directive and falling back to those in
-        the JS code.
+        comments or JS code.
 
         Return a ReST-escaped string ready for substitution into the template.
 
         """
-        def format_default_according_to_type_hints(value, declared_types):
-            """Return the default value for a param, formatted as a string
-            ready to be used in a formal parameter list.
-
-            JSDoc is a mess at extracting default values. It can unambiguously
-            extract only a few simple types from the function signature, and
-            ambiguity is even more rife when extracting from doclets. So we use
-            any declared types to resolve the ambiguity.
-
-            :arg value: The extracted value, which may be of the right or wrong type
-            :arg declared_types: A list of types declared in the doclet for
-                this param. For example ``{string|number}`` would yield ['string',
-                'number'].
-
-            """
-            def first(list, default):
-                try:
-                    return list[0]
-                except IndexError:
-                    return default
-
-            declared_type_implies_string = first(declared_types, '') == 'string'
-
-            # If the first item of the type disjunction is "string", we treat
-            # the default value as a string. Otherwise, we don't. So, if you
-            # want your ambiguously documented default like ``@param
-            # {string|Array} [foo=[]]`` to be treated as a string, make sure
-            # "string" comes first.
-            if isinstance(value, str):  # JSDoc threw it to us as a string in the JSON.
-                if declared_types and not declared_type_implies_string:
-                    # It's a spurious string, like ``() => 5`` or a variable name.
-                    # Let it through verbatim.
-                    return value
-                else:
-                    # It's a real string.
-                    return dumps(value)  # Escape any contained quotes.
-            else:  # It came in as a non-string.
-                if declared_type_implies_string:
-                    # It came in as an int, null, or bool, and we have to
-                    # convert it back to a string.
-                    return '"%s"' % (dumps(value),)
-                else:
-                    # It's fine as the type it is.
-                    return dumps(value)
-
         if self._explicit_formal_params:
             return self._explicit_formal_params
 
-        # Harvest params from the @param tag unless they collide with an
-        # explicit formal param. Even look at params that are really
-        # documenting subproperties of formal params. Also handle param default
-        # values.
-        params = []
-        used_names = []
-        MARKER = object()
+        formals = []
+        used_names = set()
 
-        for param in doclet.get('params', []):
-            name = param['name'].split('.')[0]
-            default = param.get('defaultvalue', MARKER)
-            type = param.get('type', {'names': []})
+        for param in obj.params:
+            # Turn "@param p2.subProperty" into just p2. We wouldn't want to
+            # add subproperties to the flat formal param list:
+            name = param.name.split('.')[0]
 
             # Add '...' to the parameter name if it's a variadic argument
-            if param.get('variable'):
+            if param.is_variadic:
                 name = '...' + name
 
             if name not in used_names:
@@ -179,18 +127,13 @@ def first(list, default):
                 # the js:function directive (or maybe directive params in
                 # general) automatically ignores markup constructs in its
                 # parameter (though not its contents).
-                params.append(name if default is MARKER else
-                              '%s=%s' % (name,
-                                         format_default_according_to_type_hints(default, type['names'])))
-                used_names.append(name)
-
-        # Use params from JS code if there are no documented params:
-        if not params:
-            params = [p for p in doclet['meta']['code'].get('paramnames', [])]
+                formals.append(name if not param.has_default else
+                               '%s=%s' % (name, param.default))
+                used_names.add(name)
 
-        return '(%s)' % ', '.join(params)
+        return '(%s)' % ', '.join(formals)
 
-    def _fields(self, doclet):
+    def _fields(self, obj):
         """Return an iterable of "info fields" to be included in the directive,
         like params, return values, and exceptions.
 
@@ -199,185 +142,205 @@ def _fields(self, doclet):
         tail comes after.
 
         """
-        FIELD_TYPES = [('params', _params_formatter),
+        FIELD_TYPES = [('params', _param_formatter),
                        ('params', _param_type_formatter),
-                       ('properties', _params_formatter),
+                       ('properties', _param_formatter),
                        ('properties', _param_type_formatter),
-                       ('exceptions', _exceptions_formatter),
-                       ('returns', _returns_formatter)]
-        for field_name, callback in FIELD_TYPES:
-            for field in doclet.get(field_name, []):
-                description = field.get('description', '')
-                unwrapped = sub(r'[ \t]*[\r\n]+[ \t]*', ' ', description)  # TODO: Don't unwrap unless totally unindented. Maybe this would let us support OLs and ULs in param descriptions.
-                heads, tail = callback(field, unwrapped)
-                yield [rst.escape(h) for h in heads], tail
+                       ('exceptions', _exception_formatter),
+                       ('returns', _return_formatter)]
+        for collection_attr, callback in FIELD_TYPES:
+            for instance in getattr(obj, collection_attr, []):
+                result = callback(instance)
+                if result:
+                    heads, tail = result
+                    yield [rst.escape(h) for h in heads], tail
 
 
 class AutoFunctionRenderer(JsRenderer):
     _template = 'function.rst'
+    _renderer_type = 'function'
 
-    def _template_vars(self, name, full_path, doclet):
+    def _template_vars(self, name, obj):
         return dict(
             name=name,
-            params=self._formal_params(doclet),
-            fields=self._fields(doclet),
-            description=doclet.get('description', ''),
-            examples=doclet.get('examples', ''),
-            deprecated=doclet.get('deprecated', False),
-            see_also=doclet.get('see', []),
+            params=self._formal_params(obj),
+            fields=self._fields(obj),
+            description=obj.description,
+            examples=obj.examples,
+            deprecated=obj.deprecated,
+            is_optional=obj.is_optional,
+            see_also=obj.see_alsos,
             content='\n'.join(self._content))
 
 
 class AutoClassRenderer(JsRenderer):
     _template = 'class.rst'
-
-    def _template_vars(self, name, full_path, doclet):
+    _renderer_type = 'class'
+
+    def _template_vars(self, name, obj):
+        # TODO: At the moment, we pull most fields (params, returns,
+        # exceptions, etc.) off the constructor only. We could pull them off
+        # the class itself too in the future.
+        if not isinstance(obj, Class) or not obj.constructor:
+            # One way or another, it has no constructor, so make a blank one to
+            # keep from repeating this long test for every constructor-using
+            # line in the dict() call:
+            constructor = Function(
+                name='',
+                path=Pathname([]),
+                filename='',
+                description='',
+                line=0,
+                deprecated=False,
+                examples=[],
+                see_alsos=[],
+                properties=[],
+                exported_from=None,
+                is_abstract=False,
+                is_optional=False,
+                is_static=False,
+                is_private=False,
+                params=[],
+                exceptions=[],
+                returns=[])
+        else:
+            constructor = obj.constructor
         return dict(
             name=name,
-            params=self._formal_params(doclet),
-            fields=self._fields(doclet),
-            examples=doclet.get('examples', ''),
-            deprecated=doclet.get('deprecated', False),
-            see_also=doclet.get('see', []),
-            class_comment=doclet.get('classdesc', ''),
-            constructor_comment=doclet.get('description', ''),
+            params=self._formal_params(constructor),
+            fields=self._fields(constructor),
+            examples=constructor.examples,
+            deprecated=constructor.deprecated,
+            see_also=constructor.see_alsos,
+            exported_from=obj.exported_from,
+            class_comment=obj.description,
+            is_abstract=isinstance(obj, Class) and obj.is_abstract,
+            interfaces=obj.interfaces if isinstance(obj, Class) else [],
+            is_interface=isinstance(obj, Interface),  # TODO: Make interfaces not look so much like classes. This will require taking complete control of templating from Sphinx.
+            supers=obj.supers,
+            constructor_comment=constructor.description,
             content='\n'.join(self._content),
-            members=self._members_of(full_path,
+            members=self._members_of(obj,
                                      include=self._options['members'],
                                      exclude=self._options.get('exclude-members', set()),
                                      should_include_private='private-members' in self._options)
                     if 'members' in self._options else '')
 
-    def _members_of(self, full_path, include, exclude, should_include_private):
-        """Return RST describing the members of the named class.
+    def _members_of(self, obj, include, exclude, should_include_private):
+        """Return RST describing the members of a given class.
 
-        :arg full_path list: The unambiguous path of the class we're documenting
+        :arg obj Class: The class we're documenting
         :arg include: List of names of members to include. If empty, include
             all.
         :arg exclude: Set of names of members to exclude
         :arg should_include_private: Whether to include private members
 
         """
-        def rst_for(doclet):
-            renderer = (AutoFunctionRenderer if doclet.get('kind') in ['function', 'typedef']
+        def rst_for(obj):
+            renderer = (AutoFunctionRenderer if isinstance(obj, Function)
                         else AutoAttributeRenderer)
-            # Pass a dummy arg list with no formal param list so
-            # _formal_params() won't find an explicit param list in there and
-            # override what it finds in the code:
             return renderer(self._directive, self._app, arguments=['dummy']).rst(
-                [doclet['name']],
-                'dummy_full_path',
-                doclet,
+                [obj.name],
+                obj,
                 use_short_name=False)
 
-        def doclets_to_include(include):
-            """Return the doclets that should be included (before excludes and
+        def members_to_include(include):
+            """Return the members that should be included (before excludes and
             access specifiers are taken into account).
 
-            This will either be the doclets explicitly listed after the
-            ``:members:`` option, in that order; all doclets that are
-            members of the class; or listed members with remaining ones
-            inserted at the placeholder "*".
+            This will either be the ones explicitly listed after the
+            ``:members:`` option, in that order; all members of the class; or
+            listed members with remaining ones inserted at the placeholder "*".
 
             """
-            doclets = self._app._sphinxjs_doclets_by_class[tuple(full_path)]
+            def sort_attributes_first_then_by_path(obj):
+                """Return a sort key for IR objects."""
+                return isinstance(obj, Function), obj.path.segments
+
+            members = obj.members
             if not include:
                 # Specifying none means listing all.
-                return sorted(doclets, key=lambda d: d.get('longname', d['name']))
+                return sorted(members, key=sort_attributes_first_then_by_path)
             included_set = set(include)
 
-            # If the special name * is included in the list, include
-            # all other doclets, in sorted order.
+            # If the special name * is included in the list, include all other
+            # members, in sorted order.
             if '*' in included_set:
                 star_index = include.index('*')
-                sorted_not_included_doclets = sorted(
-                    (d for d in doclets if d['name'] not in included_set),
-                    key=lambda d: d.get('longname', d['name'])
+                sorted_not_included_members = sorted(
+                    (m for m in members if m.name not in included_set),
+                    key=sort_attributes_first_then_by_path
                 )
-                not_included = [d['name'] for d in sorted_not_included_doclets]
+                not_included = [m.name for m in sorted_not_included_members]
                 include = include[:star_index] + not_included + include[star_index + 1:]
                 included_set.update(not_included)
 
-            # Even if there are 2 doclets with the same short name (e.g. a
+            # Even if there are 2 members with the same short name (e.g. a
             # static member and an instance one), keep them both. This
             # prefiltering step should make the below sort less horrible, even
             # though I'm calling index().
-            included_doclets = [d for d in doclets if d['name'] in included_set]
-            # sort()'s stability should keep same-named doclets in the order
+            included_members = [m for m in members if m.name in included_set]
+            # sort()'s stability should keep same-named members in the order
             # JSDoc spits them out in.
-            included_doclets.sort(key=lambda d: include.index(d['name']))
-            return included_doclets
+            included_members.sort(key=lambda m: include.index(m.name))
+            return included_members
 
         return '\n\n'.join(
-            rst_for(doclet) for doclet in doclets_to_include(include)
-            if (doclet.get('access', 'public') in ('public', 'protected')
-                or (doclet.get('access') == 'private' and should_include_private))
-            and doclet['name'] not in exclude)
+            rst_for(member) for member in members_to_include(include)
+            if (not member.is_private
+                or (member.is_private and should_include_private))
+            and member.name not in exclude)
 
 
 class AutoAttributeRenderer(JsRenderer):
     _template = 'attribute.rst'
+    _renderer_type = 'attribute'
 
-    def _template_vars(self, name, full_path, doclet):
+    def _template_vars(self, name, obj):
         return dict(
             name=name,
-            description=doclet.get('description', ''),
-            deprecated=doclet.get('deprecated', False),
-            see_also=doclet.get('see', []),
-            examples=doclet.get('examples', ''),
-            type='|'.join(doclet.get('type', {}).get('names', [])),
+            description=obj.description,
+            deprecated=obj.deprecated,
+            is_optional=obj.is_optional,
+            see_also=obj.see_alsos,
+            examples=obj.examples,
+            type=obj.type,
             content='\n'.join(self._content))
 
 
-def _returns_formatter(field, description):
+def _return_formatter(return_):
     """Derive heads and tail from ``@returns`` blocks."""
-    types = _or_types(field)
-    tail = ('**%s** -- ' % rst.escape(types)) if types else ''
-    tail += description
+    tail = ('**%s** -- ' % rst.escape(return_.type)) if return_.type else ''
+    tail += return_.description
     return ['returns'], tail
 
 
-def _params_formatter(field, description):
+def _param_formatter(param):
     """Derive heads and tail from ``@param`` blocks."""
+    if not param.type and not param.description:
+        # There's nothing worth saying about this param.
+        return None
     heads = ['param']
-    types = _or_types(field)
-    if types:
-        heads.append(types)
-    heads.append(field['name'])
-    tail = description
+    if param.type:
+        heads.append(param.type)
+    heads.append(param.name)
+    tail = param.description
     return heads, tail
 
 
-def _param_type_formatter(field, description):
+def _param_type_formatter(param):
     """Generate types for function parameters specified in field."""
-    heads = ['type', field['name']]
-    tail = rst.escape(_or_types(field))
+    if not param.type:
+        return None
+    heads = ['type', param.name]
+    tail = rst.escape(param.type)
     return heads, tail
 
 
-def _exceptions_formatter(field, description):
+def _exception_formatter(exception):
     """Derive heads and tail from ``@throws`` blocks."""
     heads = ['throws']
-    types = _or_types(field)
-    if types:
-        heads.append(types)
-    tail = description
+    if exception.type:
+        heads.append(exception.type)
+    tail = exception.description
     return heads, tail
-
-
-def _or_types(field):
-    """Return all the types in a doclet subfield like "params" or "returns"
-    with vertical bars between them, like "number|string".
-
-    ReST-escape the types.
-
-    """
-    return '|'.join(field.get('type', {}).get('names', []))
-
-
-def _dotted_path(segments):
-    """Convert a JS object path (``['dir/', 'file/', 'class#',
-    'instanceMethod']``) to a dotted style that Sphinx will better index."""
-    segments_without_separators = [s[:-1] for s in segments[:-1]]
-    segments_without_separators.append(segments[-1])
-    return '.'.join(segments_without_separators)
diff --git a/sphinx_js/suffix_tree.py b/sphinx_js/suffix_tree.py
index 80fccd90..2ebd27bd 100644
--- a/sphinx_js/suffix_tree.py
+++ b/sphinx_js/suffix_tree.py
@@ -13,6 +13,13 @@ def __init__(self):
         self._tree = {}
 
     def add(self, unambiguous_segments, value):
+        """Add an item to the tree.
+
+        :arg unambiguous_segments: A list of path segments that correspond
+            uniquely to the object
+        :arg value: Any value representing the JS object
+
+        """
         tree = self._tree
         for seg in reversed(unambiguous_segments[1:]):
             tree = tree.setdefault(seg, {})
@@ -22,6 +29,25 @@ def add(self, unambiguous_segments, value):
         else:
             tree[seg] = Value(value)
 
+    def add_many(self, segments_and_values):
+        """Add a batch of items to the tree all at once, and collect any
+        errors.
+
+        :arg segments_and_values: An iterable of (unambiguous segment path,
+            value)
+
+        If any of the segment paths are duplicates, raise PathsTaken.
+
+        """
+        conflicts = []
+        for segs, value in segments_and_values:
+            try:
+                self.add(segs, value)
+            except PathTaken as conflict:
+                conflicts.append(conflict.segments)
+        if conflicts:
+            raise PathsTaken(conflicts)
+
     def get_with_path(self, segments):
         """Return the value stored at a path ending in the given segments,
         along with the full path found.
@@ -72,6 +98,25 @@ class PathTaken(SuffixError):
     _message = 'Attempted to add a path already in the suffix tree: %s.'
 
 
+class PathsTaken(Exception):
+    """One or more JS objects had the same paths.
+
+    Rolls up multiple PathTaken exceptions for mass reporting.
+
+    """
+    def __init__(self, conflicts):
+        """
+        :arg conflicts: A list of paths, each given as a list of segments
+        """
+        self.conflicts = conflicts
+
+    def __str__(self):
+        return ('Your code contains multiple documented objects at each of '
+                "these paths:\n\n  %s\n\nWe won't know which one you're "
+                'talking about.' %
+                '\n  '.join(''.join(c) for c in self.conflicts))
+
+
 class SuffixNotFound(SuffixError):
     """No keys ended in the given suffix."""
     _message = 'No path found ending in %s.'
diff --git a/sphinx_js/templates/attribute.rst b/sphinx_js/templates/attribute.rst
index 2582205c..42067010 100644
--- a/sphinx_js/templates/attribute.rst
+++ b/sphinx_js/templates/attribute.rst
@@ -1,6 +1,6 @@
 {% import 'common.rst' as common %}
 
-.. js:attribute:: {{ name }}
+.. js:attribute:: {{ name }}{{ '?' if is_optional else '' }}
 
    {{ common.deprecated(deprecated)|indent(3) }}
 
diff --git a/sphinx_js/templates/class.rst b/sphinx_js/templates/class.rst
index 3550bc59..b44ac1f5 100644
--- a/sphinx_js/templates/class.rst
+++ b/sphinx_js/templates/class.rst
@@ -8,6 +8,30 @@
      {{ class_comment|indent(3) }}
    {%- endif %}
 
+   {% if is_abstract -%}
+     *abstract*
+   {%- endif %}
+
+   {% if is_interface -%}
+     *interface*
+   {%- endif %}
+
+   {{ common.exported_from(exported_from)|indent(3) }}
+
+   {% if supers -%}
+     **Extends:**
+       {% for super in supers -%}
+         - :js:class:`~{{ super.dotted() }}`
+       {% endfor %}
+   {%- endif %}
+
+   {% if interfaces -%}
+     **Implements:**
+       {% for interface in interfaces -%}
+         - :js:class:`~{{ interface.dotted() }}`
+       {% endfor %}
+   {%- endif %}
+
    {% if constructor_comment -%}
      {{ constructor_comment|indent(3) }}
    {%- endif %}
diff --git a/sphinx_js/templates/common.rst b/sphinx_js/templates/common.rst
index b1eb66af..70568deb 100644
--- a/sphinx_js/templates/common.rst
+++ b/sphinx_js/templates/common.rst
@@ -28,3 +28,9 @@
    {% endfor %}
 {%- endif %}
 {% endmacro %}
+
+{% macro exported_from(pathname) %}
+{% if pathname -%}
+    *exported from* :js:mod:`{{ pathname.dotted() }}`
+{%- endif %}
+{% endmacro %}
diff --git a/sphinx_js/templates/function.rst b/sphinx_js/templates/function.rst
index a323ee8e..105538dc 100644
--- a/sphinx_js/templates/function.rst
+++ b/sphinx_js/templates/function.rst
@@ -1,6 +1,6 @@
 {% import 'common.rst' as common %}
 
-.. js:function:: {{ name }}{{ params }}
+.. js:function:: {{ name }}{{ '?' if is_optional else '' }}{{ params }}
 
    {{ common.deprecated(deprecated)|indent(3) }}
 
diff --git a/sphinx_js/typedoc.py b/sphinx_js/typedoc.py
index c244f829..2e739225 100644
--- a/sphinx_js/typedoc.py
+++ b/sphinx_js/typedoc.py
@@ -1,398 +1,460 @@
-"""Converter from typedoc output to jsdoc doclet format"""
-
-import os
-import sys
-import json
-
-# JSDoc entries used in sphinx-js:
-# - optional access of [ public, private, protected ]
-# - optional classdesc
-# - optional comment (controls doclet inclusion)
-# - optional description
-# - optional exceptions
-# - optional exclude-members
-# -          kind of [ function, typedef, <other> ]
-# -          longname
-# - optional memberof
-# -          meta.filename
-# -          meta.lineno
-# -          meta.code.paramnames
-# -          meta.code.path
-# -          name
-# - optional params
-# - optional properties
-# - optional returns
-# -          type.names
-# - optional undocumented
-
-
-class TypeDoc(object):
-    """
-    Encapsulation of the Typedoc to JSDoc conversion process.
-    Upon construction this class will convert the typedoc JSON
-    object to a list of JSDoc doclets in :py:attr:`jsdoc`.
+"""Converter from TypeDoc output to IR format"""
 
-    This class holds all the state used during the conversion making
-    it easy to do multiple (sequential) conversions.
+from codecs import getreader
+from errno import ENOENT
+from json import load
+from os.path import basename, join, normpath, relpath, sep, splitext
+import re
+import subprocess
+from tempfile import NamedTemporaryFile
+from typing import List, Optional, Tuple, Union
 
-    :ivar jsdoc: the list of generated doclets
-    :ivar nodelist: the flattened typedoc entries indexed by 'id'
+from sphinx.errors import SphinxError
 
-    JSDoc JSON schema: https://github.com/jsdoc3/jsdoc/blob/master/lib/jsdoc/schema.js
-    """
+from .analyzer_utils import Command, is_explicitly_rooted
+from .ir import Attribute, Class, Function, Interface, NO_DEFAULT, Param, Pathname, Return, TopLevel
+from .suffix_tree import SuffixTree
 
-    def __init__(self, root):
-        """
-        Construct a list of jsdoc entries from the typedoc JSON object.
 
-        :param root: a JSON object from a typedoc JSON file
+class Analyzer:
+    def __init__(self, json, base_dir):
         """
-        self.jsdoc = []
-        self.nodelist = {}
-        self.make_node_list(root)
-        self.convert_node(root)
+        :arg json: The loaded JSON output from typedoc
+        :arg base_dir: The absolute path of the dir relative to which to
+            construct file-path segments of object paths
 
-    def get_parent(self, node):
         """
-        Get the parent of a node.
+        self._base_dir = base_dir
+        self._index = index_by_id({}, json)
+        ir_objects = self._convert_all_nodes(json)
+        # Toss this overboard to save RAM. We're done with it now:
+        del self._index
+        self._objects_by_path = SuffixTree()
+        self._objects_by_path.add_many((obj.path.segments, obj) for obj in ir_objects)
 
-        :param node: A Typedoc node
-        :return: The parent Typedoc node, or None if node was the root.
-        """
-        parentId = node.get('__parentId')
-        return self.nodelist[parentId] if parentId is not None else None
+    @classmethod
+    def from_disk(cls, abs_source_paths, app, base_dir):
+        json = typedoc_output(abs_source_paths,
+                              app.confdir,
+                              app.config.jsdoc_config_path)
+        return cls(json, base_dir)
 
-    def extend_doclet(self, result, **kwargs):
-        """
-        Extend a jsdoc entry.
+    def get_object(self, path_suffix, as_type=None):
+        """Return the IR object with the given path suffix.
+
+        :arg as_type: Ignored
 
-        .. note::
+        Well, we can't scan through the raw TypeDoc output at runtime, because it's just a linear list of files, each containing a linear list of nodes. They're not indexed at all. And since we need to index by suffix, we need to traverse all the way down, eagerly. Also, we will keep the flattening, because we need it to resolve the IDs of references. (Some of the references are potentially important in the future: that's how TypeDoc points to superclass definitions of methods inherited by subclasses.
 
-           Filters out keywords with value None. This is used
-           explicitely, for example in :py:func:`simple_doclet`, and
-           implicitely when typedoc may lack an entry, for example in
-           the description field in :py:func:`make_result`.
         """
-        result.update(**kwargs)
-        return {k: v for k, v in result.items() if v is not None}
-
-    def make_doclet(self, **kwargs):
-        """Create a new jsdoc entry"""
-        return self.extend_doclet({}, **kwargs)
-
-    def make_longname(self, node):
-        """Construct the jsdoc longname entry for a typedoc node"""
-        parent = self.get_parent(node)
-        longname = self.make_longname(parent) if parent is not None else ''
-
-        kindString = node.get('kindString')
-        if kindString in [None, 'Function', 'Constructor', 'Method']:
-            return longname
-        if longname != '':
-            flags = node.get('flags')
-            if (parent.get('kindString') in ['Class', 'Interface'] and
-                    flags.get('isStatic') is not True):
-                longname += '#'
-            elif parent.get('kindString') in ['Function', 'Method']:
-                longname += '.'
-            else:
-                longname += '~'
-        if kindString == 'Module':
-            return longname + 'module:' + node.get('name')[1:-1]
-        elif kindString == 'External module':
-            return longname + 'external:' + node.get('name')[1:-1]
-        else:
-            return longname + node.get('name')
+        return self._objects_by_path.get(path_suffix)
+
+    def _containing_module(self, node) -> Optional[Pathname]:
+        """Return the Pathname pointing to the module containing the given
+        node, None if one isn't found."""
+        while True:
+            node = node.get('__parent')
+            if not node or node['id'] == 0:
+                # We went all the way up but didn't find a containing module.
+                return
+            elif node.get('kindString') == 'External module':
+                # Found one!
+                return Pathname(make_path_segments(node, self._base_dir))
 
-    def make_meta(self, node):
-        """Construct the jsdoc meta entry for a typedoc node"""
+    def _top_level_properties(self, node):
         source = node.get('sources')[0]
-        return {
-            'path': os.path.dirname(source.get('fileName')) or './',
-            'filename': os.path.basename(source.get('fileName')),
-            'lineno': source.get('line'),
-            'code': {}
-        }
-
-    def make_type_name(self, type):
-        """Construct the name of a type from a Typedoc type entry"""
-        names = []
-        if type.get('type') == 'reference' and type.get('id'):
-            node = self.nodelist[type.get('id')]
-            # Should be: names = [ self.make_longname(node)]
-            parent = self.nodelist[node.get('__parentId')]
-            if parent.get('kindString') == 'External module':
-                names = [parent['name'][1:-1] + '.' + node['name']]
-            else:
-                names = [node['name']]
-        elif type.get('type') in ['intrinsic', 'reference', 'unknown']:
-            names = [type.get('name')]
-        elif type.get('type') == 'stringLiteral':
-            names = ['"' + type.get('value') + '"']
-        elif type.get('type') == 'array':
-            names = [self.make_type_name(type.get('elementType'))[0] + '[]']
-        elif type.get('type') == 'tuple' and type.get('elements'):
-            types = ['|'.join(self.make_type_name(t)) for t in type.get('elements')]
-            names = ['[' + ','.join(types) + ']']
-        elif type.get('type') == 'union':
-            names = [self.make_type_name(t)[0] for t in type.get('types') if self.make_type_name(t)]
-        elif type.get('type') == 'typeOperator':
-            target_name = self.make_type_name(type.get('target'))
-            names = [type.get('operator') + ':' + ':'.join(target_name)]
-        elif type.get('type') == 'typeParameter':
-            names = [type.get('name')]
-            constraint = type.get('constraint')
-            if constraint is not None:
-                names.extend(['extends', self.make_type_name(constraint)])
-        elif type.get('type') == 'reflection':
-            names = ['<TODO>']
-        if type.get('typeArguments'):
-            argNames = ['|'.join(self.make_type_name(arg)) for arg in type.get('typeArguments')]
-            names = [names[0] + '<' + ','.join(argNames) + '>']
-        return names
-
-    def make_type(self, type):
-        """Construct a jsdoc type entry"""
-        type_name = self.make_type_name(type)
-        return {'names': type_name}
-
-    def make_description(self, comment):
-        """Construct a jsdoc description entry"""
-        if not comment:
-            return ''
+        if node.get('flags', {}).get('isExported', False):
+            exported_from = self._containing_module(node)
         else:
-            return '\n\n'.join([
-                comment.get('shortText', ''),
-                comment.get('text', '')
-            ])
-
-    def make_param(self, param, tags_comment=None):
-        """Construct a jsdoc parameter entry"""
-        typeEntry = param.get('type')
-        description = self.make_description(param.get('comment', {}))
-        name = param.get('name')
-        # this part reads parameter descriptions that were written with
-        # @arg or @argument instead of @param, which are valid aliases
-        # but treated differently by typedoc
-        # see: https://github.com/Microsoft/TypeScript/wiki/JsDoc-support-in-JavaScript
-        if tags_comment is not None and tags_comment.get('tags', []):
-            for tag in tags_comment.get('tags', []):
-                tag_text = tag.get('text')
-                # the space prevents partial matching
-                # i.e. 'simple2' starts with 'simple' but not with 'simple '
-                if tag.get('tag') in ['arg', 'argument'] and tag_text.startswith(name + ' '):
-                    tag_text.replace(name, '', 1)
-                    description += '\n\n' + tag_text
-        if typeEntry is None:
-            return self.make_doclet(
-                name=name,
-                description=description
-            )
-        else:
-            return self.make_doclet(
-                name=name,
-                type=self.make_type(typeEntry),
-                description=description
-            )
-
-    def make_result(self, param):
-        """Construct a jsdoc function result entry"""
-        type = param.get('type')
-        if type is None or type.get('name') == 'void':
-            return []
-        return [self.make_doclet(
-            name=param.get('name'),
-            type=self.make_type(type),
-            description=param.get('comment', {}).get('returns')
-        )]
-
-    def simple_doclet(self, kind, node):
-        """Construct a jsdoc entry with some frequently used fields."""
-        memberof = self.make_longname(self.get_parent(node))
-        if memberof == '':
-            memberof = None
-        if node.get('flags').get('isPrivate'):
-            access = 'private'
-        elif node.get('flags').get('isProtected'):
-            access = 'protected'
-        else:
-            access = None
-        comment = node.get('comment')
-        return self.make_doclet(
-            kind=kind,
-            access=access,
-            comment=node.get('comment', {}).get('text', '<empty>'),
-            meta=self.make_meta(node),
-            name=node.get('name'),
-            longname=self.make_longname(node),
-            memberof=memberof,
-            description=self.make_description(comment),
-        )
-
-    def convert_node(self, node):
-        """
-        Convert a typedoc entry to a jsdoc entry. Typedoc entries are
-        hierarchical, so this function will recurse.
-        New entries are added to :py:attr:`self.jsdoc`.
+            exported_from = None
+        return dict(
+            name=short_name(node),
+            path=Pathname(make_path_segments(node, self._base_dir)),
+            filename=basename(source['fileName']),
+            description=make_description(node.get('comment', {})),
+            line=source['line'],
+
+            # These properties aren't supported by TypeDoc:
+            deprecated=False,
+            examples=[],
+            see_alsos=[],
+            properties=[],
+
+            exported_from=exported_from)
+
+    def _constructor_and_members(self, cls) -> Tuple[Optional[Function], List[Union[Function, Attribute]]]:
+        """Return the constructor and other members of a class.
+
+        In TS, a constructor may have multiple (overloaded) type signatures but
+        only one implementation. (Same with functions.) So there's at most 1
+        constructor to return. Return None for the constructor if it is inherited
+        or implied rather than explicitly present in the class.
 
-        .. rubric:: To do
+        :arg cls: A TypeDoc node of the class to take apart
+        :return: A tuple of (constructor Function, list of other members)
+
+        """
+        constructor = None
+        members = []
+        for child in cls.get('children', []):
+            ir, _ = self._convert_node(child)
+            if ir:
+                if (child.get('kindString') == 'Constructor'):
+                    # This really, really should happen exactly once per class.
+                    constructor = ir
+                else:
+                    members.append(ir)
+        return constructor, members
+
+    def _convert_all_nodes(self, root):
+        todo = [root]
+        done = []
+        while todo:
+            converted, more_todo = self._convert_node(todo.pop())
+            if converted:
+                done.append(converted)
+            todo.extend(more_todo)
+        return done
+
+    def _convert_node(self, node) -> Tuple[TopLevel, List[dict]]:
+        """Convert a node of TypeScript JSON output to an IR object.
+
+        :return: A tuple: (the IR object, a list of other nodes found within
+            that you can convert to other IR objects). For the second item of
+            the tuple, nodes that can't presently be converted to IR objects
+            are omitted.
 
-           Some entries generate restructured text. Preferably this
-           information should be captured in the jsdoc entries and
-           used in the templates.
         """
         if node.get('inheritedFrom'):
-            return
+            return None, []
         if node.get('sources'):
             # Ignore nodes with a reference to absolute paths (like /usr/lib)
             source = node.get('sources')[0]
             if source.get('fileName', '.')[0] == '/':
-                return
+                return None, []
+
+        ir = None
+        kind = node.get('kindString')
+        if kind == 'External module':
+            # We shouldn't need these until we implement automodule. But what of
+            # js:mod in the templates?
+            pass
+        elif kind == 'Module':
+            # Does anybody even use TS's old internal modules anymore?
+            pass
+        elif kind == 'Interface':
+            _, members = self._constructor_and_members(node)
+            ir = Interface(
+                members=members,
+                supers=self._related_types(node, kind='extendedTypes'),
+                **self._top_level_properties(node))
+        elif kind == 'Class':
+            # Every class has a constructor in the JSON, even if it's only
+            # inherited.
+            constructor, members = self._constructor_and_members(node)
+            ir = Class(
+                constructor=constructor,
+                members=members,
+                supers=self._related_types(node, kind='extendedTypes'),
+                is_abstract=node.get('flags', {}).get('isAbstract', False),
+                interfaces=self._related_types(node, kind='implementedTypes'),
+                **self._top_level_properties(node))
+        elif kind in ['Property', 'Variable']:
+            ir = Attribute(
+                type=self._type_name(node.get('type')),
+                **member_properties(node),
+                **self._top_level_properties(node))
+        elif kind == 'Accessor':  # NEXT: Write renderers.
+            get_signature = node.get('getSignature')
+            if get_signature:
+                # There's no signature to speak of for a getter: only a return type.
+                type = get_signature[0]['type']
+            else:
+                # ES6 says setters have exactly 1 param. I'm not sure if they can
+                # have multiple signatures, though.
+                type = node['setSignature'][0]['parameters'][0]['type']
+            ir = Attribute(
+                type=self._type_name(type),
+                **member_properties(node),
+                **self._top_level_properties(node))
+        elif kind in ['Function', 'Constructor', 'Method']:
+            # There's really nothing in these; all the interesting bits are in the
+            # contained 'Call signature' keys. We support only the first signature
+            # at the moment, because to do otherwise would create multiple
+            # identical pathnames to the same function, which would cause the
+            # suffix tree to raise an exception while being built. An eventual
+            # solution might be to store the signatures in a one-to-many attr of
+            # Functions.
+            sigs = node.get('signatures')
+            first_sig = sigs[0]  # Should always have at least one
+            first_sig['sources'] = node['sources']
+            return self._convert_node(first_sig)
+        elif kind in ['Call signature', 'Constructor signature']:
+            # This is the real meat of a function, method, or constructor.
+            #
+            # Constructors' .name attrs end up being like 'new Foo'. They
+            # should probably be called "constructor", but I'm not bothering
+            # with that yet because nobody uses that attr on constructors atm.
+            ir = Function(
+                params=[self._make_param(p) for p in node.get('parameters', [])],
+                # Exceptions are discouraged in TS as being unrepresentable in its
+                # type system. More importantly, TypeDoc does not support them.
+                exceptions=[],
+                # Though perhaps technically true, it looks weird to the user
+                # (and in the template) if constructors have a return value:
+                returns=self._make_returns(node) if kind != 'Constructor signature' else [],
+                **member_properties(node['__parent']),
+                **self._top_level_properties(node))
+
+        return ir, node.get('children', [])
+
+    def _related_types(self, node, kind):
+        """Return the unambiguous pathnames of implemented interfaces or
+        extended classes.
+
+        If we encounter a formulation of interface or class reference that we
+        don't understand (which I expect to occur only if it turns out you can
+        use a class or interface literal rather than referencing a declared
+        one), return 'UNIMPLEMENTED' for that interface or class so somebody
+        files a bug requesting we fix it. (It's not worth crashing for.)
 
-        kindString = node.get('kindString')
-        if kindString == 'External module':
-            doclet = self.simple_doclet('external', node)
+        """
+        types = []
+        for type in node.get(kind, []):
+            if type['type'] == 'reference':
+                pathname = Pathname(make_path_segments(self._index[type['id']],
+                                                       self._base_dir))
+                types.append(pathname)
+            # else it's some other thing we should go implement
+        return types
 
-        elif kindString == 'Module':
-            doclet = self.simple_doclet('module', node)
+    def _type_name(self, type):
+        """Return a string description of a type.
 
-        elif kindString in ['Class', 'Interface']:
-            specifiers = []
-            if kindString == 'Interface':
-                doclet = self.simple_doclet('interface', node)
-                specifiers.append('*interface*')
-            else:
-                doclet = self.simple_doclet('class', node)
-            doclet['classdesc'] = ''
-            if node.get('flags', {}).get('isAbstract'):
-                specifiers.append('*abstract*')
-            if node.get('flags', {}).get('isExported'):
-                module_name = self.get_parent(node).get('name')[1:-1]
-                specifiers.append('*exported from* :js:mod:`' + module_name + '`')
-            doclet['classdesc'] += ', '.join(specifiers)
-            if node.get('extendedTypes'):
-                doclet['classdesc'] += '\n\n**Extends:**\n'
-                for type in node.get('extendedTypes', []):
-                    type_name = ' '.join(self.make_type_name(type))
-                    doclet['classdesc'] += ' * :js:class:`' + type_name + '`\n'
-            if node.get('implementedTypes'):
-                doclet['classdesc'] += '\n\n**Implements:**\n'
-                for type in node.get('implementedTypes', []):
-                    type_name = ' '.join(self.make_type_name(type))
-                    doclet['classdesc'] += ' * :js:class:`' + type_name + '`\n'
+        :arg type: A TypeDoc-emitted type node
 
+        """
+        type_of_type = type.get('type')
+
+        if type_of_type == 'reference' and type.get('id'):
+            node = self._index[type['id']]
+            name = node['name']
+        elif type_of_type == 'unknown':
+            if re.match(r'-?\d*(\.\d+)?', type['name']):  # It's a number.
+                # TypeDoc apparently sticks numeric constants' values into the
+                # type name. String constants? Nope. Function ones? Nope.
+                name = 'number'
             else:
-                doclet['params'] = []
-                for param in node.get('typeParameter', []):
-                    doclet['params'].append(self.make_param(param))
-
-            if kindString == 'Class':
-                for child in node.get('children', []):
-                    if (child.get('kindString') == 'Constructor'):
-                        child_comment = child.get('comment', {})
-                        child_description = self.make_description(child_comment)
-                        doclet['description'] += '\n\n{}'.format(child_description)
-                        if child.get('flags', {}).get('isAbstract'):
-                            doclet['description'] = '*abstract*\n\n' + doclet['description']
-                        if child.get('flags', {}).get('isOptional'):
-                            doclet['description'] = '*optional*\n\n' + doclet['description']
-                        self.extend_doclet(
-                            doclet,
-                            params=[],
-                            returns=self.make_result(child)
-                        )
-                        signature = child.get('signatures')[0]
-                        doclet['meta']['code']['paramnames'] = []
-                        for param in signature.get('parameters', []):
-                            doclet['params'].append(self.make_param(param, child_comment))
-                            doclet['meta']['code']['paramnames'].append(param.get('name'))
-                        break
-
-            self.extend_doclet(
-                doclet,
-                extends=[e['name'] for e in node.get('extendedTypes', [])]
-            )
-
-        elif kindString == 'Property':
-            doclet = self.simple_doclet('member', node)
-            if node.get('flags', {}).get('isAbstract'):
-                doclet['description'] = '*abstract*\n\n' + doclet['description']
-            self.extend_doclet(
-                doclet,
-                type=self.make_type(node.get('type'))
-            )
-
-        elif kindString == 'Accessor':
-            doclet = self.simple_doclet('member', node)
-            if node.get('getSignature'):
-                type = self.make_type(node['getSignature'][0]['type'])
-            else:
-                type_name = node['setSignature'][0]['parameters'][0]['type']
-                type = self.make_type(type_name)
-            self.extend_doclet(doclet, type=type)
-
-        elif kindString in ['Function', 'Constructor', 'Method']:
-            for sig in node.get('signatures'):
-                sig['sources'] = node['sources']
-                self.convert_node(sig)
-            return
-
-        elif kindString in ['Call signature']:
-            parent = self.get_parent(node)
-            doclet = self.simple_doclet('function', node)
-            if parent.get('flags', {}).get('isAbstract'):
-                doclet['description'] = '*abstract*\n\n' + doclet['description']
-            if parent.get('flags', {}).get('isOptional'):
-                doclet['description'] = '*optional*\n\n' + doclet['description']
-            self.extend_doclet(
-                doclet,
-                params=[],
-                returns=self.make_result(node)
-            )
-            doclet['meta']['code']['paramnames'] = []
-            for param in node.get('parameters', []):
-                doclet['params'].append(self.make_param(param, node.get('comment')))
-                doclet['meta']['code']['paramnames'].append(param.get('name'))
+                name = type['name']
+        elif type_of_type in ['intrinsic', 'reference']:
+            name = type['name']
+        elif type_of_type == 'stringLiteral':
+            name = '"' + type['value'] + '"'
+        elif type_of_type == 'array':
+            name = self._type_name(type['elementType']) + '[]'
+        elif type_of_type == 'tuple' and type.get('elements'):
+            types = [self._type_name(t) for t in type['elements']]
+            name = '[' + ', '.join(types) + ']'
+        elif type_of_type == 'union':
+            name = ' | '.join(self._type_name(t) for t in type['types'])
+        elif type_of_type == 'intersection':
+            name = ' & '.join(self._type_name(t) for t in type['types'])
+        elif type_of_type == 'typeOperator':
+            name = type['operator'] + ' ' + self._type_name(type['target'])
+            # e.g. "keyof T"
+        elif type_of_type == 'typeParameter':
+            name = type['name']
+            constraint = type.get('constraint')
+            if constraint is not None:
+                name += ' extends ' + self._type_name(constraint)
+                # e.g. K += extends + keyof T
+        elif type_of_type == 'reflection':
+            name = '<TODO: reflection>'
+            # test_generic_member() (currently skipped) tests this.
         else:
-            doclet = None
-        if doclet:
-            self.jsdoc.append(doclet)
-        for child in node.get('children', []):
-            self.convert_node(child)
-
-    def make_node_list(self, node, parent=None):
-        """Flatten the tree of Typedoc entries to a list indexed by 'id'"""
-        if node is None:
-            return
-        if node.get('id') is not None:
-            node['__parentId'] = parent
-            self.nodelist[node['id']] = node
+            name = '<TODO: other type>'
+
+        type_args = type.get('typeArguments')
+        if type_args:
+            arg_names = ', '.join(self._type_name(arg) for arg in type_args)
+            name += f'<{arg_names}>'
+
+        return name
+
+    def _make_param(self, param):
+        """Make a Param from a 'parameters' JSON item"""
+        has_default = 'defaultValue' in param
+        return Param(
+            name=param.get('name'),
+            description=make_description(param.get('comment', {})),
+            has_default=has_default,
+            is_variadic=param.get('flags', {}).get('isRest', False),
+            # For now, we just pass a single string in as the type rather than a
+            # list of types to be unioned by the renderer. There's really no
+            # disadvantage.
+            type=self._type_name(param.get('type', {})),
+            default=param['defaultValue'] if has_default else NO_DEFAULT)
+
+    def _make_returns(self, signature) -> List[Return]:
+        """Return the Returns a function signature can have.
+
+        Because, in TypeDoc, each signature can have only 1 @return tag, we return
+        a list of either 0 or 1 item.
+
+        """
+        type = signature.get('type')
+        if type is None or type.get('name') == 'void':
+            # Returns nothing
+            return []
+        return [Return(
+            type=self._type_name(type),
+            description=signature.get('comment', {}).get('returns', '').strip())]
+
+
+def typedoc_output(abs_source_paths, sphinx_conf_dir, config_path):
+    """Return the loaded JSON output of the TypeDoc command run over the given
+    paths."""
+    command = Command('typedoc')
+    if config_path:
+        command.add('--tsconfig', normpath(join(sphinx_conf_dir, config_path)))
+
+    with NamedTemporaryFile(mode='w+b') as temp:
+        command.add('--json', temp.name, *abs_source_paths)
+        try:
+            subprocess.call(command.make())
+        except OSError as exc:
+            if exc.errno == ENOENT:
+                raise SphinxError('%s was not found. Install it using "npm install -g typedoc".' % command.program)
+            else:
+                raise
+        # typedoc emits a valid JSON file even if it finds no TS files in the dir:
+        return load(getreader('utf-8')(temp))
+
+
+def index_by_id(index, node, parent=None):
+    """Create an ID-to-node mapping for all the TypeDoc output nodes.
+
+    We don't unnest them, but we do add ``__parent`` keys so we can easily walk
+    both up and down.
+
+    :arg index: The mapping to add keys to as we go
+    :arg node: The node to start traversing down from
+    :arg parent: The parent node of ``node``
+
+    """
+    # TODO: Can we just uniformly set __parent on nodes rather than doing it
+    # differently in different cases? I think the reason for the cases is that
+    # we used to set a parent ID rather than a parent pointer and not all nodes
+    # have IDs.
+    if node is not None:
+        id = node.get('id')
+        if id is not None:  # 0 is okay; it's the root node.
+            # Give anything in the map a parent:
+            node['__parent'] = parent  # Parents are used for (1) building longnames, (2) setting memberof (which we shouldn't have to do anymore; we can just traverse children), (3) getting the module a class or interface is defined in so we can link to it, and (4) one other things, so it's worth setting them.
+            index[id] = node
+
+        # Burrow into everything that could contain more ID'd items. We don't
+        # need setSignature or getSignature for now. Do we need indexSignature?
         for tag in ['children', 'signatures', 'parameters']:
             for child in node.get(tag, []):
-                self.make_node_list(child, node.get('id'))
-        typetag = node.get('type')
-        if isinstance(typetag, dict) and typetag['type'] != 'reference':
-            self.make_node_list(typetag, parent)
-        self.make_node_list(node.get('declaration'), None)
+                index_by_id(index, child, parent=node)
 
+        return index
 
-def parse_typedoc(inputfile):
-    """Parse and convert the typedoc JSON file to a list jsdoc entries"""
-    typedoc = TypeDoc(json.load(inputfile))
-    return typedoc.jsdoc
 
+def make_description(comment):
+    """Construct a single comment string from a fancy object."""
+    ret = '\n\n'.join(text for text in [comment.get('shortText'),
+                                        comment.get('text')]
+                      if text)
+    return ret.strip()
 
-def typedoc(inputname):
-    """
-    Read a typedoc file and print the resulting jsdoc list.
 
-    .. note::
+def member_properties(node):
+    flags = node.get('flags', {})
+    return dict(
+        is_abstract=flags.get('isAbstract', False),
+        is_optional=flags.get('isOptional', False),
+        is_static=flags.get('isStatic', False),
+        is_private=flags.get('isPrivate', False))
 
-       This function only exists to test this module in isolation.
-    """
-    with open(inputname, 'r') as inputfile:
-        json.dump(parse_typedoc(inputfile), sys.stdout, indent=2)
 
+def short_name(node):
+    if node['kindString'] in ['Module', 'External module']:
+        return node['name'][1:-1]  # strip quotes
+    return node['name']
+
+
+# Optimization: Could memoize this for probably a decent perf gain: every child
+# of an object redoes th work for all its parents.
+def make_path_segments(node, base_dir, child_was_static=None):
+    """Return the full, unambiguous list of path segments that points to an
+    entity described by a TypeDoc JSON node.
+
+    Example: ``['./', 'dir/', 'dir/', 'file.', 'object.', 'object#', 'object']``
 
-if __name__ == '__main__':
-    typedoc(sys.argv[1])
+    :arg base_dir: Absolute path of the dir relative to which file-path
+        segments are constructed
+    :arg child_was_static: True if the child node we're computing the path of
+        is a static member of the node under consideration. False if it is.
+        None if the current node is the one we're ultimately computing the path
+        of.
+
+    TypeDoc uses a totally different, locality-sensitive resolution mechanism
+    for links: https://typedoc.org/guides/link-resolution/. It seems like a
+    less well thought-out system than JSDoc's namepaths, as it doesn't
+    distinguish between, say, static and instance properties of the same name.
+    (AFAICT, TypeDoc does not emit documentation for inner properties, as for a
+    function nested within another function.) We're sticking with our own
+    namepath-like paths, even if we eventually support {@link} syntax.
+
+    """
+    node_is_static = node.get('flags', {}).get('isStatic', False)
+
+    parent = node.get('__parent')
+    parent_segments = (make_path_segments(parent, base_dir, child_was_static=node_is_static)
+                       if parent else [])
+
+    kind = node.get('kindString')
+    delimiter = '' if child_was_static is None else '.'
+
+    # Handle the cases here that are handled in _convert_node(), plus any that
+    # are encountered on other nodes on the way up to the root.
+    if kind in ['Variable', 'Property', 'Accessor', 'Interface', 'Module']:
+        # We emit a segment for a Method's child Call Signature but skip the
+        # Method itself. They 2 nodes have the same names, but, by taking the
+        # child, we fortuitously end up without a trailing delimiter on our
+        # last segment.
+        segments = [node['name']]
+    elif kind in ['Call signature', 'Constructor signature']:
+        # Similar to above, we skip the parent Constructor and glom onto the
+        # Constructor Signature. That gets us no trailing delimiter. However,
+        # the signature has name == 'new Foo', so we go up to the parent to get
+        # the real name, which is usually (always?) "constructor".
+        segments = [parent['name']]
+    elif kind == 'Class':
+        segments = [node['name']]
+        if child_was_static is False:
+            delimiter = '#'
+    elif kind == 'External module':
+        # 'name' contains folder names if multiple folders are passed into
+        # TypeDoc. It's also got excess quotes. So we ignore it and take
+        # 'originalName', which has a nice, absolute path.
+        rel = relpath(node['originalName'], base_dir)
+        if not is_explicitly_rooted(rel):
+            rel = f'.{sep}{rel}'
+        segments = rel.split(sep)
+        filename = splitext(segments[-1])[0]
+        segments = [s + '/' for s in segments[:-1]] + [filename]
+    else:
+        # None, as for a root node, Constructor, or Method
+        segments = []
+
+    if segments:
+        # It's not some abstract thing the user doesn't think about and we skip
+        # over.
+        segments[-1] += delimiter
+        return parent_segments + segments
+    else:
+        # Allow some levels of the JSON to not have a corresponding path segment:
+        return parent_segments
diff --git a/tox.ini b/tox.ini
index 801404c2..8f8e05e4 100644
--- a/tox.ini
+++ b/tox.ini
@@ -26,6 +26,6 @@ commands = flake8 sphinx_js tests
 [flake8]
 # I101: the "pep8" import-order-style is advertised as not complaining about
 #   import order, but it does. Ignore it.
-ignore = E501, E127, W503, I101
+ignore = E501, E127, E302, E305, W503, I101
 import-order-style = pep8
 application-import-names = sphinx_js, tests


Test patch:
diff --git a/tests/source/docs/autoattribute_example.rst b/tests/source/docs/autoattribute_example.rst
deleted file mode 100644
index cd212a00..00000000
--- a/tests/source/docs/autoattribute_example.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-.. js:autoattribute:: ExampleAttribute
diff --git a/tests/source/docs/autoclass_example.rst b/tests/source/docs/autoclass_example.rst
deleted file mode 100644
index 816c8d90..00000000
--- a/tests/source/docs/autoclass_example.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-.. js:autoclass:: ExampleClass
diff --git a/tests/source/docs/autofunction_example.rst b/tests/source/docs/autofunction_example.rst
deleted file mode 100644
index 96955854..00000000
--- a/tests/source/docs/autofunction_example.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-.. js:autofunction:: exampleTag
diff --git a/tests/test_build/source/code.js b/tests/test_build/source/code.js
index a6862f1a..8f14a6f8 100644
--- a/tests/test_build/source/code.js
+++ b/tests/test_build/source/code.js
@@ -151,7 +151,7 @@ const ExampleAttribute = null;
 function destructuredParams(p1, {foo, bar}) {}
 
 /**
- * @param a_
+ * @param a_ Snorf
  * @param {type_} b Borf_
  * @returns {rtype_} Dorf_
  */
@@ -165,7 +165,7 @@ function injection() {}
  * @param [num=5]
  * @param [nil=null]
  */
-function defaultsDocumentedInDoclet(func, strNum, strBool, num, nil) {}
+function defaultsDocumentedInDoclet(func, str, strNum, strBool, num, nil) {}
 
 /**
  * @param [num]
diff --git a/tests/test_build/test_build.py b/tests/test_build/test_build.py
index d174c481..a5c79146 100644
--- a/tests/test_build/test_build.py
+++ b/tests/test_build/test_build.py
@@ -78,33 +78,22 @@ def test_autofunction_defaults_in_doclet(self):
             'defaultsDocumentedInDoclet(func=() => 5, str="a string with \\" quote", strNum="42", strBool="true", num=5, nil=null)\n\n'
             '   Arguments:\n'
             '      * **func** (*function*) --\n\n'
-            '      * **str** --\n\n'
             '      * **strNum** (*string*) --\n\n'
-            '      * **strBool** (*string*) --\n\n'
-            '      * **num** --\n\n'
-            '      * **nil** --\n')
+            '      * **strBool** (*string*) --\n')
 
     def test_autofunction_defaults_in_code(self):
         """Make sure param default values appear in the function definition,
         when defined in code."""
         self._file_contents_eq(
             'autofunction_defaults_code',
-            'defaultsDocumentedInCode(num=5, str="true", bool=true, nil=null)\n\n'
-            '   Arguments:\n'
-            '      * **num** --\n\n'
-            '      * **str** --\n\n'
-            '      * **bool** --\n\n'
-            '      * **nil** --\n')
+            'defaultsDocumentedInCode(num=5, str="true", bool=true, nil=null)\n')
 
     def test_autofunction_variadic(self):
         """Make sure variadic parameters are rendered as ellipses."""
         self._file_contents_eq(
             'autofunction_variadic',
             'variadicParameter(a, ...args)\n\n'
-            '   Variadic parameter\n\n'
-            '   Arguments:\n'
-            '      * **a** --\n\n'
-            '      * **args** --\n')
+            '   Variadic parameter\n')
 
     def test_autofunction_deprecated(self):
         """Make sure @deprecated tags can be documented with autofunction."""
@@ -144,7 +133,33 @@ def test_autoclass_members(self):
         """
         self._file_contents_eq(
             'autoclass_members',
-            u'class ContainingClass(ho)\n\n   Class doc.\n\n   Constructor doc.\n\n   Arguments:\n      * **ho** -- A thing\n\n   ContainingClass.anotherMethod()\n\n      Another.\n\n   ContainingClass.bar\n\n      Setting this also frobs the frobnicator.\n\n   ContainingClass.someMethod(hi)\n\n      Here.\n\n   ContainingClass.someVar\n\n      A var\n\n   ContainingClass.yetAnotherMethod()\n\n      More.\n')
+            'class ContainingClass(ho)\n\n'
+            '   Class doc.\n'
+            '\n'
+            '   Constructor doc.\n'
+            '\n'
+            '   Arguments:\n'
+            '      * **ho** -- A thing\n'
+            '\n'
+            '   ContainingClass.bar\n'
+            '\n'
+            '      Setting this also frobs the frobnicator.\n'
+            '\n'
+            '   ContainingClass.someVar\n'
+            '\n'
+            '      A var\n'
+            '\n'
+            '   ContainingClass.anotherMethod()\n'
+            '\n'
+            '      Another.\n'
+            '\n'
+            '   ContainingClass.someMethod(hi)\n'
+            '\n'
+            '      Here.\n'
+            '\n'
+            '   ContainingClass.yetAnotherMethod()\n'
+            '\n'
+            '      More.\n')
 
     def test_autoclass_members_list(self):
         """Make sure including a list of names after ``members`` limits it to
@@ -159,7 +174,34 @@ def test_autoclass_members_list_star(self):
         at that point."""
         self._file_contents_eq(
             'autoclass_members_list_star',
-            u'class ContainingClass(ho)\n\n   Class doc.\n\n   Constructor doc.\n\n   Arguments:\n      * **ho** -- A thing\n\n   ContainingClass.bar\n\n      Setting this also frobs the frobnicator.\n\n   ContainingClass.anotherMethod()\n\n      Another.\n\n   ContainingClass.someVar\n\n      A var\n\n   ContainingClass.yetAnotherMethod()\n\n      More.\n\n   ContainingClass.someMethod(hi)\n\n      Here.\n')
+            'class ContainingClass(ho)\n'
+            '\n'
+            '   Class doc.\n'
+            '\n'
+            '   Constructor doc.\n'
+            '\n'
+            '   Arguments:\n'
+            '      * **ho** -- A thing\n'
+            '\n'
+            '   ContainingClass.bar\n'
+            '\n'
+            '      Setting this also frobs the frobnicator.\n'
+            '\n'
+            '   ContainingClass.someVar\n'
+            '\n'
+            '      A var\n'
+            '\n'
+            '   ContainingClass.anotherMethod()\n'
+            '\n'
+            '      Another.\n'
+            '\n'
+            '   ContainingClass.yetAnotherMethod()\n'
+            '\n'
+            '      More.\n'
+            '\n'
+            '   ContainingClass.someMethod(hi)\n'
+            '\n'
+            '      Here.\n')
 
     def test_autoclass_alphabetical(self):
         """Make sure members sort alphabetically when not otherwise specified."""
@@ -273,7 +315,7 @@ def test_restructuredtext_injection(self):
             'injection',
             u'injection(a_, b)\n\n'
             '   Arguments:\n'
-            '      * **a_** --\n\n'
+            '      * **a_** -- Snorf\n\n'
             '      * **b** (*type_*) -- >>Borf_<<\n\n'
             '   Returns:\n'
             '      **rtype_** -- >>Dorf_<<\n')
@@ -290,8 +332,8 @@ def test_restructuredtext_injection(self):
       * **node** (*Node*) -- Something of a single type
 
    Throws:
-      **PartyError|FartyError** -- Something with multiple types and a
-      line that wraps
+      **PartyError | FartyError** -- Something with multiple types and
+      a line that wraps
 
    Returns:
       **Number** -- What a thing
diff --git a/tests/test_build_ts/source/class.ts b/tests/test_build_ts/source/class.ts
index ae709407..f7008862 100644
--- a/tests/test_build_ts/source/class.ts
+++ b/tests/test_build_ts/source/class.ts
@@ -26,4 +26,30 @@ class ClassDefinition {
     anotherMethod() {
 
     }
-}
\ No newline at end of file
+}
+
+interface Interface {
+}
+
+abstract class ClassWithSupersAndInterfacesAndAbstract extends ClassDefinition implements Interface {
+  /** I construct. */
+  constructor() {
+    super(8);
+  }
+}
+
+interface InterfaceWithSupers extends Interface {
+}
+
+export class ExportedClass {
+  constructor() {
+  }
+}
+
+class ConstructorlessClass {
+}
+
+interface OptionalThings {
+  foop?(): void;
+  boop?: boolean;
+}
diff --git a/tests/test_build_ts/source/docs/autoclass_class_with_interface_and_supers.rst b/tests/test_build_ts/source/docs/autoclass_class_with_interface_and_supers.rst
new file mode 100644
index 00000000..1d51b3a4
--- /dev/null
+++ b/tests/test_build_ts/source/docs/autoclass_class_with_interface_and_supers.rst
@@ -0,0 +1,1 @@
+.. js:autoclass:: ClassWithSupersAndInterfacesAndAbstract
diff --git a/tests/test_build_ts/source/docs/autoclass_constructorless.rst b/tests/test_build_ts/source/docs/autoclass_constructorless.rst
new file mode 100644
index 00000000..e2dba4e7
--- /dev/null
+++ b/tests/test_build_ts/source/docs/autoclass_constructorless.rst
@@ -0,0 +1,1 @@
+.. js:autoclass:: ConstructorlessClass
diff --git a/tests/test_build_ts/source/docs/autoclass_exported.rst b/tests/test_build_ts/source/docs/autoclass_exported.rst
new file mode 100644
index 00000000..7761106f
--- /dev/null
+++ b/tests/test_build_ts/source/docs/autoclass_exported.rst
@@ -0,0 +1,1 @@
+.. js:autoclass:: ExportedClass
diff --git a/tests/test_build_ts/source/docs/autoclass_interface_optionals.rst b/tests/test_build_ts/source/docs/autoclass_interface_optionals.rst
new file mode 100644
index 00000000..83d474ab
--- /dev/null
+++ b/tests/test_build_ts/source/docs/autoclass_interface_optionals.rst
@@ -0,0 +1,2 @@
+.. js:autoclass:: OptionalThings
+   :members:
diff --git a/tests/test_build_ts/source/docs/index.rst b/tests/test_build_ts/source/docs/index.rst
index 11aff71a..ec0517c7 100644
--- a/tests/test_build_ts/source/docs/index.rst
+++ b/tests/test_build_ts/source/docs/index.rst
@@ -1,2 +1,9 @@
-.. js:autoclass:: ClassDefinition
-   :members:
\ No newline at end of file
+.. js:autoclass:: class.ClassDefinition
+   :members:
+
+   This class name includes an explicit "class." prefix so the link from the superclass in autoclass_class_with_interface_and_supers.rst will point here.
+
+.. js:autoclass:: class.Interface
+   :members:
+
+   Same here.
diff --git a/tests/test_build_ts/source/tsconfig.json b/tests/test_build_ts/source/tsconfig.json
index 38d3c7c5..cea13d00 100644
--- a/tests/test_build_ts/source/tsconfig.json
+++ b/tests/test_build_ts/source/tsconfig.json
@@ -4,4 +4,4 @@
       "module": "commonjs",
       "moduleResolution": "node"
     }
-  }
\ No newline at end of file
+  }
diff --git a/tests/test_build_ts/test_build_ts.py b/tests/test_build_ts/test_build_ts.py
index 7eca053b..1371c0d1 100644
--- a/tests/test_build_ts/test_build_ts.py
+++ b/tests/test_build_ts/test_build_ts.py
@@ -2,8 +2,8 @@
 from tests.testing import SphinxBuildTestCase
 
 
-class Tests(SphinxBuildTestCase):
-    """Tests which require our one big Sphinx tree to be built (typescript version)"""
+class TextBuilderTests(SphinxBuildTestCase):
+    """Tests which require our one big Sphinx tree to be built (as text)"""
 
     def test_autoclass_constructor(self):
         """Make sure class constructor comes before methods."""
@@ -27,3 +27,77 @@ def test_autoclass_star_order(self):
         pos_field = contents.index('ClassDefinition.field')
         pos_method2 = contents.index('ClassDefinition.anotherMethod')
         assert pos_method < pos_field < pos_method2, 'Methods and fields are not in right order in ' + contents
+
+    def test_abstract_extends_and_implements(self):
+        """Make sure the template correctly represents abstract classes,
+        classes with superclasses, and classes that implement interfaces.
+
+        These are all TypeScript-specific features.
+
+        """
+        # The quotes around ClassDefinition() must be some weird decision in
+        # Sphinx's text output. I don't care if they go away in a future
+        # version of Sphinx. It doesn't affect the HTML output.
+        self._file_contents_eq(
+            'autoclass_class_with_interface_and_supers',
+            'class ClassWithSupersAndInterfacesAndAbstract()\n'
+            '\n'
+            '   *abstract*\n'
+            '\n'
+            '   **Extends:**\n'
+            '      * "ClassDefinition()"\n'
+            '\n'
+            '   **Implements:**\n'
+            '      * "Interface()"\n'
+            '\n'
+            '   I construct.\n')
+
+    def test_exported_from(self):
+        """Make sure classes say where they were exported from.
+
+        I'm divided on whether this is even useful. Maybe people should just
+        specify full.path.Names in the js:autoclass directives if they want to
+        surface that info.
+
+        """
+        self._file_contents_eq(
+            'autoclass_exported',
+            'class ExportedClass()\n'
+            '\n'
+            '   *exported from* "class"\n')
+
+    def test_constructorless_class(self):
+        """Make sure constructorless classes don't crash the renderer."""
+        self._file_contents_eq(
+            'autoclass_constructorless',
+            'class ConstructorlessClass()\n')
+
+    def test_optional_members(self):
+        """Make sure optional attributes and functions of interfaces get
+        question marks sticking out of them."""
+        self._file_contents_eq(
+            'autoclass_interface_optionals',
+            'class OptionalThings()\n'
+            '\n'
+            '   *interface*\n'
+            '\n'
+            '   OptionalThings.boop?\n'
+            '\n'
+            '      **type:** boolean\n'
+            '\n'
+            '   OptionalThings.foop?()\n')
+
+
+class HtmlBuilderTests(SphinxBuildTestCase):
+    """Tests which require an HTML build of our Sphinx tree, for checking
+    links"""
+
+    builder = 'html'
+
+    def test_extends_links(self):
+        """Make sure superclass mentions link to their definitions."""
+        assert 'href="index.html#class.ClassDefinition"' in self._file_contents('autoclass_class_with_interface_and_supers')
+
+    def test_implements_links(self):
+        """Make sure implemented interfaces link to their definitions."""
+        assert 'href="index.html#class.Interface"' in self._file_contents('autoclass_class_with_interface_and_supers')
diff --git a/tests/test_doclets.py b/tests/test_doclets.py
deleted file mode 100644
index daebdc1d..00000000
--- a/tests/test_doclets.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# -*- coding: utf-8 -*-
-import pytest
-from sphinx.errors import SphinxError
-
-from sphinx_js.doclets import doclet_full_path, root_or_fallback
-
-
-def test_doclet_full_path():
-    """Sanity-check doclet_full_path(), including throwing it a non-.js filename."""
-    doclet = {
-        'meta': {
-            'filename': 'utils.jsm',
-            'path': '/boogie/smoo/Checkouts/fathom',
-        },
-        'longname': 'best#thing~yeah'
-    }
-    assert doclet_full_path(doclet, '/boogie/smoo/Checkouts') == [
-        './',
-        'fathom/',
-        'utils.',
-        'best#',
-        'thing~',
-        'yeah',
-    ]
-
-
-def test_relative_path_root():
-    """Make sure the computation of the root path for relative JS entity
-    pathnames is right."""
-    # Fall back to the only source path if not specified.
-    assert root_or_fallback(None, ['a']) == 'a'
-    with pytest.raises(SphinxError):
-        root_or_fallback(None, ['a', 'b'])
-    assert root_or_fallback('smoo', ['a']) == 'smoo'
diff --git a/tests/test_init.py b/tests/test_init.py
new file mode 100644
index 00000000..90697111
--- /dev/null
+++ b/tests/test_init.py
@@ -0,0 +1,14 @@
+import pytest
+from sphinx.errors import SphinxError
+
+from sphinx_js import root_or_fallback
+
+
+def test_relative_path_root():
+    """Make sure the computation of the root path for relative JS entity
+    pathnames is right."""
+    # Fall back to the only source path if not specified.
+    assert root_or_fallback(None, ['a']) == 'a'
+    with pytest.raises(SphinxError):
+        root_or_fallback(None, ['a', 'b'])
+    assert root_or_fallback('smoo', ['a']) == 'smoo'
diff --git a/tests/test_ir.py b/tests/test_ir.py
new file mode 100644
index 00000000..0d53d451
--- /dev/null
+++ b/tests/test_ir.py
@@ -0,0 +1,20 @@
+import pytest
+
+from sphinx_js.ir import Param
+
+
+def test_default():
+    """Accessing ``.default`` on a Param having a default should return the
+    default value."""
+    p = Param(name='fred',
+              has_default=True,
+              default='boof')
+    assert p.default == 'boof'
+
+
+def test_missing_default():
+    """Constructing a Param with ``has_default=True`` but without a ``default``
+    value should raise an error."""
+    with pytest.raises(ValueError):
+        Param(name='fred',
+              has_default=True)
diff --git a/tests/test_jsdoc/source/class.js b/tests/test_jsdoc/source/class.js
new file mode 100644
index 00000000..b34f8e47
--- /dev/null
+++ b/tests/test_jsdoc/source/class.js
@@ -0,0 +1,31 @@
+/**
+ * Class doc.
+ *
+ * @example Example in class
+ */
+class Foo {
+    /**
+     * Constructor doc.
+     *
+     * @arg ho A thing
+     * @example Example in constructor
+     */
+    constructor(ho) {}
+
+    /**
+     * Setting this also frobs the frobnicator.
+     */
+    get bar() {
+      return this._bar;
+    }
+    set bar(baz) {
+      this._bar = _bar;
+    }
+
+    /**
+     * Private method.
+     *
+     * @private
+     */
+    secret() {}
+}
diff --git a/tests/test_jsdoc/source/function.js b/tests/test_jsdoc/source/function.js
new file mode 100644
index 00000000..f1139454
--- /dev/null
+++ b/tests/test_jsdoc/source/function.js
@@ -0,0 +1,10 @@
+/**
+ * Function foo.
+ *
+ * @param {String} bar Which bar
+ * @param baz
+ * @return {Number} How many things
+ *     there are
+ * @exception ExplosionError It went boom.
+ */
+function foo(bar, baz=8) {}
diff --git a/tests/test_jsdoc/test_jsdoc.py b/tests/test_jsdoc/test_jsdoc.py
new file mode 100644
index 00000000..99a0af20
--- /dev/null
+++ b/tests/test_jsdoc/test_jsdoc.py
@@ -0,0 +1,105 @@
+from sphinx_js.ir import Attribute, Exc, Function, Param, Pathname, Return
+from sphinx_js.jsdoc import full_path_segments
+from tests.testing import JsDocTestCase
+
+
+def test_doclet_full_path():
+    """Sanity-check full_path_segments(), including throwing it a non-.js filename."""
+    doclet = {
+        'meta': {
+            'filename': 'utils.jsm',
+            'path': '/boogie/smoo/Checkouts/fathom',
+        },
+        'longname': 'best#thing~yeah'
+    }
+    assert full_path_segments(doclet, '/boogie/smoo/Checkouts') == [
+        './',
+        'fathom/',
+        'utils.',
+        'best#',
+        'thing~',
+        'yeah',
+    ]
+
+
+class FunctionTests(JsDocTestCase):
+    file = 'function.js'
+
+    def test_top_level_and_function(self):
+        """Test Function (and thus also TopLevel) analysis.
+
+        This also includes exceptions, returns, params, and default values.
+
+        """
+        function = self.analyzer.get_object(['foo'], 'function')
+        assert function == Function(
+            name='foo',
+            path=Pathname(['./', 'function.', 'foo']),
+            filename='function.js',
+            description='Function foo.',
+            line=10,
+            deprecated=False,
+            examples=[],
+            see_alsos=[],
+            properties=[],
+            is_private=False,
+            exported_from=None,
+            is_abstract=False,
+            is_optional=False,
+            is_static=False,
+            params=[Param(name='bar',
+                          description='Which bar',
+                          has_default=False,
+                          is_variadic=False,
+                          type='String'),
+                    Param(name='baz',
+                          description='',
+                          has_default=True,
+                          default='8',
+                          is_variadic=False,
+                          type=None)],
+            exceptions=[Exc(type=None,
+                            description='ExplosionError It went boom.')],
+            returns=[
+                Return(type='Number',
+                       description='How many things there are')])  # Test text unwrapping.
+
+
+class ClassTests(JsDocTestCase):
+    file = 'class.js'
+
+    def test_class(self):
+        """Test Class analysis, including members, attributes, and privacy."""
+        cls = self.analyzer.get_object(['Foo'], 'class')
+        assert cls.name == 'Foo'
+        assert cls.path == Pathname(['./', 'class.', 'Foo'])
+        assert cls.filename == 'class.js'
+        assert cls.description == 'Class doc.'
+        assert cls.line == 13  # Not ideal, as it refers to the constructor, but we'll allow it
+        assert cls.examples == ['Example in constructor']  # We ignore examples and other fields from the class doclet so far. This could change someday.
+
+        # Members:
+        getter, private_method = cls.members  # default constructor not included here
+        assert isinstance(private_method, Function)
+        assert private_method.name == 'secret'
+        assert private_method.path == Pathname(['./', 'class.', 'Foo#', 'secret'])
+        assert private_method.description == 'Private method.'
+        assert private_method.is_private is True
+        assert isinstance(getter, Attribute)
+        assert getter.name == 'bar'
+        assert getter.path == Pathname(['./', 'class.', 'Foo#', 'bar'])
+        assert getter.filename == 'class.js'
+        assert getter.description == 'Setting this also frobs the frobnicator.'
+
+        # Constructor:
+        constructor = cls.constructor
+        assert constructor.name == 'Foo'
+        assert constructor.path == Pathname(['./', 'class.', 'Foo'])  # Same path as class. This might differ in different languages.
+        assert constructor.filename == 'class.js'
+        assert constructor.description == 'Constructor doc.'
+        assert constructor.examples == ['Example in constructor']
+        assert constructor.params == [Param(name='ho',
+                                            description='A thing',
+                                            has_default=False,
+                                            is_variadic=False,
+                                            type=None)]
diff --git a/tests/test_testing.py b/tests/test_testing.py
new file mode 100644
index 00000000..ab1288d2
--- /dev/null
+++ b/tests/test_testing.py
@@ -0,0 +1,8 @@
+from tests.testing import dict_where, NO_MATCH
+
+
+def test_dict_where():
+    json = {'hi': 'there', 'more': {'mister': 'zangler', 'and': 'friends'}}
+    assert dict_where(json, mister='zangler') == {'mister': 'zangler', 'and': 'friends'}
+    assert dict_where(json, mister='zangler', fee='foo') == NO_MATCH
+    assert dict_where(json, hi='there') == json
diff --git a/tests/test_typedoc.py b/tests/test_typedoc.py
deleted file mode 100644
index 521f7b09..00000000
--- a/tests/test_typedoc.py
+++ /dev/null
@@ -1,69 +0,0 @@
-# -*- coding: utf-8 -*-
-import os
-import subprocess
-import shutil
-import json
-from tempfile import mkdtemp
-
-import pytest
-
-from sphinx_js.doclets import program_name_on_this_platform
-from sphinx_js.typedoc import TypeDoc, parse_typedoc
-
-TS_SOURCE_DIR = os.path.join(os.path.dirname(__file__), 'typescript')
-TS_FILE_LIST = list(filter(lambda x: x.endswith('.ts'), os.listdir(TS_SOURCE_DIR)))
-
-
-@pytest.fixture(scope='module')
-def typedoc():
-    tmpdir = mkdtemp()
-
-    def inner_func(source):
-        outfile = os.path.join(tmpdir, source + '.json')
-        typedoc_command_name = program_name_on_this_platform('typedoc')
-
-        subprocess.call([
-            typedoc_command_name,
-            '--out', tmpdir,
-            '--ignoreCompilerErrors',
-            '--target', 'ES6',
-            '--json', outfile,
-            os.path.join(TS_SOURCE_DIR, source)
-        ])
-        return outfile
-    yield inner_func
-    shutil.rmtree(tmpdir)
-
-
-@pytest.fixture(params=TS_FILE_LIST)
-def references(request, typedoc):
-    source = request.param
-    with open(typedoc(source), 'r') as typedocfile:
-        jsdoc = parse_typedoc(typedocfile)
-    jsdoc_ref_file = os.path.join(TS_SOURCE_DIR, source + '.jsdoc')
-    if not os.path.exists(jsdoc_ref_file):
-        # When a reference file is missing, this is probably a new test.
-        # Generate a reference file for the developer to review.  If the
-        # generated reference is good, just remove the '.ref' extension.
-        with open(jsdoc_ref_file + '.ref', 'w') as jsdocfile:
-            json.dump(
-                jsdoc,
-                jsdocfile,
-                sort_keys=True,
-                indent=4,
-                separators=(',', ': '))
-        print('wrote %s' % jsdoc_ref_file)
-    else:
-        with open(jsdoc_ref_file, 'r') as jsdocfile:
-            jsdoc_ref = json.load(jsdocfile)
-        return {'jsdoc': jsdoc, 'jsdoc_ref': jsdoc_ref}
-
-
-def test_empty():
-    json = {}
-    jsdoc = TypeDoc(json).jsdoc
-    assert jsdoc == []
-
-
-def test_references(references):
-    assert references['jsdoc'] == references['jsdoc_ref']
diff --git a/tests/test_typedoc_analysis/source/nodes.ts b/tests/test_typedoc_analysis/source/nodes.ts
new file mode 100644
index 00000000..a05def40
--- /dev/null
+++ b/tests/test_typedoc_analysis/source/nodes.ts
@@ -0,0 +1,55 @@
+class Superclass {
+  method() {
+  }
+}
+
+interface SuperInterface {
+}
+
+interface Interface extends SuperInterface {
+}
+
+interface InterfaceWithMembers {
+  callableProperty(): void;
+}
+
+/**
+ * An empty subclass
+ */
+export abstract class EmptySubclass extends Superclass implements Interface {
+}
+
+const topLevelConst = 3;
+
+/**
+ * @param a Some number
+ * @param b Some strings
+ * @return The best number
+ */
+function func(a: number = 1, ...b: string[]): number {
+  return 4;
+}
+
+class ClassWithProperties {
+  static someStatic: number;
+  someOptional?: number;
+  private somePrivate: number;
+  /**
+   * This is totally normal!
+   */
+  someNormal: number;
+
+  constructor(a: number) {
+  }
+
+  get gettable(): number {
+    return 5;
+  }
+
+  set settable(value: string) {
+  }
+}
+
+class Indexable {
+  [id:string]: any;  // smoketest
+}
diff --git a/tests/test_typedoc_analysis/source/pathSegments.ts b/tests/test_typedoc_analysis/source/pathSegments.ts
new file mode 100644
index 00000000..39efbeb5
--- /dev/null
+++ b/tests/test_typedoc_analysis/source/pathSegments.ts
@@ -0,0 +1,77 @@
+/**
+ * Function
+ */
+function foo(): void {
+  /**
+   * An inner function
+   */
+  function inner(): void {
+  }
+}
+foo.adHocInner = 'innerValue';
+
+/**
+ * Foo class
+ */
+class Foo {
+  /**
+   * Static member
+   */
+  static staticMember = 8;
+
+  /**
+   * Num instance var
+   */
+  numInstanceVar: number;
+
+  /**
+   * Weird var
+   */
+  "weird#Var": number;
+
+  /**
+   * Constructor
+   */
+  constructor (num: number) {
+    this.numInstanceVar = num;
+  }
+
+  /**
+   * Method
+   */
+  someMethod(): void {
+  }
+
+  /**
+   * Static method
+   */
+  static staticMethod(): void {
+  }
+
+  /**
+   * Getter
+   */
+  get getter(): number {
+    return 5;
+  }
+
+  /**
+   * Setter
+   */
+  set setter(n: number) {
+  }
+}
+
+interface Face {
+  /**
+   * Interface property
+   */
+  moof: string;
+}
+
+namespace SomeSpace {
+  /**
+   * Namespaced number
+   */
+  const spacedNumber = 4;
+}
diff --git a/tests/test_typedoc_analysis/source/tsconfig.json b/tests/test_typedoc_analysis/source/tsconfig.json
new file mode 100644
index 00000000..cea13d00
--- /dev/null
+++ b/tests/test_typedoc_analysis/source/tsconfig.json
@@ -0,0 +1,7 @@
+{
+    "compilerOptions": {
+      "target": "es6",
+      "module": "commonjs",
+      "moduleResolution": "node"
+    }
+  }
diff --git a/tests/test_typedoc_analysis/source/types.ts b/tests/test_typedoc_analysis/source/types.ts
new file mode 100644
index 00000000..f7ad5b3c
--- /dev/null
+++ b/tests/test_typedoc_analysis/source/types.ts
@@ -0,0 +1,106 @@
+// Basic types: https://www.typescriptlang.org/docs/handbook/basic-types.html
+
+enum Color{
+  Red = 1,
+  Green = 2
+}
+
+let bool: boolean;
+let num: number;
+let str: string;
+let array: number[];
+let genericArray: Array<number>;
+let tuple: [string, number];
+let color: Color;
+let unk: unknown;
+let whatever: any;
+let voidy: void;
+let undef: undefined;
+let nully: null;
+let nev: never;
+let obj: object;
+let sym: symbol;
+
+
+// Interfaces (https://www.typescriptlang.org/docs/handbook/interfaces.html)
+
+interface Interface {
+  readonly readOnlyNum: number;
+  [someProp: number]: string;  // Just a smoketest for now. (IOW, make sure the analysis engine doesn't crash on it.) We'll need more work to handle members with no names.
+}
+
+function interfacer(a: Interface) {
+}
+
+interface FunctionInterface {
+  (thing: string, ding: number): boolean;  // just a smoketest for now
+}
+
+// Functions. Basic function types are covered by ConvertNodeTests.test_function.
+
+function noThis(this: void) {  // smoketest
+}
+
+// Make sure multi-signature functions don't crash us:
+function overload(x: string[]): number;
+function overload(x: number): number;
+function overload(x): any {}
+
+// Literal types (https://www.typescriptlang.org/docs/handbook/literal-types.html)
+
+type CertainNumbers = 1 | 2 | 4;
+let certainNumbers: CertainNumbers = 2;
+
+// Unions and intersections (https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html)
+
+let union: number | string | Color = Color.Red;
+
+interface FooHaver {
+  foo: string;
+}
+
+interface BarHaver {
+  bar: string;
+}
+
+let intersection: FooHaver & BarHaver;
+
+// Generics (https://www.typescriptlang.org/docs/handbook/generics.html)
+
+function aryIdentity<T>(things: T[]): T[] {
+  console.log(things.length);
+  return things;
+}
+
+class GenericNumber<T> {
+  add: (x: T, y: T) => T;
+}
+
+// Generic constraints:
+
+interface Lengthwise {
+  length: number;
+}
+
+function constrainedIdentity<T extends Lengthwise>(arg: T): T {
+  return arg;
+}
+
+function getProperty<T, K extends keyof T>(obj: T, key: K) {
+  return obj[key];
+}
+
+function create<T>(c: { new (): T }): T {
+  return new c();
+}
+
+// Utility types (https://www.typescriptlang.org/docs/handbook/utility-types.html)
+
+let partial: Partial<string>;
+
+// Complex: nested nightmares that show our ability to handle compound typing constructs
+
+function objProps(a: {label: string}) {
+}
+
+let option: {a: number; b?: string};
diff --git a/tests/test_typedoc_analysis/test_typedoc_analysis.py b/tests/test_typedoc_analysis/test_typedoc_analysis.py
new file mode 100644
index 00000000..68b0c9eb
--- /dev/null
+++ b/tests/test_typedoc_analysis/test_typedoc_analysis.py
@@ -0,0 +1,407 @@
+from json import loads
+from os.path import dirname
+from unittest import TestCase
+
+import pytest
+
+from sphinx_js.ir import Attribute, Class, Function, Param, Pathname, Return
+from sphinx_js.typedoc import index_by_id, make_path_segments
+from tests.testing import dict_where, NO_MATCH, TypeDocAnalyzerTestCase, TypeDocTestCase
+
+
+class IndexByIdTests(TestCase):
+    def test_top_level_function(self):
+        """Make sure nodes get indexed."""
+        # A simple TypeDoc JSON dump of a soruce file with a single, top-level
+        # function with no params or return value:
+        json = loads(r"""{
+          "id": 0,
+          "name": "misterRoot",
+          "children": [
+            {
+              "id": 1,
+              "name": "\"longnames\"",
+              "kindString": "External module",
+              "originalName": "/a/b/c/tests/test_typedoc_analysis/source/longnames.ts",
+              "children": [
+                {
+                  "id": 2,
+                  "name": "foo",
+                  "kindString": "Function",
+                  "signatures": [
+                    {
+                      "id": 3,
+                      "name": "foo",
+                      "kindString": "Call signature",
+                      "comment": {
+                        "shortText": "Foo function."
+                      },
+                      "type": {
+                        "type": "intrinsic",
+                        "name": "void"
+                      }
+                    }
+                  ],
+                  "sources": [
+                    {
+                      "fileName": "longnames.ts",
+                      "line": 4,
+                      "character": 12
+                    }
+                  ]
+                }
+              ],
+              "groups": [
+                {
+                  "title": "Functions",
+                  "children": [
+                    2
+                  ]
+                }
+              ],
+              "sources": [
+                {
+                  "fileName": "longnames.ts",
+                  "line": 1,
+                  "character": 0
+                }
+              ]
+            }
+          ],
+          "groups": [
+            {
+              "title": "External modules",
+              "children": [
+                1
+              ]
+            }
+          ]
+        }""")
+        index = index_by_id({}, json)
+        # Things get indexed by ID:
+        function = index[2]
+        assert function['name'] == 'foo'
+        # Things get parent links:
+        assert function['__parent']['name'] == '"longnames"'
+        assert function['__parent']['__parent']['name'] == 'misterRoot'
+        # Root gets indexed by ID:
+        root = index[0]
+        assert root['name'] == 'misterRoot'
+        # Root parent link is absent or None:
+        assert root.get('__parent') is None
+
+
+class PathSegmentsTests(TypeDocTestCase):
+    """Make sure ``make_path_segments() `` works on all its manifold cases."""
+
+    files = ['pathSegments.ts']
+
+    def commented_object(self, comment, **kwargs):
+        """Return the object from ``json`` having the given comment short-text."""
+        return dict_where(self.json, comment={'shortText': comment}, **kwargs)
+
+    def commented_object_path(self, comment, **kwargs):
+        """Return the path segments of the object with the given comment."""
+        obj = self.commented_object(comment, **kwargs)
+        if obj is NO_MATCH:
+            raise RuntimeError(f'No object found with the comment "{comment}".')
+        return make_path_segments(obj, self._source_dir)
+
+    def test_class(self):
+        assert self.commented_object_path('Foo class') == ['./', 'pathSegments.', 'Foo']
+
+    def test_instance_property(self):
+        assert self.commented_object_path('Num instance var') == ['./', 'pathSegments.', 'Foo#', 'numInstanceVar']
+
+    def test_static_property(self):
+        assert self.commented_object_path('Static member') == ['./', 'pathSegments.', 'Foo.', 'staticMember']
+
+    def test_interface_property(self):
+        assert self.commented_object_path('Interface property') == ['./', 'pathSegments.', 'Face.', 'moof']
+
+    def test_weird_name(self):
+        """Make sure property names that themselves contain delimiter chars
+        like #./~ get their pathnames built correctly."""
+        assert self.commented_object_path('Weird var') == ['./', 'pathSegments.', 'Foo#', 'weird#Var']
+
+    def test_getter(self):
+        assert self.commented_object_path('Getter') == ['./', 'pathSegments.', 'Foo#', 'getter']
+
+    def test_setter(self):
+        assert self.commented_object_path('Setter') == ['./', 'pathSegments.', 'Foo#', 'setter']
+
+    def test_method(self):
+        assert self.commented_object_path('Method') == ['./', 'pathSegments.', 'Foo#', 'someMethod']
+
+    def test_static_method(self):
+        """Since ``make_path_segments()`` looks at the inner Call Signature,
+        make sure the flags (which determine staticness) are on the node we
+        expect."""
+        assert self.commented_object_path('Static method') == ['./', 'pathSegments.', 'Foo.', 'staticMethod']
+
+    def test_constructor(self):
+        # Pass the kindString so we're sure to find the signature (which is
+        # what convert_nodes() passes to make_path_segments()) rather than the
+        # constructor itself. They both have the same comments.
+        #
+        # Constructors get a #. They aren't static; they can see ``this``.
+        assert self.commented_object_path('Constructor', kindString='Constructor signature') == ['./', 'pathSegments.', 'Foo#', 'constructor']
+
+    def test_function(self):
+        assert self.commented_object_path('Function') == ['./', 'pathSegments.', 'foo']
+
+    def test_relative_paths(self):
+        """Make sure FS path segments are emitted if ``base_dir`` doesn't
+        directly contain the code."""
+        obj = self.commented_object('Function')
+        segments = make_path_segments(obj, dirname(dirname(self._source_dir)))
+        assert segments == ['./', 'test_typedoc_analysis/', 'source/', 'pathSegments.', 'foo']
+
+    def test_namespaced_var(self):
+        """Make sure namespaces get into the path segments."""
+        assert self.commented_object_path('Namespaced number') == ['./', 'pathSegments.', 'SomeSpace.', 'spacedNumber']
+
+
+class ConvertNodeTests(TypeDocAnalyzerTestCase):
+    """Test all the branches of ``convert_node()`` by analyzing every kind of
+    TypeDoc JSON object."""
+
+    files = ['nodes.ts']
+
+    def test_class(self):
+        """Test that superclasses, implemented interfaces, abstractness, and
+        nonexistent constructors, members, and top-level attrs are surfaced."""
+        # Make sure is_abstract is sometimes false:
+        super = self.analyzer.get_object(['Superclass'])
+        assert not super.is_abstract
+
+        # There should be a single member representing method():
+        method, = super.members
+        assert isinstance(method, Function)
+        assert method.name == 'method'
+
+        # Class-specific attrs:
+        subclass = self.analyzer.get_object(['EmptySubclass'])
+        assert isinstance(subclass, Class)
+        assert subclass.constructor is None
+        assert subclass.is_abstract
+        assert subclass.interfaces == [Pathname(['./', 'nodes.', 'Interface'])]
+
+        # _MembersAndSupers attrs:
+        assert subclass.supers == [Pathname(['./', 'nodes.', 'Superclass'])]
+        assert subclass.members == []
+
+        # TopLevel attrs. This should cover them for other kinds of objs as
+        # well (if node structures are the same across object kinds), since we
+        # have the filling of them factored out.
+        assert subclass.name == 'EmptySubclass'
+        assert subclass.path == Pathname(['./', 'nodes.', 'EmptySubclass'])
+        assert subclass.filename == 'nodes.ts'
+        assert subclass.description == 'An empty subclass'
+        assert subclass.deprecated is False
+        assert subclass.examples == []
+        assert subclass.see_alsos == []
+        assert subclass.properties == []
+        assert subclass.exported_from == Pathname(['./', 'nodes'])
+
+    def test_interface(self):
+        """Test that interfaces get indexed and have their supers exposed.
+
+        Members and top-level properties should be covered in test_class()
+        assuming node structure is the same as for classes.
+
+        """
+        interface = self.analyzer.get_object(['Interface'])
+        assert interface.supers == [Pathname(['./', 'nodes.', 'SuperInterface'])]
+
+    def test_interface_function_member(self):
+        """Make sure function-like properties are understood."""
+        obj = self.analyzer.get_object(['InterfaceWithMembers'])
+        prop = obj.members[0]
+        assert isinstance(prop, Function)
+        assert prop.name == 'callableProperty'
+
+    def test_variable(self):
+        """Make sure top-level consts and vars are found."""
+        const = self.analyzer.get_object(['topLevelConst'])
+        assert const.type == 'number'
+
+    def test_function(self):
+        """Make sure Functions, Params, and Returns are built properly for
+        top-level functions.
+
+        This covers a few simple function typing cases as well.
+
+        """
+        func = self.analyzer.get_object(['func'])
+        assert isinstance(func, Function)
+        assert func.params == [
+            Param(name='a',
+                  description='Some number',
+                  has_default=True,
+                  is_variadic=False,
+                  type='number',
+                  default='1'),
+            Param(name='b',
+                  description='Some strings',
+                  has_default=False,
+                  is_variadic=True,
+                  type='string[]')]
+        assert func.exceptions == []
+        assert func.returns == [Return(type='number', description='The best number')]
+
+    def test_constructor(self):
+        """Make sure constructors get attached to classes and analyzed into
+        Functions.
+
+        The rest of their analysis should share a code path with functions.
+
+        """
+        cls = self.analyzer.get_object(['ClassWithProperties'])
+        assert isinstance(cls.constructor, Function)
+
+    def test_properties(self):
+        """Make sure properties are hooked onto classes and expose their
+        flags."""
+        cls = self.analyzer.get_object(['ClassWithProperties'])
+        # The properties are on the class and are Attributes:
+        assert len([m for m in cls.members
+                    if isinstance(m, Attribute)
+                    and m.name in ['someStatic', 'someOptional', 'somePrivate', 'someNormal']]) == 4
+        # The unique things about properties (over and above Variables) are set
+        # right:
+        assert self.analyzer.get_object(['ClassWithProperties.', 'someStatic']).is_static
+        assert self.analyzer.get_object(['ClassWithProperties#', 'someOptional']).is_optional
+        assert self.analyzer.get_object(['ClassWithProperties#', 'somePrivate']).is_private
+        normal_property = self.analyzer.get_object(['ClassWithProperties#', 'someNormal'])
+        assert (not normal_property.is_optional and
+                not normal_property.is_static and
+                not normal_property.is_abstract and
+                not normal_property.is_private)
+
+    def test_getter(self):
+        """Test that we represent getters as Attributes and find their return
+        types."""
+        getter = self.analyzer.get_object(['gettable'])
+        assert isinstance(getter, Attribute)
+        assert getter.type == 'number'
+
+    def test_setter(self):
+        """Test that we represent setters as Attributes and find the type of
+        their 1 param."""
+        setter = self.analyzer.get_object(['settable'])
+        assert isinstance(setter, Attribute)
+        assert setter.type == 'string'
+
+
+class TypeNameTests(TypeDocAnalyzerTestCase):
+    """Make sure our rendering of TypeScript types into text works."""
+
+    files = ['types.ts']
+
+    def test_basic(self):
+        """Test intrinsic types."""
+        for obj_name, type_name in [
+                ('bool', 'boolean'),
+                ('num', 'number'),
+                ('str', 'string'),
+                ('array', 'number[]'),
+                ('genericArray', 'Array<number>'),
+                ('tuple', '[string, number]'),
+                ('color', 'Color'),
+                ('unk', 'unknown'),
+                ('whatever', 'any'),
+                ('voidy', 'void'),
+                ('undef', 'undefined'),
+                ('nully', 'null'),
+                ('nev', 'never'),
+                ('obj', 'object'),
+                ('sym', 'symbol')]:
+            obj = self.analyzer.get_object([obj_name])
+            assert obj.type == type_name
+
+    def test_named_interface(self):
+        """Make sure interfaces can be referenced by name."""
+        obj = self.analyzer.get_object(['interfacer'])
+        assert obj.params[0].type == 'Interface'
+
+    def test_interface_readonly_member(self):
+        """Make sure the readonly modifier doesn't keep us from computing the
+        type of a property."""
+        obj = self.analyzer.get_object(['Interface'])
+        read_only_num = obj.members[0]
+        assert read_only_num.name == 'readOnlyNum'
+        assert read_only_num.type == 'number'
+
+    def test_array(self):
+        """Make sure array types are rendered correctly.
+
+        As a bonus, make sure we grab the first signature of an overloaded
+        function.
+
+        """
+        obj = self.analyzer.get_object(['overload'])
+        assert obj.params[0].type == 'string[]'
+
+    def test_literal_types(self):
+        """Make sure a thing of a named literal type has that type name
+        attached."""
+        obj = self.analyzer.get_object(['certainNumbers'])
+        assert obj.type == 'CertainNumbers'
+
+    def test_unions(self):
+        """Make sure unions get rendered properly."""
+        obj = self.analyzer.get_object(['union'])
+        assert obj.type == 'number | string | Color'
+
+    def test_intersection(self):
+        obj = self.analyzer.get_object(['intersection'])
+        assert obj.type == 'FooHaver & BarHaver'
+
+    def test_generic_function(self):
+        """Make sure type params appear in args and return types."""
+        obj = self.analyzer.get_object(['aryIdentity'])
+        assert obj.params[0].type == 'T[]'
+        assert obj.returns[0].type == 'T[]'
+
+    @pytest.mark.xfail(reason='reflection not implemented yet')
+    def test_generic_member(self):
+        """Make sure members of a class have their type params taken into
+        account."""
+        obj = self.analyzer.get_object(['add'])
+        assert obj.type == 'T'
+        assert obj.params[0].type == 'T'
+
+    def test_constrained_by_interface(self):
+        """Make sure ``extends SomeInterface`` constraints are rendered."""
+        obj = self.analyzer.get_object(['constrainedIdentity'])
+        assert obj.params[0].type == 'T extends Lengthwise'
+        assert obj.returns[0].type == 'T extends Lengthwise'
+
+    def test_constrained_by_key(self):
+        """Make sure ``extends keyof SomeObject`` constraints are rendered."""
+        obj = self.analyzer.get_object(['getProperty'])
+        assert obj.params[1].type == 'K extends keyof T'
+
+    @pytest.mark.xfail(reason='reflection not implemented yet')
+    def test_constrained_by_constructor(self):
+        """Make sure ``new ()`` expressions and, more generally, per-property
+        constraints are rendered properly."""
+        obj = self.analyzer.get_object(['create'])
+        assert obj.params[0].type == '{new (): T}'
+
+    def test_utility_types(self):
+        """Test that a representative one of TS's utility types renders."""
+        obj = self.analyzer.get_object(['partial'])
+        assert obj.type == 'Partial<string>'
+
+    @pytest.mark.xfail(reason='reflection not implemented yet')
+    def test_constrained_by_property(self):
+        obj = self.analyzer.get_object(['objProps'])
+        assert obj.params[0].type == '{label: string}'
+
+    @pytest.mark.xfail(reason='reflection not implemented yet')
+    def test_optional_property(self):
+        """Make sure optional properties render properly."""
+        obj = self.analyzer.get_object(['option'])
+        assert obj.type == '{a: number; b?: string}'
diff --git a/tests/testing.py b/tests/testing.py
index 4f0c8746..1cfa7789 100644
--- a/tests/testing.py
+++ b/tests/testing.py
@@ -6,37 +6,132 @@
 import sys
 
 from sphinx.cmd.build import main as sphinx_main
-from sphinx.util.osutil import cd
 
+from sphinx_js.jsdoc import Analyzer as JsAnalyzer, jsdoc_output
+from sphinx_js.typedoc import Analyzer as TsAnalyzer, index_by_id, typedoc_output
 
-class SphinxBuildTestCase(TestCase):
+
+class ThisDirTestCase(TestCase):
+    """A TestCase that knows how to find the directory the subclass is defined
+    in"""
+
+    @classmethod
+    def this_dir(cls):
+        """Return the path to the dir containing the testcase class."""
+        # nose does some amazing magic that makes this work even if there are
+        # multiple test modules with the same name:
+        return dirname(sys.modules[cls.__module__].__file__)
+
+
+class SphinxBuildTestCase(ThisDirTestCase):
     """Base class for tests which require a Sphinx tree to be built and then
     deleted afterward
 
     """
+    builder = 'text'
+
     @classmethod
     def setup_class(cls):
         """Run Sphinx against the dir adjacent to the testcase."""
         cls.docs_dir = join(cls.this_dir(), 'source', 'docs')
-        with cd(cls.docs_dir):  # Matters only to keep test_build_ts tests passing. Remove once we clean that module up. Its cwd-sensitivity still means it doesn't work for actual users if the cwd isn't just right.
-            if sphinx_main([cls.docs_dir, '-b', 'text', '-v', '-E', join(cls.docs_dir, '_build')]):
+        # -v for better tracebacks:
+        if sphinx_main([cls.docs_dir, '-b', cls.builder, '-v', '-E', join(cls.docs_dir, '_build')]):
                 raise RuntimeError('Sphinx build exploded.')
 
     @classmethod
     def teardown_class(cls):
         rmtree(join(cls.docs_dir, '_build'))
 
-    @classmethod
-    def this_dir(cls):
-        """Return the path to the dir containing the testcase class."""
-        # nose does some amazing magic that makes this work even if there are
-        # multiple test modules with the same name:
-        return dirname(sys.modules[cls.__module__].__file__)
-
     def _file_contents(self, filename):
-        with open(join(self.docs_dir, '_build', '%s.txt' % filename),
+        extension = 'txt' if self.builder == 'text' else 'html'
+        with open(join(self.docs_dir, '_build', '%s.%s' % (filename, extension)),
                   encoding='utf8') as file:
             return file.read()
 
     def _file_contents_eq(self, filename, contents):
         assert self._file_contents(filename) == contents
+
+
+class JsDocTestCase(ThisDirTestCase):
+    """Base class for tests which analyze a file using JSDoc"""
+
+    @classmethod
+    def setup_class(cls):
+        """Run the JS analyzer over the JSDoc output."""
+        source_dir = join(cls.this_dir(), 'source')
+        output = jsdoc_output(None,
+                              [join(source_dir, cls.file)],
+                              source_dir,
+                              source_dir)
+        cls.analyzer = JsAnalyzer(output, source_dir)
+
+
+class TypeDocTestCase(ThisDirTestCase):
+    """Base class for tests which analyze a file using TypeDoc"""
+
+    @classmethod
+    def setup_class(cls):
+        """Run the TS analyzer over the TypeDoc output."""
+        cls._source_dir = join(cls.this_dir(), 'source')
+        cls.json = typedoc_output([join(cls._source_dir, file)
+                                   for file in cls.files],
+                                  cls._source_dir,
+                                  'tsconfig.json')
+        index_by_id({}, cls.json)
+
+
+class TypeDocAnalyzerTestCase(TypeDocTestCase):
+    """Base class for tests which analyze a file using TypeDoc"""
+
+    @classmethod
+    def setup_class(cls):
+        """Run the TS analyzer over the TypeDoc output."""
+        super().setup_class()
+        cls.analyzer = TsAnalyzer(cls.json, cls._source_dir)
+
+
+NO_MATCH = object()
+def dict_where(json, already_seen=None, **kwargs):
+    """Return the first object in the given data structure with properties
+    equal to the  ones given by ``kwargs``.
+
+    For example::
+
+        >>> dict_where({'hi': 'there', {'mister': 'zangler', 'and': 'friends'}},
+                       mister=zangler)
+        {'mister': 'zangler', 'and': 'friends'}
+
+    So far, only dicts and lists are supported. Other data structures won't be
+    recursed into. Cycles are avoided.
+
+    """
+    def object_if_matches_properties(json, **kwargs):
+        """Return the given JSON object iff all the properties and values given
+        by ``kwargs`` are in it. Else, return NO_MATCH."""
+        for k, v in kwargs.items():
+            if json.get(k, NO_MATCH) != v:
+                return NO_MATCH
+        return json
+
+    if already_seen is None:
+        already_seen = set()
+    already_seen.add(id(json))
+    if isinstance(json, list):
+        for list_item in json:
+            if id(list_item) not in already_seen:
+                match = dict_where(list_item, already_seen, **kwargs)
+                if match is not NO_MATCH:
+                    return match
+    elif isinstance(json, dict):
+        match = object_if_matches_properties(json, **kwargs)
+        if match is not NO_MATCH:
+            return match
+        for k, v in json.items():
+            if id(v) not in already_seen:
+                match = dict_where(v, already_seen, **kwargs)
+                if match is not NO_MATCH:
+                    return match
+    else:
+        # We don't know how to match leaf values yet.
+        pass
+    return NO_MATCH
diff --git a/tests/typescript/class.ts b/tests/typescript/class.ts
deleted file mode 100644
index 331b8204..00000000
--- a/tests/typescript/class.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-/**
- * A definition of a class
- */
-class ClassDefinition {
-    /**
-     * ClassDefinition constructor
-     * @param simple A parameter with a simple type
-     * @param complex A parameter with a complex type
-     * @param args rest arguments
-     */
-    constructor(simple:number, complex:{[id:string]:any}, ...args:any[]) {
-
-    }
-    /**
-     * This is a method without return type
-     * @param simple A parameter with a simple type
-     * @param complex A parameter with a complex type
-     * @param args rest arguments
-     */
-    method1(simple:number, complex:{[id:string]:any}, ...args:any[]) : void {
-
-    }
-    /**
-     * This is a method with a simple return type
-     * @param simple A parameter with a simple type
-     * @param complex A parameter with a complex type
-     * @param args rest arguments
-     * @returns 42
-     */
-    method2(simple:number, complex:{[id:string]:any}, ...args:any[]) : number {
-        return 42;
-    }
-    /**
-     * This is a method with a union type
-     * @param union A parameter with a union type
-     * @returns union
-     */
-    method3(union: number | string | any) : number | string | any{
-        return union;
-    }
-
-    /** Read/write property/variable */
-    property: any
-
-    /** Read-only  or getter property */
-    readonly property2: any
-
-    /**
-     * Property getter
-     * @returns 42
-     */
-    get property3() : any {
-        return 42
-    }
-
-    /**
-     * Property setter
-     * @param value new value of the property
-     */
-    set property4(value:any) {
-
-    }
-    /**
-     * Index signature
-     */
-    [id:string]: any;
-}
\ No newline at end of file
diff --git a/tests/typescript/class.ts.jsdoc b/tests/typescript/class.ts.jsdoc
deleted file mode 100644
index 0fbdb5b5..00000000
--- a/tests/typescript/class.ts.jsdoc
+++ /dev/null
@@ -1,297 +0,0 @@
-[
-    {
-        "comment": "<empty>",
-        "description": "",
-        "kind": "external",
-        "longname": "external:class",
-        "meta": {
-            "code": {},
-            "filename": "class.ts",
-            "lineno": 1,
-            "path": "./"
-        },
-        "name": "\"class\""
-    },
-    {
-        "classdesc": "",
-        "comment": "<empty>",
-        "description": "A definition of a class\n\n\n\nClassDefinition constructor\n\n",
-        "extends": [],
-        "kind": "class",
-        "longname": "external:class~ClassDefinition",
-        "memberof": "external:class",
-        "meta": {
-            "code": {
-                "paramnames": [
-                    "simple",
-                    "complex",
-                    "args"
-                ]
-            },
-            "filename": "class.ts",
-            "lineno": 4,
-            "path": "./"
-        },
-        "name": "ClassDefinition",
-        "params": [
-            {
-                "description": "A parameter with a simple type\n\n",
-                "name": "simple",
-                "type": {
-                    "names": [
-                        "number"
-                    ]
-                }
-            },
-            {
-                "description": "A parameter with a complex type\n\n",
-                "name": "complex",
-                "type": {
-                    "names": [
-                        "<TODO>"
-                    ]
-                }
-            },
-            {
-                "description": "rest arguments\n\n\n",
-                "name": "args",
-                "type": {
-                    "names": [
-                        "any[]"
-                    ]
-                }
-            }
-        ],
-        "returns": []
-    },
-    {
-        "comment": "<empty>",
-        "description": "Read/write property/variable\n\n",
-        "kind": "member",
-        "longname": "external:class~ClassDefinition#property",
-        "memberof": "external:class~ClassDefinition",
-        "meta": {
-            "code": {},
-            "filename": "class.ts",
-            "lineno": 43,
-            "path": "./"
-        },
-        "name": "property",
-        "type": {
-            "names": [
-                "any"
-            ]
-        }
-    },
-    {
-        "comment": "<empty>",
-        "description": "Read-only  or getter property\n\n",
-        "kind": "member",
-        "longname": "external:class~ClassDefinition#property2",
-        "memberof": "external:class~ClassDefinition",
-        "meta": {
-            "code": {},
-            "filename": "class.ts",
-            "lineno": 46,
-            "path": "./"
-        },
-        "name": "property2",
-        "type": {
-            "names": [
-                "any"
-            ]
-        }
-    },
-    {
-        "comment": "<empty>",
-        "description": "Property getter\n\n",
-        "kind": "member",
-        "longname": "external:class~ClassDefinition#property3",
-        "memberof": "external:class~ClassDefinition",
-        "meta": {
-            "code": {},
-            "filename": "class.ts",
-            "lineno": 52,
-            "path": "./"
-        },
-        "name": "property3",
-        "type": {
-            "names": [
-                "any"
-            ]
-        }
-    },
-    {
-        "comment": "<empty>",
-        "description": "Property setter\n\n",
-        "kind": "member",
-        "longname": "external:class~ClassDefinition#property4",
-        "memberof": "external:class~ClassDefinition",
-        "meta": {
-            "code": {},
-            "filename": "class.ts",
-            "lineno": 60,
-            "path": "./"
-        },
-        "name": "property4",
-        "type": {
-            "names": [
-                "any"
-            ]
-        }
-    },
-    {
-        "comment": "<empty>",
-        "description": "This is a method without return type\n\n",
-        "kind": "function",
-        "longname": "external:class~ClassDefinition.method1",
-        "memberof": "external:class~ClassDefinition",
-        "meta": {
-            "code": {
-                "paramnames": [
-                    "simple",
-                    "complex",
-                    "args"
-                ]
-            },
-            "filename": "class.ts",
-            "lineno": 20,
-            "path": "./"
-        },
-        "name": "method1",
-        "params": [
-            {
-                "description": "\n\nA parameter with a simple type",
-                "name": "simple",
-                "type": {
-                    "names": [
-                        "number"
-                    ]
-                }
-            },
-            {
-                "description": "\n\nA parameter with a complex type",
-                "name": "complex",
-                "type": {
-                    "names": [
-                        "<TODO>"
-                    ]
-                }
-            },
-            {
-                "description": "\n\nrest arguments\n",
-                "name": "args",
-                "type": {
-                    "names": [
-                        "any[]"
-                    ]
-                }
-            }
-        ],
-        "returns": []
-    },
-    {
-        "comment": "<empty>",
-        "description": "This is a method with a simple return type\n\n",
-        "kind": "function",
-        "longname": "external:class~ClassDefinition.method2",
-        "memberof": "external:class~ClassDefinition",
-        "meta": {
-            "code": {
-                "paramnames": [
-                    "simple",
-                    "complex",
-                    "args"
-                ]
-            },
-            "filename": "class.ts",
-            "lineno": 30,
-            "path": "./"
-        },
-        "name": "method2",
-        "params": [
-            {
-                "description": "\n\nA parameter with a simple type",
-                "name": "simple",
-                "type": {
-                    "names": [
-                        "number"
-                    ]
-                }
-            },
-            {
-                "description": "\n\nA parameter with a complex type",
-                "name": "complex",
-                "type": {
-                    "names": [
-                        "<TODO>"
-                    ]
-                }
-            },
-            {
-                "description": "\n\nrest arguments",
-                "name": "args",
-                "type": {
-                    "names": [
-                        "any[]"
-                    ]
-                }
-            }
-        ],
-        "returns": [
-            {
-                "description": "42\n",
-                "name": "method2",
-                "type": {
-                    "names": [
-                        "number"
-                    ]
-                }
-            }
-        ]
-    },
-    {
-        "comment": "<empty>",
-        "description": "This is a method with a union type\n\n",
-        "kind": "function",
-        "longname": "external:class~ClassDefinition.method3",
-        "memberof": "external:class~ClassDefinition",
-        "meta": {
-            "code": {
-                "paramnames": [
-                    "union"
-                ]
-            },
-            "filename": "class.ts",
-            "lineno": 38,
-            "path": "./"
-        },
-        "name": "method3",
-        "params": [
-            {
-                "description": "\n\nA parameter with a union type",
-                "name": "union",
-                "type": {
-                    "names": [
-                        "number",
-                        "string",
-                        "any"
-                    ]
-                }
-            }
-        ],
-        "returns": [
-            {
-                "description": "union\n",
-                "name": "method3",
-                "type": {
-                    "names": [
-                        "number",
-                        "string",
-                        "any"
-                    ]
-                }
-            }
-        ]
-    }
-]
\ No newline at end of file
diff --git a/tests/typescript/docstring-alias.ts b/tests/typescript/docstring-alias.ts
deleted file mode 100644
index 7eb75488..00000000
--- a/tests/typescript/docstring-alias.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * A definition of a class with aliases in the docstrings
- */
-class AliasClassDefinition {
-  /**
-   * Constructor doc.
-   *
-   * This is a constructor with @arg and @argument as alias for @param
-   * in its docstring
-   * @arg simple {number} parameter with 'arg' as alias for param in its docstring
-   * @argument simple2 {number} parameter with 'argument' as alias for param in its docstring
-   */
-  constructor(simple: number, simple2: number) {}
-
-  /**
-   * This is a method with @arg and @argument as alias for @param and
-   * @return as alias for @returns in its docstring
-   * @arg simple3 {number} parameter with 'arg' as alias for param in its docstring
-   * @argument simple4 {number} parameter with 'argument' as alias for param in its docstring
-   * @return number
-   */
-  alias_method(simple3: number, simple4: number): number {
-    return simple3 + simple4;
-  }
-}
diff --git a/tests/typescript/docstring-alias.ts.jsdoc b/tests/typescript/docstring-alias.ts.jsdoc
deleted file mode 100644
index 3cad5b9f..00000000
--- a/tests/typescript/docstring-alias.ts.jsdoc
+++ /dev/null
@@ -1,107 +0,0 @@
-[
-    {
-        "comment": "<empty>",
-        "description": "",
-        "kind": "external",
-        "longname": "external:docstring-alias",
-        "meta": {
-            "code": {},
-            "filename": "docstring-alias.ts",
-            "lineno": 1,
-            "path": "./"
-        },
-        "name": "\"docstring-alias\""
-    },
-    {
-        "classdesc": "",
-        "comment": "<empty>",
-        "description": "A definition of a class with aliases in the docstrings\n\n\n\nConstructor doc.\n\nThis is a constructor with @arg and @argument as alias for @param\nin its docstring",
-        "extends": [],
-        "kind": "class",
-        "longname": "external:docstring-alias~AliasClassDefinition",
-        "memberof": "external:docstring-alias",
-        "meta": {
-            "code": {
-                "paramnames": [
-                    "simple",
-                    "simple2"
-                ]
-            },
-            "filename": "docstring-alias.ts",
-            "lineno": 4,
-            "path": "./"
-        },
-        "name": "AliasClassDefinition",
-        "params": [
-            {
-                "description": "\n\nsimple {number} parameter with 'arg' as alias for param in its docstring",
-                "name": "simple",
-                "type": {
-                    "names": [
-                        "number"
-                    ]
-                }
-            },
-            {
-                "description": "\n\nsimple2 {number} parameter with 'argument' as alias for param in its docstring\n",
-                "name": "simple2",
-                "type": {
-                    "names": [
-                        "number"
-                    ]
-                }
-            }
-        ],
-        "returns": []
-    },
-    {
-        "comment": "<empty>",
-        "description": "This is a method with @arg and @argument as alias for @param and\n\n",
-        "kind": "function",
-        "longname": "external:docstring-alias~AliasClassDefinition.alias_method",
-        "memberof": "external:docstring-alias~AliasClassDefinition",
-        "meta": {
-            "code": {
-                "paramnames": [
-                    "simple3",
-                    "simple4"
-                ]
-            },
-            "filename": "docstring-alias.ts",
-            "lineno": 22,
-            "path": "./"
-        },
-        "name": "alias_method",
-        "params": [
-            {
-                "description": "\n\nsimple3 {number} parameter with 'arg' as alias for param in its docstring",
-                "name": "simple3",
-                "type": {
-                    "names": [
-                        "number"
-                    ]
-                }
-            },
-            {
-                "description": "\n\nsimple4 {number} parameter with 'argument' as alias for param in its docstring",
-                "name": "simple4",
-                "type": {
-                    "names": [
-                        "number"
-                    ]
-                }
-            }
-        ],
-        "returns": [
-            {
-                "description": "as alias for @returns in its docstring",
-                "name": "alias_method",
-                "type": {
-                    "names": [
-                        "number"
-                    ]
-                }
-            }
-        ]
-    }
-]
\ No newline at end of file
diff --git a/tests/typescript/extends.ts b/tests/typescript/extends.ts
deleted file mode 100644
index 7c85d5de..00000000
--- a/tests/typescript/extends.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * A super interface
- */
-interface ISuper {
-    /**
-     * A method
-     * @param arg A value
-     */
-    method1(arg:number);
-}
-
-/**
- * A super class
- */
-class Super {
-    /**
-     * A method
-     * @param arg A value
-     */
-    method1(arg:number) { }
-}
-
-/**
- * A sub class
- */
-class Sub extends Super {
-    /**
-     * Another method
-     * @param arg A string
-     */
-    method2(arg:string) { }
-}
-
-/**
- * An interface extending a class
- */
-interface ISub1 extends Super {
-    /**
-     * Another method
-     * @param arg A string
-     */
-    method2(arg:string);
-}
-
-/**
- * An interface extending another interface
- */
-interface ISub2 extends ISuper {
-    /**
-     * Another method
-     * @param arg A string
-     */
-    method2(arg:string);
-}
\ No newline at end of file
diff --git a/tests/typescript/extends.ts.jsdoc b/tests/typescript/extends.ts.jsdoc
deleted file mode 100644
index 44f02e8f..00000000
--- a/tests/typescript/extends.ts.jsdoc
+++ /dev/null
@@ -1,283 +0,0 @@
-[
-    {
-        "comment": "<empty>",
-        "description": "",
-        "kind": "external",
-        "longname": "external:extends",
-        "meta": {
-            "code": {},
-            "filename": "extends.ts",
-            "lineno": 1,
-            "path": "./"
-        },
-        "name": "\"extends\""
-    },
-    {
-        "classdesc": "\n\n**Extends:**\n * :js:class:`extends.Super`\n",
-        "comment": "<empty>",
-        "description": "A sub class\n\n",
-        "extends": [
-            "Super"
-        ],
-        "kind": "class",
-        "longname": "external:extends~Sub",
-        "memberof": "external:extends",
-        "meta": {
-            "code": {},
-            "filename": "extends.ts",
-            "lineno": 26,
-            "path": "./"
-        },
-        "name": "Sub",
-        "params": []
-    },
-    {
-        "comment": "<empty>",
-        "description": "Another method\n\n",
-        "kind": "function",
-        "longname": "external:extends~Sub.method2",
-        "memberof": "external:extends~Sub",
-        "meta": {
-            "code": {
-                "paramnames": [
-                    "arg"
-                ]
-            },
-            "filename": "extends.ts",
-            "lineno": 31,
-            "path": "./"
-        },
-        "name": "method2",
-        "params": [
-            {
-                "description": "\n\nA string\n",
-                "name": "arg",
-                "type": {
-                    "names": [
-                        "string"
-                    ]
-                }
-            }
-        ],
-        "returns": []
-    },
-    {
-        "classdesc": "",
-        "comment": "<empty>",
-        "description": "A super class\n\n",
-        "extends": [],
-        "kind": "class",
-        "longname": "external:extends~Super",
-        "memberof": "external:extends",
-        "meta": {
-            "code": {},
-            "filename": "extends.ts",
-            "lineno": 15,
-            "path": "./"
-        },
-        "name": "Super",
-        "params": []
-    },
-    {
-        "comment": "<empty>",
-        "description": "A method\n\n",
-        "kind": "function",
-        "longname": "external:extends~Super.method1",
-        "memberof": "external:extends~Super",
-        "meta": {
-            "code": {
-                "paramnames": [
-                    "arg"
-                ]
-            },
-            "filename": "extends.ts",
-            "lineno": 20,
-            "path": "./"
-        },
-        "name": "method1",
-        "params": [
-            {
-                "description": "\n\nA value\n",
-                "name": "arg",
-                "type": {
-                    "names": [
-                        "number"
-                    ]
-                }
-            }
-        ],
-        "returns": []
-    },
-    {
-        "classdesc": "*interface*\n\n**Extends:**\n * :js:class:`extends.Super`\n",
-        "comment": "<empty>",
-        "description": "An interface extending a class\n\n",
-        "extends": [
-            "Super"
-        ],
-        "kind": "interface",
-        "longname": "external:extends~ISub1",
-        "memberof": "external:extends",
-        "meta": {
-            "code": {},
-            "filename": "extends.ts",
-            "lineno": 37,
-            "path": "./"
-        },
-        "name": "ISub1",
-        "params": []
-    },
-    {
-        "comment": "<empty>",
-        "description": "Another method\n\n",
-        "kind": "function",
-        "longname": "external:extends~ISub1.method2",
-        "memberof": "external:extends~ISub1",
-        "meta": {
-            "code": {
-                "paramnames": [
-                    "arg"
-                ]
-            },
-            "filename": "extends.ts",
-            "lineno": 42,
-            "path": "./"
-        },
-        "name": "method2",
-        "params": [
-            {
-                "description": "\n\nA string\n",
-                "name": "arg",
-                "type": {
-                    "names": [
-                        "string"
-                    ]
-                }
-            }
-        ],
-        "returns": [
-            {
-                "name": "method2",
-                "type": {
-                    "names": [
-                        "any"
-                    ]
-                }
-            }
-        ]
-    },
-    {
-        "classdesc": "*interface*\n\n**Extends:**\n * :js:class:`extends.ISuper`\n",
-        "comment": "<empty>",
-        "description": "An interface extending another interface\n\n",
-        "extends": [
-            "ISuper"
-        ],
-        "kind": "interface",
-        "longname": "external:extends~ISub2",
-        "memberof": "external:extends",
-        "meta": {
-            "code": {},
-            "filename": "extends.ts",
-            "lineno": 48,
-            "path": "./"
-        },
-        "name": "ISub2",
-        "params": []
-    },
-    {
-        "comment": "<empty>",
-        "description": "Another method\n\n",
-        "kind": "function",
-        "longname": "external:extends~ISub2.method2",
-        "memberof": "external:extends~ISub2",
-        "meta": {
-            "code": {
-                "paramnames": [
-                    "arg"
-                ]
-            },
-            "filename": "extends.ts",
-            "lineno": 53,
-            "path": "./"
-        },
-        "name": "method2",
-        "params": [
-            {
-                "description": "\n\nA string\n",
-                "name": "arg",
-                "type": {
-                    "names": [
-                        "string"
-                    ]
-                }
-            }
-        ],
-        "returns": [
-            {
-                "name": "method2",
-                "type": {
-                    "names": [
-                        "any"
-                    ]
-                }
-            }
-        ]
-    },
-    {
-        "classdesc": "*interface*",
-        "comment": "<empty>",
-        "description": "A super interface\n\n",
-        "extends": [],
-        "kind": "interface",
-        "longname": "external:extends~ISuper",
-        "memberof": "external:extends",
-        "meta": {
-            "code": {},
-            "filename": "extends.ts",
-            "lineno": 4,
-            "path": "./"
-        },
-        "name": "ISuper",
-        "params": []
-    },
-    {
-        "comment": "<empty>",
-        "description": "A method\n\n",
-        "kind": "function",
-        "longname": "external:extends~ISuper.method1",
-        "memberof": "external:extends~ISuper",
-        "meta": {
-            "code": {
-                "paramnames": [
-                    "arg"
-                ]
-            },
-            "filename": "extends.ts",
-            "lineno": 9,
-            "path": "./"
-        },
-        "name": "method1",
-        "params": [
-            {
-                "description": "\n\nA value\n",
-                "name": "arg",
-                "type": {
-                    "names": [
-                        "number"
-                    ]
-                }
-            }
-        ],
-        "returns": [
-            {
-                "name": "method1",
-                "type": {
-                    "names": [
-                        "any"
-                    ]
-                }
-            }
-        ]
-    }
-]
\ No newline at end of file
diff --git a/tests/typescript/generic-class.ts b/tests/typescript/generic-class.ts
deleted file mode 100644
index 3d46efad..00000000
--- a/tests/typescript/generic-class.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * A definition of a generic class
- */
-class GenericClass<T> {
-    /**
-     * Generic member type
-     */
-    member:T
-
-    /**
-     * GenericClass constructor
-     * @param arg Generic as argument
-     */
-    constructor(arg: T) {
-    	this.member = arg
-    }
-
-    /**
-     * This is a method with a generic return type
-     * @returns 42
-     */
-    method2(): T {
-        return this.member
-    }
-}
\ No newline at end of file
diff --git a/tests/typescript/generic-class.ts.jsdoc b/tests/typescript/generic-class.ts.jsdoc
deleted file mode 100644
index 93d0a293..00000000
--- a/tests/typescript/generic-class.ts.jsdoc
+++ /dev/null
@@ -1,94 +0,0 @@
-[
-    {
-        "comment": "<empty>",
-        "description": "",
-        "kind": "external",
-        "longname": "external:generic-class",
-        "meta": {
-            "code": {},
-            "filename": "generic-class.ts",
-            "lineno": 1,
-            "path": "./"
-        },
-        "name": "\"generic-class\""
-    },
-    {
-        "classdesc": "",
-        "comment": "<empty>",
-        "description": "A definition of a generic class\n\n\n\nGenericClass constructor\n\n",
-        "extends": [],
-        "kind": "class",
-        "longname": "external:generic-class~GenericClass",
-        "memberof": "external:generic-class",
-        "meta": {
-            "code": {
-                "paramnames": [
-                    "arg"
-                ]
-            },
-            "filename": "generic-class.ts",
-            "lineno": 4,
-            "path": "./"
-        },
-        "name": "GenericClass",
-        "params": [
-            {
-                "description": "Generic as argument\n\n\n",
-                "name": "arg",
-                "type": {
-                    "names": [
-                        "T"
-                    ]
-                }
-            }
-        ],
-        "returns": []
-    },
-    {
-        "comment": "<empty>",
-        "description": "Generic member type\n\n",
-        "kind": "member",
-        "longname": "external:generic-class~GenericClass#member",
-        "memberof": "external:generic-class~GenericClass",
-        "meta": {
-            "code": {},
-            "filename": "generic-class.ts",
-            "lineno": 8,
-            "path": "./"
-        },
-        "name": "member",
-        "type": {
-            "names": [
-                "T"
-            ]
-        }
-    },
-    {
-        "comment": "<empty>",
-        "description": "This is a method with a generic return type\n\n",
-        "kind": "function",
-        "longname": "external:generic-class~GenericClass.method2",
-        "memberof": "external:generic-class~GenericClass",
-        "meta": {
-            "code": {
-                "paramnames": []
-            },
-            "filename": "generic-class.ts",
-            "lineno": 22,
-            "path": "./"
-        },
-        "name": "method2",
-        "params": [],
-        "returns": [
-            {
-                "description": "42\n",
-                "name": "method2",
-                "type": {
-                    "names": [
-                        "T"
-                    ]
-                }
-            }
-        ]
-    }
-]
\ No newline at end of file
diff --git a/tests/typescript/generic-complex.ts b/tests/typescript/generic-complex.ts
deleted file mode 100644
index 1f108c05..00000000
--- a/tests/typescript/generic-complex.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-/**
- * A definition of a class, using generic types containing unions and lists
- */
-class ClassWithComplexGenericMethodArgs {
-    /**
-     * A generic method
-     * @param arg a generic argument
-     * @returns a value of the generic type
-     */
-    async method<T>(arg: Partial<T|number>): Promise<[Partial<T|number>, number]> {
-        return [arg, 1];
-    }
-}
\ No newline at end of file
diff --git a/tests/typescript/generic-complex.ts.jsdoc b/tests/typescript/generic-complex.ts.jsdoc
deleted file mode 100644
index 7fa9c4f8..00000000
--- a/tests/typescript/generic-complex.ts.jsdoc
+++ /dev/null
@@ -1,72 +0,0 @@
-[
-    {
-        "comment": "<empty>",
-        "description": "",
-        "kind": "external",
-        "longname": "external:generic-complex",
-        "meta": {
-            "code": {},
-            "filename": "generic-complex.ts",
-            "lineno": 1,
-            "path": "./"
-        },
-        "name": "\"generic-complex\""
-    },
-    {
-        "classdesc": "",
-        "comment": "<empty>",
-        "description": "A definition of a class, using generic types containing unions and lists\n\n",
-        "extends": [],
-        "kind": "class",
-        "longname": "external:generic-complex~ClassWithComplexGenericMethodArgs",
-        "memberof": "external:generic-complex",
-        "meta": {
-            "code": {},
-            "filename": "generic-complex.ts",
-            "lineno": 4,
-            "path": "./"
-        },
-        "name": "ClassWithComplexGenericMethodArgs",
-        "params": []
-    },
-    {
-        "comment": "<empty>",
-        "description": "A generic method\n\n",
-        "kind": "function",
-        "longname": "external:generic-complex~ClassWithComplexGenericMethodArgs.method",
-        "memberof": "external:generic-complex~ClassWithComplexGenericMethodArgs",
-        "meta": {
-            "code": {
-                "paramnames": [
-                    "arg"
-                ]
-            },
-            "filename": "generic-complex.ts",
-            "lineno": 10,
-            "path": "./"
-        },
-        "name": "method",
-        "params": [
-            {
-                "description": "\n\na generic argument",
-                "name": "arg",
-                "type": {
-                    "names": [
-                        "Partial<T|number>"
-                    ]
-                }
-            }
-        ],
-        "returns": [
-            {
-                "description": "a value of the generic type\n",
-                "name": "method",
-                "type": {
-                    "names": [
-                        "Promise<[Partial<T|number>,number]>"
-                    ]
-                }
-            }
-        ]
-    }
-]
\ No newline at end of file
diff --git a/tests/typescript/generic-interface.ts b/tests/typescript/generic-interface.ts
deleted file mode 100644
index a6a8b894..00000000
--- a/tests/typescript/generic-interface.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- * A definition of a generic interface
- */
-interface GenericInterface<T> {
-    /**
-     * Generic member type
-     */
-    member: T
-
-    /**
-     * This is a method with a generic return type
-     * @returns 42
-     */
-    method2(): T
-}
\ No newline at end of file
diff --git a/tests/typescript/generic-interface.ts.jsdoc b/tests/typescript/generic-interface.ts.jsdoc
deleted file mode 100644
index d560bb68..00000000
--- a/tests/typescript/generic-interface.ts.jsdoc
+++ /dev/null
@@ -1,84 +0,0 @@
-[
-    {
-        "comment": "<empty>",
-        "description": "",
-        "kind": "external",
-        "longname": "external:generic-interface",
-        "meta": {
-            "code": {},
-            "filename": "generic-interface.ts",
-            "lineno": 1,
-            "path": "./"
-        },
-        "name": "\"generic-interface\""
-    },
-    {
-        "classdesc": "*interface*",
-        "comment": "<empty>",
-        "description": "A definition of a generic interface\n\n",
-        "extends": [],
-        "kind": "interface",
-        "longname": "external:generic-interface~GenericInterface",
-        "memberof": "external:generic-interface",
-        "meta": {
-            "code": {},
-            "filename": "generic-interface.ts",
-            "lineno": 4,
-            "path": "./"
-        },
-        "name": "GenericInterface",
-        "params": [
-            {
-                "description": "",
-                "name": "T"
-            }
-        ]
-    },
-    {
-        "comment": "<empty>",
-        "description": "Generic member type\n\n",
-        "kind": "member",
-        "longname": "external:generic-interface~GenericInterface#member",
-        "memberof": "external:generic-interface~GenericInterface",
-        "meta": {
-            "code": {},
-            "filename": "generic-interface.ts",
-            "lineno": 8,
-            "path": "./"
-        },
-        "name": "member",
-        "type": {
-            "names": [
-                "T"
-            ]
-        }
-    },
-    {
-        "comment": "<empty>",
-        "description": "This is a method with a generic return type\n\n",
-        "kind": "function",
-        "longname": "external:generic-interface~GenericInterface.method2",
-        "memberof": "external:generic-interface~GenericInterface",
-        "meta": {
-            "code": {
-                "paramnames": []
-            },
-            "filename": "generic-interface.ts",
-            "lineno": 14,
-            "path": "./"
-        },
-        "name": "method2",
-        "params": [],
-        "returns": [
-            {
-                "description": "42\n",
-                "name": "method2",
-                "type": {
-                    "names": [
-                        "T"
-                    ]
-                }
-            }
-        ]
-    }
-]
\ No newline at end of file
diff --git a/tests/typescript/generic-method.ts b/tests/typescript/generic-method.ts
deleted file mode 100644
index f2039c8b..00000000
--- a/tests/typescript/generic-method.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-/**
- * A definition of a class
- */
-class ClassWithGenericMethod {
-    /**
-     * A generic method
-     * @param arg a generic argument
-     * @returns a value of the generic type
-     */
-    method<T>(arg:T) : T { return arg }
-}
\ No newline at end of file
diff --git a/tests/typescript/generic-method.ts.jsdoc b/tests/typescript/generic-method.ts.jsdoc
deleted file mode 100644
index 1a7338e1..00000000
--- a/tests/typescript/generic-method.ts.jsdoc
+++ /dev/null
@@ -1,72 +0,0 @@
-[
-    {
-        "comment": "<empty>",
-        "description": "",
-        "kind": "external",
-        "longname": "external:generic-method",
-        "meta": {
-            "code": {},
-            "filename": "generic-method.ts",
-            "lineno": 1,
-            "path": "./"
-        },
-        "name": "\"generic-method\""
-    },
-    {
-        "classdesc": "",
-        "comment": "<empty>",
-        "description": "A definition of a class\n\n",
-        "extends": [],
-        "kind": "class",
-        "longname": "external:generic-method~ClassWithGenericMethod",
-        "memberof": "external:generic-method",
-        "meta": {
-            "code": {},
-            "filename": "generic-method.ts",
-            "lineno": 4,
-            "path": "./"
-        },
-        "name": "ClassWithGenericMethod",
-        "params": []
-    },
-    {
-        "comment": "<empty>",
-        "description": "A generic method\n\n",
-        "kind": "function",
-        "longname": "external:generic-method~ClassWithGenericMethod.method",
-        "memberof": "external:generic-method~ClassWithGenericMethod",
-        "meta": {
-            "code": {
-                "paramnames": [
-                    "arg"
-                ]
-            },
-            "filename": "generic-method.ts",
-            "lineno": 10,
-            "path": "./"
-        },
-        "name": "method",
-        "params": [
-            {
-                "description": "\n\na generic argument",
-                "name": "arg",
-                "type": {
-                    "names": [
-                        "T"
-                    ]
-                }
-            }
-        ],
-        "returns": [
-            {
-                "description": "a value of the generic type\n",
-                "name": "method",
-                "type": {
-                    "names": [
-                        "T"
-                    ]
-                }
-            }
-        ]
-    }
-]
\ No newline at end of file
diff --git a/tests/typescript/generic.ts b/tests/typescript/generic.ts
deleted file mode 100644
index d9a91473..00000000
--- a/tests/typescript/generic.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * Generic class with type expressions
- * @param T Type
- * @param K keys of T
- */
-class Generic<T, K extends keyof T> {
-    /** Optional entry of T */
-    K?: T[K]
-}
\ No newline at end of file
diff --git a/tests/typescript/generic.ts.jsdoc b/tests/typescript/generic.ts.jsdoc
deleted file mode 100644
index a88dcd65..00000000
--- a/tests/typescript/generic.ts.jsdoc
+++ /dev/null
@@ -1,65 +0,0 @@
-[
-    {
-        "comment": "<empty>",
-        "description": "",
-        "kind": "external",
-        "longname": "external:generic",
-        "meta": {
-            "code": {},
-            "filename": "generic.ts",
-            "lineno": 1,
-            "path": "./"
-        },
-        "name": "\"generic\""
-    },
-    {
-        "classdesc": "",
-        "comment": "<empty>",
-        "description": "Generic class with type expressions\n\n",
-        "extends": [],
-        "kind": "class",
-        "longname": "external:generic~Generic",
-        "memberof": "external:generic",
-        "meta": {
-            "code": {},
-            "filename": "generic.ts",
-            "lineno": 6,
-            "path": "./"
-        },
-        "name": "Generic",
-        "params": [
-            {
-                "description": "Type\n\n",
-                "name": "T"
-            },
-            {
-                "description": "keys of T\n\n\n",
-                "name": "K",
-                "type": {
-                    "names": [
-                        "keyof:T"
-                    ]
-                }
-            }
-        ]
-    },
-    {
-        "comment": "<empty>",
-        "description": "Optional entry of T\n\n",
-        "kind": "member",
-        "longname": "external:generic~Generic#K",
-        "memberof": "external:generic~Generic",
-        "meta": {
-            "code": {},
-            "filename": "generic.ts",
-            "lineno": 8,
-            "path": "./"
-        },
-        "name": "K",
-        "type": {
-            "names": [
-                "T[K]"
-            ]
-        }
-    }
-]
\ No newline at end of file
diff --git a/tests/typescript/implements.ts b/tests/typescript/implements.ts
deleted file mode 100644
index 4b603254..00000000
--- a/tests/typescript/implements.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * A definition of an interface
- */
-interface Interface {
-    /**
-     * A method specification
-     * @param arg A value
-     */
-    method(arg:number):void;
-
-    /**
-     * Another documented method
-     * @param arg A string
-     * @returns A string
-     */
-    documented(arg:string) : string;
-}
-
-/**
- * An implementation of an interface
- */
-class Implementation implements Interface {
-    /**
-     * A method implementation
-     * @param arg A value
-     */
-    method(arg:number) { }
-
-    // Is this documented?
-    documented(arg:string) : string {
-        return arg;
-    }
-}
\ No newline at end of file
diff --git a/tests/typescript/implements.ts.jsdoc b/tests/typescript/implements.ts.jsdoc
deleted file mode 100644
index b61336d0..00000000
--- a/tests/typescript/implements.ts.jsdoc
+++ /dev/null
@@ -1,187 +0,0 @@
-[
-    {
-        "comment": "<empty>",
-        "description": "",
-        "kind": "external",
-        "longname": "external:implements",
-        "meta": {
-            "code": {},
-            "filename": "implements.ts",
-            "lineno": 1,
-            "path": "./"
-        },
-        "name": "\"implements\""
-    },
-    {
-        "classdesc": "\n\n**Implements:**\n * :js:class:`implements.Interface`\n",
-        "comment": "<empty>",
-        "description": "An implementation of an interface\n\n",
-        "extends": [],
-        "kind": "class",
-        "longname": "external:implements~Implementation",
-        "memberof": "external:implements",
-        "meta": {
-            "code": {},
-            "filename": "implements.ts",
-            "lineno": 22,
-            "path": "./"
-        },
-        "name": "Implementation"
-    },
-    {
-        "comment": "<empty>",
-        "description": "",
-        "kind": "function",
-        "longname": "external:implements~Implementation.documented",
-        "memberof": "external:implements~Implementation",
-        "meta": {
-            "code": {
-                "paramnames": [
-                    "arg"
-                ]
-            },
-            "filename": "implements.ts",
-            "lineno": 30,
-            "path": "./"
-        },
-        "name": "documented",
-        "params": [
-            {
-                "description": "",
-                "name": "arg",
-                "type": {
-                    "names": [
-                        "string"
-                    ]
-                }
-            }
-        ],
-        "returns": [
-            {
-                "name": "documented",
-                "type": {
-                    "names": [
-                        "string"
-                    ]
-                }
-            }
-        ]
-    },
-    {
-        "comment": "<empty>",
-        "description": "A method implementation\n\n",
-        "kind": "function",
-        "longname": "external:implements~Implementation.method",
-        "memberof": "external:implements~Implementation",
-        "meta": {
-            "code": {
-                "paramnames": [
-                    "arg"
-                ]
-            },
-            "filename": "implements.ts",
-            "lineno": 27,
-            "path": "./"
-        },
-        "name": "method",
-        "params": [
-            {
-                "description": "\n\nA value\n",
-                "name": "arg",
-                "type": {
-                    "names": [
-                        "number"
-                    ]
-                }
-            }
-        ],
-        "returns": []
-    },
-    {
-        "classdesc": "*interface*",
-        "comment": "<empty>",
-        "description": "A definition of an interface\n\n",
-        "extends": [],
-        "kind": "interface",
-        "longname": "external:implements~Interface",
-        "memberof": "external:implements",
-        "meta": {
-            "code": {},
-            "filename": "implements.ts",
-            "lineno": 4,
-            "path": "./"
-        },
-        "name": "Interface",
-        "params": []
-    },
-    {
-        "comment": "<empty>",
-        "description": "Another documented method\n\n",
-        "kind": "function",
-        "longname": "external:implements~Interface.documented",
-        "memberof": "external:implements~Interface",
-        "meta": {
-            "code": {
-                "paramnames": [
-                    "arg"
-                ]
-            },
-            "filename": "implements.ts",
-            "lineno": 16,
-            "path": "./"
-        },
-        "name": "documented",
-        "params": [
-            {
-                "description": "\n\nA string",
-                "name": "arg",
-                "type": {
-                    "names": [
-                        "string"
-                    ]
-                }
-            }
-        ],
-        "returns": [
-            {
-                "description": "A string\n",
-                "name": "documented",
-                "type": {
-                    "names": [
-                        "string"
-                    ]
-                }
-            }
-        ]
-    },
-    {
-        "comment": "<empty>",
-        "description": "A method specification\n\n",
-        "kind": "function",
-        "longname": "external:implements~Interface.method",
-        "memberof": "external:implements~Interface",
-        "meta": {
-            "code": {
-                "paramnames": [
-                    "arg"
-                ]
-            },
-            "filename": "implements.ts",
-            "lineno": 9,
-            "path": "./"
-        },
-        "name": "method",
-        "params": [
-            {
-                "description": "\n\nA value\n",
-                "name": "arg",
-                "type": {
-                    "names": [
-                        "number"
-                    ]
-                }
-            }
-        ],
-        "returns": []
-    }
-]
\ No newline at end of file
diff --git a/tests/typescript/interface.ts b/tests/typescript/interface.ts
deleted file mode 100644
index bbb75c3a..00000000
--- a/tests/typescript/interface.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * A definition of an interface
- */
-interface IDefinition {
-    /**
-     * This is a method without return type
-     * @param simple A parameter with a simple type
-     * @param complex A parameter with a complex type
-     * @param args rest arguments
-     */
-    method1(simple:number, complex:{[id:string]:any}, ...args:any[]) : void;
-    /**
-     * This is a method with a simple return type
-     * @param simple A parameter with a simple type
-     * @param complex A parameter with a complex type
-     * @param args rest arguments
-     */
-    method2(simple:number, complex:{[id:string]:any}, ...args:any[]) : number;
-
-    /** Read/write property/variable */
-    property: any
-
-    /** Read-only  or getter property */
-    readonly property2: any
-
-    /** 
-     * Call signature
-     * @param arg1 argument to the call signature
-     */
-    (arg1:any) : void;
-
-    /**
-     * Index signature
-     */
-    [id:string]: any
-}
\ No newline at end of file
diff --git a/tests/typescript/interface.ts.jsdoc b/tests/typescript/interface.ts.jsdoc
deleted file mode 100644
index a1bd2b99..00000000
--- a/tests/typescript/interface.ts.jsdoc
+++ /dev/null
@@ -1,179 +0,0 @@
-[
-    {
-        "comment": "<empty>",
-        "description": "",
-        "kind": "external",
-        "longname": "external:interface",
-        "meta": {
-            "code": {},
-            "filename": "interface.ts",
-            "lineno": 1,
-            "path": "./"
-        },
-        "name": "\"interface\""
-    },
-    {
-        "classdesc": "*interface*",
-        "comment": "<empty>",
-        "description": "A definition of an interface\n\n",
-        "extends": [],
-        "kind": "interface",
-        "longname": "external:interface~IDefinition",
-        "memberof": "external:interface",
-        "meta": {
-            "code": {},
-            "filename": "interface.ts",
-            "lineno": 4,
-            "path": "./"
-        },
-        "name": "IDefinition",
-        "params": []
-    },
-    {
-        "comment": "<empty>",
-        "description": "Read/write property/variable\n\n",
-        "kind": "member",
-        "longname": "external:interface~IDefinition#property",
-        "memberof": "external:interface~IDefinition",
-        "meta": {
-            "code": {},
-            "filename": "interface.ts",
-            "lineno": 21,
-            "path": "./"
-        },
-        "name": "property",
-        "type": {
-            "names": [
-                "any"
-            ]
-        }
-    },
-    {
-        "comment": "<empty>",
-        "description": "Read-only  or getter property\n\n",
-        "kind": "member",
-        "longname": "external:interface~IDefinition#property2",
-        "memberof": "external:interface~IDefinition",
-        "meta": {
-            "code": {},
-            "filename": "interface.ts",
-            "lineno": 24,
-            "path": "./"
-        },
-        "name": "property2",
-        "type": {
-            "names": [
-                "any"
-            ]
-        }
-    },
-    {
-        "comment": "<empty>",
-        "description": "This is a method without return type\n\n",
-        "kind": "function",
-        "longname": "external:interface~IDefinition.method1",
-        "memberof": "external:interface~IDefinition",
-        "meta": {
-            "code": {
-                "paramnames": [
-                    "simple",
-                    "complex",
-                    "args"
-                ]
-            },
-            "filename": "interface.ts",
-            "lineno": 11,
-            "path": "./"
-        },
-        "name": "method1",
-        "params": [
-            {
-                "description": "\n\nA parameter with a simple type",
-                "name": "simple",
-                "type": {
-                    "names": [
-                        "number"
-                    ]
-                }
-            },
-            {
-                "description": "\n\nA parameter with a complex type",
-                "name": "complex",
-                "type": {
-                    "names": [
-                        "<TODO>"
-                    ]
-                }
-            },
-            {
-                "description": "\n\nrest arguments\n",
-                "name": "args",
-                "type": {
-                    "names": [
-                        "any[]"
-                    ]
-                }
-            }
-        ],
-        "returns": []
-    },
-    {
-        "comment": "<empty>",
-        "description": "This is a method with a simple return type\n\n",
-        "kind": "function",
-        "longname": "external:interface~IDefinition.method2",
-        "memberof": "external:interface~IDefinition",
-        "meta": {
-            "code": {
-                "paramnames": [
-                    "simple",
-                    "complex",
-                    "args"
-                ]
-            },
-            "filename": "interface.ts",
-            "lineno": 18,
-            "path": "./"
-        },
-        "name": "method2",
-        "params": [
-            {
-                "description": "\n\nA parameter with a simple type",
-                "name": "simple",
-                "type": {
-                    "names": [
-                        "number"
-                    ]
-                }
-            },
-            {
-                "description": "\n\nA parameter with a complex type",
-                "name": "complex",
-                "type": {
-                    "names": [
-                        "<TODO>"
-                    ]
-                }
-            },
-            {
-                "description": "\n\nrest arguments\n",
-                "name": "args",
-                "type": {
-                    "names": [
-                        "any[]"
-                    ]
-                }
-            }
-        ],
-        "returns": [
-            {
-                "name": "method2",
-                "type": {
-                    "names": [
-                        "number"
-                    ]
-                }
-            }
-        ]
-    }
-]
\ No newline at end of file