From 8dfb58ffc49ac585980eaca63f373b3b9cdf2751 Mon Sep 17 00:00:00 2001
From: Momtchil Momtchev <momtchil@momtchev.com>
Date: Tue, 30 Nov 2021 19:55:53 +0100
Subject: [PATCH 01/10] implement a plugin framework

---
 __tests__/__snapshots__/bin.js.snap  | 156 +++++++++++++++++++++++++
 __tests__/__snapshots__/test.js.snap | 164 +++++++++++++++++++++++++++
 __tests__/bin.js                     |   8 +-
 __tests__/fixture/plugin.txt         |   6 +
 __tests__/test.js                    |  25 ++++
 docs/POLYGLOT.md                     |   2 -
 src/commands/shared_options.js       |   4 +
 src/config.js                        |   8 +-
 src/index.js                         |  37 +++++-
 src/merge_config.js                  |  30 +++++
 src/mock_plugin.js                   |  50 ++++++++
 src/plugin_api.js                    |   8 ++
 12 files changed, 490 insertions(+), 8 deletions(-)
 create mode 100644 __tests__/fixture/plugin.txt
 delete mode 100644 docs/POLYGLOT.md
 create mode 100644 src/mock_plugin.js
 create mode 100644 src/plugin_api.js

diff --git a/__tests__/__snapshots__/bin.js.snap b/__tests__/__snapshots__/bin.js.snap
index e89925da4..b31635f72 100644
--- a/__tests__/__snapshots__/bin.js.snap
+++ b/__tests__/__snapshots__/bin.js.snap
@@ -2024,6 +2024,162 @@ f5 comment
 
 exports[`lint command generates lint output 1`] = `""`;
 
+exports[`load a plugin 1`] = `
+Array [
+  Object {
+    "augments": Array [],
+    "context": Object {
+      "file": "[path]",
+      "loc": Object {
+        "end": Object {
+          "column": 2,
+          "line": 8,
+        },
+        "start": Object {
+          "column": 0,
+          "line": 5,
+        },
+      },
+    },
+    "description": Object {
+      "children": Array [
+        Object {
+          "children": Array [
+            Object {
+              "type": "text",
+              "value": "This function returns the number one.",
+            },
+          ],
+          "type": "paragraph",
+        },
+      ],
+      "type": "root",
+    },
+    "examples": Array [],
+    "implements": Array [],
+    "kind": "function",
+    "loc": Object {
+      "end": Object {
+        "column": 3,
+        "line": 4,
+      },
+      "start": Object {
+        "column": 0,
+        "line": 1,
+      },
+    },
+    "members": Object {
+      "events": Array [],
+      "global": Array [],
+      "inner": Array [],
+      "instance": Array [],
+      "static": Array [],
+    },
+    "name": "simple.input",
+    "namespace": "simple.input",
+    "params": Array [],
+    "path": Array [
+      Object {
+        "kind": "function",
+        "name": "simple.input",
+      },
+    ],
+    "properties": Array [],
+    "returns": Array [
+      Object {
+        "description": Object {
+          "children": Array [
+            Object {
+              "children": Array [
+                Object {
+                  "type": "text",
+                  "value": "numberone",
+                },
+              ],
+              "type": "paragraph",
+            },
+          ],
+          "type": "root",
+        },
+        "title": "returns",
+        "type": Object {
+          "name": "number",
+          "type": "NameExpression",
+        },
+      },
+    ],
+    "sees": Array [],
+    "tags": Array [
+      Object {
+        "description": "numberone",
+        "lineNumber": 2,
+        "title": "returns",
+        "type": Object {
+          "name": "number",
+          "type": "NameExpression",
+        },
+      },
+    ],
+    "throws": Array [],
+    "todos": Array [],
+    "yields": Array [],
+  },
+  Object {
+    "after": "",
+    "api": false,
+    "augments": Array [],
+    "context": Object {
+      "file": "[path]",
+      "loc": Object {
+        "end": Object {
+          "column": 4,
+          "line": 5,
+        },
+        "start": Object {
+          "column": 1,
+          "line": 5,
+        },
+      },
+    },
+    "end": 19,
+    "examples": Array [],
+    "implements": Array [],
+    "loc": Object {
+      "end": Object {
+        "column": 1,
+        "line": 2,
+      },
+      "start": Object {
+        "column": 1,
+        "line": 0,
+      },
+    },
+    "members": Object {
+      "events": Array [],
+      "global": Array [],
+      "inner": Array [],
+      "instance": Array [],
+      "static": Array [],
+    },
+    "namespace": "",
+    "params": Array [],
+    "path": Array [],
+    "properties": Array [],
+    "returns": Array [],
+    "sees": Array [],
+    "start": 0,
+    "tags": Array [],
+    "throws": Array [],
+    "todos": Array [],
+    "type": "CommentBlock",
+    "value": "*
+ * @method dummy
+ ",
+    "yields": Array [],
+  },
+]
+`;
+
 exports[`should use browser resolve 1`] = `
 Array [
   Object {
diff --git a/__tests__/__snapshots__/test.js.snap b/__tests__/__snapshots__/test.js.snap
index e8a0a2294..3d392be7d 100644
--- a/__tests__/__snapshots__/test.js.snap
+++ b/__tests__/__snapshots__/test.js.snap
@@ -287,6 +287,170 @@ Array [
 ]
 `;
 
+exports[`Check that plugins are loaded 1`] = `
+Array [
+  Object {
+    "augments": Array [],
+    "context": Object {
+      "loc": SourceLocation {
+        "end": Position {
+          "column": 2,
+          "line": 8,
+        },
+        "filename": undefined,
+        "identifierName": undefined,
+        "start": Position {
+          "column": 0,
+          "line": 5,
+        },
+      },
+    },
+    "description": Object {
+      "children": Array [
+        Object {
+          "children": Array [
+            Object {
+              "type": "text",
+              "value": "This function returns the number one.",
+            },
+          ],
+          "type": "paragraph",
+        },
+      ],
+      "type": "root",
+    },
+    "errors": Array [],
+    "examples": Array [],
+    "implements": Array [],
+    "kind": "function",
+    "loc": SourceLocation {
+      "end": Position {
+        "column": 3,
+        "line": 4,
+      },
+      "filename": undefined,
+      "identifierName": undefined,
+      "start": Position {
+        "column": 0,
+        "line": 1,
+      },
+    },
+    "members": Object {
+      "events": Array [],
+      "global": Array [],
+      "inner": Array [],
+      "instance": Array [],
+      "static": Array [],
+    },
+    "name": "simple.input",
+    "namespace": "simple.input",
+    "params": Array [],
+    "path": Array [
+      Object {
+        "kind": "function",
+        "name": "simple.input",
+      },
+    ],
+    "properties": Array [],
+    "returns": Array [
+      Object {
+        "description": Object {
+          "children": Array [
+            Object {
+              "children": Array [
+                Object {
+                  "type": "text",
+                  "value": "numberone",
+                },
+              ],
+              "type": "paragraph",
+            },
+          ],
+          "type": "root",
+        },
+        "title": "returns",
+        "type": Object {
+          "name": "number",
+          "type": "NameExpression",
+        },
+      },
+    ],
+    "sees": Array [],
+    "tags": Array [
+      Object {
+        "description": "numberone",
+        "lineNumber": 2,
+        "title": "returns",
+        "type": Object {
+          "name": "number",
+          "type": "NameExpression",
+        },
+      },
+    ],
+    "throws": Array [],
+    "todos": Array [],
+    "yields": Array [],
+  },
+  Object {
+    "after": "",
+    "api": false,
+    "augments": Array [],
+    "context": Object {
+      "loc": Object {
+        "end": Object {
+          "column": 4,
+          "line": 5,
+        },
+        "start": Object {
+          "column": 1,
+          "line": 5,
+        },
+      },
+    },
+    "end": 19,
+    "errors": Array [
+      Object {
+        "message": "could not determine @name for hierarchy",
+      },
+    ],
+    "examples": Array [],
+    "implements": Array [],
+    "loc": Object {
+      "end": Object {
+        "column": 1,
+        "line": 2,
+      },
+      "start": Object {
+        "column": 1,
+        "line": 0,
+      },
+    },
+    "members": Object {
+      "events": Array [],
+      "global": Array [],
+      "inner": Array [],
+      "instance": Array [],
+      "static": Array [],
+    },
+    "namespace": "",
+    "params": Array [],
+    "path": Array [],
+    "properties": Array [],
+    "returns": Array [],
+    "sees": Array [],
+    "start": 0,
+    "tags": Array [],
+    "throws": Array [],
+    "todos": Array [],
+    "type": "CommentBlock",
+    "value": "*
+ * @method dummy
+ ",
+    "yields": Array [],
+  },
+]
+`;
+
 exports[`Use Source attribute only 1`] = `
 Array [
   Object {
diff --git a/__tests__/bin.js b/__tests__/bin.js
index 7f46bca1e..e668ad26f 100644
--- a/__tests__/bin.js
+++ b/__tests__/bin.js
@@ -3,7 +3,6 @@
 import path from 'path';
 import os from 'os';
 import { exec } from 'child_process';
-import tmp from 'tmp';
 import fs from 'fs-extra';
 import { fileURLToPath } from 'url';
 
@@ -60,6 +59,13 @@ test.skip('defaults to parsing package.json main', async function () {
   expect(data.length).toBeTruthy();
 });
 
+test('load a plugin', async function () {
+  const data = await documentation([
+    'build fixture/simple.input.js fixture/plugin.txt --plugin=../src/mock_plugin.js'
+  ]);
+  expect(normalize(data)).toMatchSnapshot();
+});
+
 test('accepts config file', async function () {
   const data = await documentation([
     'build fixture/sorting/input.js -c fixture/config.json'
diff --git a/__tests__/fixture/plugin.txt b/__tests__/fixture/plugin.txt
new file mode 100644
index 000000000..2244c29af
--- /dev/null
+++ b/__tests__/fixture/plugin.txt
@@ -0,0 +1,6 @@
+/**
+ * @method test
+ */
+
+test
+
diff --git a/__tests__/test.js b/__tests__/test.js
index aa9037c0c..d7263057e 100644
--- a/__tests__/test.js
+++ b/__tests__/test.js
@@ -13,6 +13,7 @@ import _ from 'lodash';
 import chdir from 'chdir';
 import config from '../src/config';
 import { fileURLToPath } from 'url';
+import { jest } from '@jest/globals';
 
 const UPDATE = !!process.env.UPDATE;
 const __filename = fileURLToPath(import.meta.url);
@@ -71,6 +72,30 @@ test('Check that external modules could parse as input', async function () {
   expect(result).toMatchSnapshot();
 });
 
+test('Check that plugins are loaded', async function () {
+  const initCb = jest.fn();
+  const parseCb = jest.fn();
+  const mockPlugin = await import('../src/mock_plugin.js');
+  mockPlugin.mockInit(initCb, parseCb);
+
+  const dir = path.join(__dirname, 'fixture');
+  const result = await documentation.build(
+    [path.join(dir, 'simple.input.js'), path.join(dir, 'plugin.txt')],
+    { plugin: ['./mock_plugin.js'] }
+  );
+  normalize(result);
+  expect(result).toMatchSnapshot();
+
+  expect(initCb.mock.calls.length).toBe(1);
+  expect(parseCb.mock.calls.length).toBe(2);
+  expect(
+    parseCb.mock.calls[0][0].file.includes('fixture/plugin.txt')
+  ).toBeTruthy();
+  expect(
+    parseCb.mock.calls[1][0].file.includes('fixture/simple.input.js')
+  ).toBeTruthy();
+});
+
 test('bad input', function () {
   glob
     .sync(path.join(__dirname, 'fixture/bad', '*.input.js'))
diff --git a/docs/POLYGLOT.md b/docs/POLYGLOT.md
deleted file mode 100644
index f0005ae0b..000000000
--- a/docs/POLYGLOT.md
+++ /dev/null
@@ -1,2 +0,0 @@
-🚨 Polyglot mode is now deprecated. It will be replaced by a pluggable
-input system in future versions. 🚨
diff --git a/src/commands/shared_options.js b/src/commands/shared_options.js
index 0379debb8..fae99fa00 100644
--- a/src/commands/shared_options.js
+++ b/src/commands/shared_options.js
@@ -45,6 +45,10 @@ export const sharedInputOptions = {
     type: 'array',
     alias: 'pe'
   },
+  plugin: {
+    type: 'array',
+    describe: 'load a plugin'
+  },
   access: {
     describe:
       'Include only comments with a given access level, out of private, ' +
diff --git a/src/config.js b/src/config.js
index 17d49d3d4..260c67e03 100644
--- a/src/config.js
+++ b/src/config.js
@@ -1,11 +1,11 @@
 const defaultConfig = {
-  // package.json ignored and don't get project infromation
+  // package.json ignored and don't get project information
   'no-package': false,
-  // Extenstions which by dafault are parse
+  // Extensions which by default are parsed
   parseExtension: ['.mjs', '.js', '.jsx', '.es5', '.es6', '.vue', '.ts', '.tsx']
 };
 
-function normalaze(config, global) {
+function normalize(config, global) {
   if (config.parseExtension) {
     config.parseExtension = Array.from(
       new Set([...config.parseExtension, ...global.parseExtension])
@@ -24,6 +24,6 @@ export default {
     this.globalConfig.parseExtension = [...defaultConfig.parseExtension];
   },
   add(parameters) {
-    Object.assign(this.globalConfig, normalaze(parameters, this.globalConfig));
+    Object.assign(this.globalConfig, normalize(parameters, this.globalConfig));
   }
 };
diff --git a/src/index.js b/src/index.js
index abe624c7c..72d89085a 100644
--- a/src/index.js
+++ b/src/index.js
@@ -26,6 +26,7 @@ import md from './output/markdown.js';
 import json from './output/json.js';
 import createFormatters from './output/util/formatters.js';
 import LinkerStack from './output/util/linker_stack.js';
+import pluginAPI from './plugin_api.js';
 
 /**
  * Build a pipeline of comment handlers.
@@ -76,7 +77,24 @@ export function expandInputs(indexes, config) {
     return shallow(indexes, config);
   }
 
-  return dependency(indexes, config);
+  let idxShallow = [];
+  if (config.plugin) {
+    for (const plugin of config.plugin) {
+      if (config._module[plugin].shallow) {
+        idxShallow = idxShallow.concat(
+          indexes.filter(idx =>
+            config._module[plugin].shallow(idx, config, pluginAPI)
+          )
+        );
+      }
+    }
+  }
+  const depsShallow = shallow(idxShallow, config);
+
+  const idxFull = indexes.filter(idx => !idxShallow.includes(idx));
+  const depsFull = dependency(idxFull, config);
+
+  return Promise.all([depsShallow, depsFull]).then(([a, b]) => a.concat(b));
 }
 
 function buildInternal(inputsAndConfig) {
@@ -104,6 +122,14 @@ function buildInternal(inputsAndConfig) {
   ]);
 
   const extractedComments = _.flatMap(inputs, function (sourceFile) {
+    if (config.plugin) {
+      for (const plugin of config.plugin) {
+        if (config._module[plugin].parse) {
+          const r = config._module[plugin].parse(sourceFile, config, pluginAPI);
+          if (r) return r.map(buildPipeline);
+        }
+      }
+    }
     return parseJavaScript(sourceFile, config).map(buildPipeline);
   }).filter(Boolean);
 
@@ -132,6 +158,14 @@ function lintInternal(inputsAndConfig) {
   ]);
 
   const extractedComments = _.flatMap(inputs, sourceFile => {
+    if (config.plugin) {
+      for (const plugin of config.plugin) {
+        if (config._module[plugin].parse) {
+          const r = config._module[plugin].parse(sourceFile, config, pluginAPI);
+          if (r) return r.map(lintPipeline);
+        }
+      }
+    }
     return parseJavaScript(sourceFile, config).map(lintPipeline);
   }).filter(Boolean);
 
@@ -180,6 +214,7 @@ export const lint = (indexes, args) =>
  * @param {Array<string>} args.external a string regex / glob match pattern
  * that defines what external modules will be whitelisted and included in the
  * generated documentation.
+ * @param {Array<string>} [args.plugin=[]] load plugins
  * @param {boolean} [args.shallow=false] whether to avoid dependency parsing
  * even in JavaScript code.
  * @param {Array<string|Object>} [args.order=[]] optional array that
diff --git a/src/merge_config.js b/src/merge_config.js
index cea2591c6..eef314f6f 100644
--- a/src/merge_config.js
+++ b/src/merge_config.js
@@ -79,6 +79,36 @@ export default async function mergeConfig(config = {}) {
   conf.add(config);
   conf.add(await readConfigFile(conf.globalConfig.config));
   conf.add(await readPackage(conf.globalConfig['no-package']));
+  if (conf.globalConfig.plugin) {
+    await loadPlugins(conf.globalConfig);
+  }
 
   return conf.globalConfig;
 }
+
+/**
+ * Load the external plugins
+ *
+ * @param {Object} configuration plugins section of the configuration
+ * @returns {void}
+ */
+async function loadPlugins(config) {
+  if (!config._module)
+    Object.defineProperty(config, '_module', {
+      enumerable: false,
+      writable: false,
+      configurable: false,
+      value: {}
+    });
+  for (const plugin of config.plugin) {
+    try {
+      config._module[plugin] = await import(plugin);
+      if (config._module[plugin].init) {
+        await config._module[plugin].init();
+      }
+    } catch (e) {
+      console.error(`Failed loading ${plugin}`);
+      throw e;
+    }
+  }
+}
diff --git a/src/mock_plugin.js b/src/mock_plugin.js
new file mode 100644
index 000000000..0471099c5
--- /dev/null
+++ b/src/mock_plugin.js
@@ -0,0 +1,50 @@
+let initCb, parseCb, depCb, dummy;
+
+export function mockInit(init, parse, dep) {
+  initCb = init;
+  parseCb = parse;
+  depCb = dep;
+}
+
+export async function init() {
+  if (initCb) initCb(...arguments);
+  dummy = [
+    {
+      after: '',
+      api: false,
+      start: 0,
+      end: 19,
+      type: 'CommentBlock',
+      value: '*\n * @method dummy\n ',
+      context: {
+        file: 'plugin.txt',
+        loc: { start: { line: 5, column: 1 }, end: { line: 5, column: 4 } }
+      },
+      loc: { start: { line: 0, column: 1 }, end: { line: 2, column: 1 } },
+      augments: [],
+      errors: [],
+      examples: [],
+      implements: [],
+      params: [],
+      properties: [],
+      returns: [],
+      sees: [],
+      tags: [],
+      throws: [],
+      todos: [],
+      yields: []
+    }
+  ];
+}
+
+export function parse(file) {
+  if (parseCb) parseCb(...arguments);
+  if (file.file.includes('plugin.txt')) return dummy;
+  return false;
+}
+
+export function shallow(file) {
+  if (depCb) depCb(...arguments);
+  if (file.includes('plugin.txt')) return true;
+  return false;
+}
diff --git a/src/plugin_api.js b/src/plugin_api.js
new file mode 100644
index 000000000..6cfdda6d7
--- /dev/null
+++ b/src/plugin_api.js
@@ -0,0 +1,8 @@
+import parseJSDoc from './parse.js';
+import isJSDocComment from './is_jsdoc_comment.js';
+const pluginAPI = {
+  parseJSDoc,
+  isJSDocComment
+};
+
+export default pluginAPI;

From 17b8f8ad2420e3d475a2f4475890fb6804d6cefa Mon Sep 17 00:00:00 2001
From: Momtchil Momtchev <momtchil@momtchev.com>
Date: Wed, 1 Dec 2021 12:40:23 +0100
Subject: [PATCH 02/10] pass the configuration object to the plugin init

---
 __tests__/test.js   | 4 +++-
 src/merge_config.js | 2 +-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/__tests__/test.js b/__tests__/test.js
index d7263057e..379b96006 100644
--- a/__tests__/test.js
+++ b/__tests__/test.js
@@ -81,12 +81,14 @@ test('Check that plugins are loaded', async function () {
   const dir = path.join(__dirname, 'fixture');
   const result = await documentation.build(
     [path.join(dir, 'simple.input.js'), path.join(dir, 'plugin.txt')],
-    { plugin: ['./mock_plugin.js'] }
+    { plugin: ['./mock_plugin.js'], order: 'test' }
   );
   normalize(result);
   expect(result).toMatchSnapshot();
 
   expect(initCb.mock.calls.length).toBe(1);
+  expect(initCb.mock.calls[0][0].order).toBe('test');
+
   expect(parseCb.mock.calls.length).toBe(2);
   expect(
     parseCb.mock.calls[0][0].file.includes('fixture/plugin.txt')
diff --git a/src/merge_config.js b/src/merge_config.js
index eef314f6f..6ad688d8c 100644
--- a/src/merge_config.js
+++ b/src/merge_config.js
@@ -104,7 +104,7 @@ async function loadPlugins(config) {
     try {
       config._module[plugin] = await import(plugin);
       if (config._module[plugin].init) {
-        await config._module[plugin].init();
+        await config._module[plugin].init(config);
       }
     } catch (e) {
       console.error(`Failed loading ${plugin}`);

From 39c4fb1de088d9fe475f1f7de6b5a83da89a7f8a Mon Sep 17 00:00:00 2001
From: Momtchil Momtchev <momtchil@momtchev.com>
Date: Wed, 1 Dec 2021 18:19:50 +0100
Subject: [PATCH 03/10] absorb context info from the parsing phase

---
 src/parse.js | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/src/parse.js b/src/parse.js
index a10c7b2b8..cd63bf99e 100644
--- a/src/parse.js
+++ b/src/parse.js
@@ -666,10 +666,24 @@ export default function parseJSDoc(comment, loc, context) {
     }
   });
 
+  for (const tag of [
+    'kind',
+    'name',
+    'returns',
+    'params',
+    'properties',
+    'errors',
+    'augments',
+    'throws',
+    'yields',
+    'implements'
+  ])
+    if (context[tag]) result[tag] = context[tag];
+
   // Using the @name tag, or any other tag that sets the name of a comment,
   // disconnects the comment from its surrounding code.
   if (context && result.name) {
-    delete context.ast;
+    if (context.ast) delete context.ast;
   }
 
   return result;

From 8f917615e013821c4662e5bd10708b895798a180 Mon Sep 17 00:00:00 2001
From: Momtchil Momtchev <momtchil@momtchev.com>
Date: Wed, 1 Dec 2021 19:05:04 +0100
Subject: [PATCH 04/10] allow explicit jsdoc tags to override the parsing

---
 src/parse.js | 28 ++++++++++++++--------------
 1 file changed, 14 insertions(+), 14 deletions(-)

diff --git a/src/parse.js b/src/parse.js
index cd63bf99e..b55e7f3f8 100644
--- a/src/parse.js
+++ b/src/parse.js
@@ -636,6 +636,20 @@ export default function parseJSDoc(comment, loc, context) {
   result.todos = [];
   result.yields = [];
 
+  for (const tag of [
+    'kind',
+    'name',
+    'returns',
+    'params',
+    'properties',
+    'errors',
+    'augments',
+    'throws',
+    'yields',
+    'implements'
+  ])
+    if (context[tag]) result[tag] = context[tag];
+
   if (result.description) {
     result.description = parseMarkdown(result.description);
   }
@@ -666,20 +680,6 @@ export default function parseJSDoc(comment, loc, context) {
     }
   });
 
-  for (const tag of [
-    'kind',
-    'name',
-    'returns',
-    'params',
-    'properties',
-    'errors',
-    'augments',
-    'throws',
-    'yields',
-    'implements'
-  ])
-    if (context[tag]) result[tag] = context[tag];
-
   // Using the @name tag, or any other tag that sets the name of a comment,
   // disconnects the comment from its surrounding code.
   if (context && result.name) {

From 1137f380218e5a391c4a00024a3aea208f2cdbb7 Mon Sep 17 00:00:00 2001
From: Momtchil Momtchev <momtchil@momtchev.com>
Date: Thu, 2 Dec 2021 12:28:25 +0100
Subject: [PATCH 05/10] check using/overriding context data from the plugin

---
 __tests__/__snapshots__/bin.js.snap  | 151 +++++++++++++++---
 __tests__/__snapshots__/test.js.snap | 227 ++++++++++++++++++++++-----
 __tests__/test.js                    |  15 +-
 src/mock_plugin.js                   |  49 +++---
 src/parse.js                         |  28 ++--
 5 files changed, 379 insertions(+), 91 deletions(-)

diff --git a/__tests__/__snapshots__/bin.js.snap b/__tests__/__snapshots__/bin.js.snap
index b31635f72..70fda8853 100644
--- a/__tests__/__snapshots__/bin.js.snap
+++ b/__tests__/__snapshots__/bin.js.snap
@@ -2125,8 +2125,6 @@ Array [
     "yields": Array [],
   },
   Object {
-    "after": "",
-    "api": false,
     "augments": Array [],
     "context": Object {
       "file": "[path]",
@@ -2141,19 +2139,128 @@ Array [
         },
       },
     },
-    "end": 19,
+    "description": "",
     "examples": Array [],
     "implements": Array [],
-    "loc": Object {
-      "end": Object {
-        "column": 1,
-        "line": 2,
+    "kind": "function",
+    "members": Object {
+      "events": Array [],
+      "global": Array [],
+      "inner": Array [],
+      "instance": Array [],
+      "static": Array [],
+    },
+    "name": "dummy",
+    "namespace": "dummy",
+    "params": Array [],
+    "path": Array [
+      Object {
+        "kind": "function",
+        "name": "dummy",
       },
-      "start": Object {
-        "column": 1,
-        "line": 0,
+    ],
+    "properties": Array [],
+    "returns": Array [],
+    "sees": Array [],
+    "tags": Array [
+      Object {
+        "description": null,
+        "lineNumber": 1,
+        "name": "dummy",
+        "title": "method",
+      },
+    ],
+    "throws": Array [],
+    "todos": Array [],
+    "yields": Array [],
+  },
+  Object {
+    "augments": Array [],
+    "context": Object {
+      "file": "[path]",
+      "kind": "method",
+      "loc": Object {
+        "end": Object {
+          "column": 4,
+          "line": 5,
+        },
+        "start": Object {
+          "column": 1,
+          "line": 5,
+        },
+      },
+      "name": "dummy_method",
+    },
+    "description": "",
+    "examples": Array [],
+    "implements": Array [],
+    "kind": "method",
+    "members": Object {
+      "events": Array [],
+      "global": Array [],
+      "inner": Array [],
+      "instance": Array [],
+      "static": Array [],
+    },
+    "name": "dummy_method",
+    "namespace": "dummy_method",
+    "params": Array [
+      Object {
+        "lineNumber": 1,
+        "name": "dummy_param",
+        "title": "param",
+        "type": Object {
+          "name": "number",
+          "type": "NameExpression",
+        },
+      },
+    ],
+    "path": Array [
+      Object {
+        "kind": "method",
+        "name": "dummy_method",
       },
+    ],
+    "properties": Array [],
+    "returns": Array [],
+    "sees": Array [],
+    "tags": Array [
+      Object {
+        "description": null,
+        "lineNumber": 1,
+        "name": "dummy_param",
+        "title": "param",
+        "type": Object {
+          "name": "number",
+          "type": "NameExpression",
+        },
+      },
+    ],
+    "throws": Array [],
+    "todos": Array [],
+    "yields": Array [],
+  },
+  Object {
+    "augments": Array [],
+    "context": Object {
+      "file": "[path]",
+      "kind": "SHOULD_NOT_APPEAR_IN_THE_RESULT",
+      "loc": Object {
+        "end": Object {
+          "column": 4,
+          "line": 5,
+        },
+        "start": Object {
+          "column": 1,
+          "line": 5,
+        },
+      },
+      "name": "SHOULD_NOT_APPEAR_IN_THE_RESULT",
     },
+    "description": "",
+    "examples": Array [],
+    "implements": Array [],
+    "kind": "function",
     "members": Object {
       "events": Array [],
       "global": Array [],
@@ -2161,20 +2268,28 @@ Array [
       "instance": Array [],
       "static": Array [],
     },
-    "namespace": "",
+    "name": "not_so_dummy",
+    "namespace": "not_so_dummy",
     "params": Array [],
-    "path": Array [],
+    "path": Array [
+      Object {
+        "kind": "function",
+        "name": "not_so_dummy",
+      },
+    ],
     "properties": Array [],
     "returns": Array [],
     "sees": Array [],
-    "start": 0,
-    "tags": Array [],
+    "tags": Array [
+      Object {
+        "description": null,
+        "lineNumber": 1,
+        "name": "not_so_dummy",
+        "title": "method",
+      },
+    ],
     "throws": Array [],
     "todos": Array [],
-    "type": "CommentBlock",
-    "value": "*
- * @method dummy
- ",
     "yields": Array [],
   },
 ]
diff --git a/__tests__/__snapshots__/test.js.snap b/__tests__/__snapshots__/test.js.snap
index 3d392be7d..ff595d1ed 100644
--- a/__tests__/__snapshots__/test.js.snap
+++ b/__tests__/__snapshots__/test.js.snap
@@ -287,21 +287,21 @@ Array [
 ]
 `;
 
-exports[`Check that plugins are loaded 1`] = `
+exports[`Check that plugins are loaded and used 1`] = `
 Array [
   Object {
     "augments": Array [],
     "context": Object {
       "loc": SourceLocation {
         "end": Position {
-          "column": 2,
-          "line": 8,
+          "column": 1,
+          "line": 13,
         },
         "filename": undefined,
         "identifierName": undefined,
         "start": Position {
           "column": 0,
-          "line": 5,
+          "line": 10,
         },
       },
     },
@@ -311,7 +311,7 @@ Array [
           "children": Array [
             Object {
               "type": "text",
-              "value": "This function returns the number one.",
+              "value": "This function returns the number plus two.",
             },
           ],
           "type": "paragraph",
@@ -320,13 +320,18 @@ Array [
       "type": "root",
     },
     "errors": Array [],
-    "examples": Array [],
+    "examples": Array [
+      Object {
+        "description": "var result = returnTwo(4);
+// result is 6",
+      },
+    ],
     "implements": Array [],
     "kind": "function",
     "loc": SourceLocation {
       "end": Position {
         "column": 3,
-        "line": 4,
+        "line": 9,
       },
       "filename": undefined,
       "identifierName": undefined,
@@ -342,13 +347,37 @@ Array [
       "instance": Array [],
       "static": Array [],
     },
-    "name": "simple.input",
-    "namespace": "simple.input",
-    "params": Array [],
+    "name": "returnTwo",
+    "namespace": "returnTwo",
+    "params": Array [
+      Object {
+        "description": Object {
+          "children": Array [
+            Object {
+              "children": Array [
+                Object {
+                  "type": "text",
+                  "value": "the number",
+                },
+              ],
+              "type": "paragraph",
+            },
+          ],
+          "type": "root",
+        },
+        "lineNumber": 3,
+        "name": "a",
+        "title": "param",
+        "type": Object {
+          "name": "Number",
+          "type": "NameExpression",
+        },
+      },
+    ],
     "path": Array [
       Object {
         "kind": "function",
-        "name": "simple.input",
+        "name": "returnTwo",
       },
     ],
     "properties": Array [],
@@ -360,7 +389,7 @@ Array [
               "children": Array [
                 Object {
                   "type": "text",
-                  "value": "numberone",
+                  "value": "numbertwo",
                 },
               ],
               "type": "paragraph",
@@ -370,7 +399,7 @@ Array [
         },
         "title": "returns",
         "type": Object {
-          "name": "number",
+          "name": "Number",
           "type": "NameExpression",
         },
       },
@@ -378,22 +407,36 @@ Array [
     "sees": Array [],
     "tags": Array [
       Object {
-        "description": "numberone",
-        "lineNumber": 2,
+        "description": "the number",
+        "lineNumber": 3,
+        "name": "a",
+        "title": "param",
+        "type": Object {
+          "name": "Number",
+          "type": "NameExpression",
+        },
+      },
+      Object {
+        "description": "numbertwo",
+        "lineNumber": 4,
         "title": "returns",
         "type": Object {
-          "name": "number",
+          "name": "Number",
           "type": "NameExpression",
         },
       },
+      Object {
+        "description": "var result = returnTwo(4);
+// result is 6",
+        "lineNumber": 5,
+        "title": "example",
+      },
     ],
     "throws": Array [],
     "todos": Array [],
     "yields": Array [],
   },
   Object {
-    "after": "",
-    "api": false,
     "augments": Array [],
     "context": Object {
       "loc": Object {
@@ -407,24 +450,128 @@ Array [
         },
       },
     },
-    "end": 19,
-    "errors": Array [
+    "description": "",
+    "errors": Array [],
+    "examples": Array [],
+    "implements": Array [],
+    "kind": "function",
+    "loc": undefined,
+    "members": Object {
+      "events": Array [],
+      "global": Array [],
+      "inner": Array [],
+      "instance": Array [],
+      "static": Array [],
+    },
+    "name": "dummy",
+    "namespace": "dummy",
+    "params": Array [],
+    "path": Array [
       Object {
-        "message": "could not determine @name for hierarchy",
+        "kind": "function",
+        "name": "dummy",
+      },
+    ],
+    "properties": Array [],
+    "returns": Array [],
+    "sees": Array [],
+    "tags": Array [
+      Object {
+        "description": null,
+        "lineNumber": 1,
+        "name": "dummy",
+        "title": "method",
       },
     ],
+    "throws": Array [],
+    "todos": Array [],
+    "yields": Array [],
+  },
+  Object {
+    "augments": Array [],
+    "context": Object {
+      "loc": Object {
+        "end": Object {
+          "column": 4,
+          "line": 5,
+        },
+        "start": Object {
+          "column": 1,
+          "line": 5,
+        },
+      },
+    },
+    "description": "",
+    "errors": Array [],
     "examples": Array [],
     "implements": Array [],
-    "loc": Object {
-      "end": Object {
-        "column": 1,
-        "line": 2,
+    "kind": "method",
+    "loc": undefined,
+    "members": Object {
+      "events": Array [],
+      "global": Array [],
+      "inner": Array [],
+      "instance": Array [],
+      "static": Array [],
+    },
+    "name": "dummy_method",
+    "namespace": "dummy_method",
+    "params": Array [
+      Object {
+        "lineNumber": 1,
+        "name": "dummy_param",
+        "title": "param",
+        "type": Object {
+          "name": "number",
+          "type": "NameExpression",
+        },
       },
-      "start": Object {
-        "column": 1,
-        "line": 0,
+    ],
+    "path": Array [
+      Object {
+        "kind": "method",
+        "name": "dummy_method",
+      },
+    ],
+    "properties": Array [],
+    "returns": Array [],
+    "sees": Array [],
+    "tags": Array [
+      Object {
+        "description": null,
+        "lineNumber": 1,
+        "name": "dummy_param",
+        "title": "param",
+        "type": Object {
+          "name": "number",
+          "type": "NameExpression",
+        },
+      },
+    ],
+    "throws": Array [],
+    "todos": Array [],
+    "yields": Array [],
+  },
+  Object {
+    "augments": Array [],
+    "context": Object {
+      "loc": Object {
+        "end": Object {
+          "column": 4,
+          "line": 5,
+        },
+        "start": Object {
+          "column": 1,
+          "line": 5,
+        },
       },
     },
+    "description": "",
+    "errors": Array [],
+    "examples": Array [],
+    "implements": Array [],
+    "kind": "function",
+    "loc": undefined,
     "members": Object {
       "events": Array [],
       "global": Array [],
@@ -432,20 +579,28 @@ Array [
       "instance": Array [],
       "static": Array [],
     },
-    "namespace": "",
+    "name": "not_so_dummy",
+    "namespace": "not_so_dummy",
     "params": Array [],
-    "path": Array [],
+    "path": Array [
+      Object {
+        "kind": "function",
+        "name": "not_so_dummy",
+      },
+    ],
     "properties": Array [],
     "returns": Array [],
     "sees": Array [],
-    "start": 0,
-    "tags": Array [],
+    "tags": Array [
+      Object {
+        "description": null,
+        "lineNumber": 1,
+        "name": "not_so_dummy",
+        "title": "method",
+      },
+    ],
     "throws": Array [],
     "todos": Array [],
-    "type": "CommentBlock",
-    "value": "*
- * @method dummy
- ",
     "yields": Array [],
   },
 ]
diff --git a/__tests__/test.js b/__tests__/test.js
index 379b96006..4c4502379 100644
--- a/__tests__/test.js
+++ b/__tests__/test.js
@@ -72,7 +72,7 @@ test('Check that external modules could parse as input', async function () {
   expect(result).toMatchSnapshot();
 });
 
-test('Check that plugins are loaded', async function () {
+test('Check that plugins are loaded and used', async function () {
   const initCb = jest.fn();
   const parseCb = jest.fn();
   const mockPlugin = await import('../src/mock_plugin.js');
@@ -80,12 +80,21 @@ test('Check that plugins are loaded', async function () {
 
   const dir = path.join(__dirname, 'fixture');
   const result = await documentation.build(
-    [path.join(dir, 'simple.input.js'), path.join(dir, 'plugin.txt')],
+    [path.join(dir, 'simple-two.input.js'), path.join(dir, 'plugin.txt')],
     { plugin: ['./mock_plugin.js'], order: 'test' }
   );
   normalize(result);
   expect(result).toMatchSnapshot();
 
+  // name from JSDoc tag
+  expect(result[1].name).toBe('dummy');
+
+  // name parsed by the plugin
+  expect(result[2].name).toBe('dummy_method');
+
+  // name from plugin parsing overridden by JSDoc tag
+  expect(result[3].name).toBe('not_so_dummy');
+
   expect(initCb.mock.calls.length).toBe(1);
   expect(initCb.mock.calls[0][0].order).toBe('test');
 
@@ -94,7 +103,7 @@ test('Check that plugins are loaded', async function () {
     parseCb.mock.calls[0][0].file.includes('fixture/plugin.txt')
   ).toBeTruthy();
   expect(
-    parseCb.mock.calls[1][0].file.includes('fixture/simple.input.js')
+    parseCb.mock.calls[1][0].file.includes('fixture/simple-two.input.js')
   ).toBeTruthy();
 });
 
diff --git a/src/mock_plugin.js b/src/mock_plugin.js
index 0471099c5..f67e8a40a 100644
--- a/src/mock_plugin.js
+++ b/src/mock_plugin.js
@@ -10,36 +10,43 @@ export async function init() {
   if (initCb) initCb(...arguments);
   dummy = [
     {
-      after: '',
-      api: false,
-      start: 0,
-      end: 19,
-      type: 'CommentBlock',
       value: '*\n * @method dummy\n ',
       context: {
         file: 'plugin.txt',
-        loc: { start: { line: 5, column: 1 }, end: { line: 5, column: 4 } }
+        loc: { start: { line: 5, column: 1 }, end: { line: 5, column: 4 } },
+        sortKey: 'a'
       },
-      loc: { start: { line: 0, column: 1 }, end: { line: 2, column: 1 } },
-      augments: [],
-      errors: [],
-      examples: [],
-      implements: [],
-      params: [],
-      properties: [],
-      returns: [],
-      sees: [],
-      tags: [],
-      throws: [],
-      todos: [],
-      yields: []
+      loc: { start: { line: 0, column: 1 }, end: { line: 2, column: 1 } }
+    },
+    {
+      value: '*\n * @param {number} dummy_param\n ',
+      context: {
+        file: 'plugin.txt',
+        loc: { start: { line: 5, column: 1 }, end: { line: 5, column: 4 } },
+        sortKey: 'b',
+        kind: 'method',
+        name: 'dummy_method'
+      },
+      loc: { start: { line: 0, column: 1 }, end: { line: 2, column: 1 } }
+    },
+    {
+      value: '*\n * @method not_so_dummy\n ',
+      context: {
+        file: 'plugin.txt',
+        loc: { start: { line: 5, column: 1 }, end: { line: 5, column: 4 } },
+        sortKey: 'c',
+        kind: 'SHOULD_NOT_APPEAR_IN_THE_RESULT',
+        name: 'SHOULD_NOT_APPEAR_IN_THE_RESULT'
+      },
+      loc: { start: { line: 0, column: 1 }, end: { line: 2, column: 1 } }
     }
   ];
 }
 
-export function parse(file) {
+export function parse(file, _config, api) {
   if (parseCb) parseCb(...arguments);
-  if (file.file.includes('plugin.txt')) return dummy;
+  if (file.file.includes('plugin.txt'))
+    return dummy.map(c => api.parseJSDoc(c.value, c.log, c.context));
   return false;
 }
 
diff --git a/src/parse.js b/src/parse.js
index b55e7f3f8..d756440a5 100644
--- a/src/parse.js
+++ b/src/parse.js
@@ -636,19 +636,21 @@ export default function parseJSDoc(comment, loc, context) {
   result.todos = [];
   result.yields = [];
 
-  for (const tag of [
-    'kind',
-    'name',
-    'returns',
-    'params',
-    'properties',
-    'errors',
-    'augments',
-    'throws',
-    'yields',
-    'implements'
-  ])
-    if (context[tag]) result[tag] = context[tag];
+  if (context) {
+    for (const tag of [
+      'kind',
+      'name',
+      'returns',
+      'params',
+      'properties',
+      'errors',
+      'augments',
+      'throws',
+      'yields',
+      'implements'
+    ])
+      if (context[tag]) result[tag] = context[tag];
+  }
 
   if (result.description) {
     result.description = parseMarkdown(result.description);

From 3c4d853c0be826577a16e12a513abb408b9bdc36 Mon Sep 17 00:00:00 2001
From: Momtchil Momtchev <momtchil@momtchev.com>
Date: Thu, 2 Dec 2021 20:52:19 +0100
Subject: [PATCH 06/10] remove the selective shallow dependencies

not compatible with globbing without extensive modifications to the code
---
 __tests__/test.js  |  4 ++--
 src/index.js       | 19 +------------------
 src/mock_plugin.js | 11 ++---------
 3 files changed, 5 insertions(+), 29 deletions(-)

diff --git a/__tests__/test.js b/__tests__/test.js
index 4c4502379..76538e1f2 100644
--- a/__tests__/test.js
+++ b/__tests__/test.js
@@ -100,10 +100,10 @@ test('Check that plugins are loaded and used', async function () {
 
   expect(parseCb.mock.calls.length).toBe(2);
   expect(
-    parseCb.mock.calls[0][0].file.includes('fixture/plugin.txt')
+    parseCb.mock.calls[0][0].file.includes('fixture/simple-two.input.js')
   ).toBeTruthy();
   expect(
-    parseCb.mock.calls[1][0].file.includes('fixture/simple-two.input.js')
+    parseCb.mock.calls[1][0].file.includes('fixture/plugin.txt')
   ).toBeTruthy();
 });
 
diff --git a/src/index.js b/src/index.js
index 72d89085a..dd0b21394 100644
--- a/src/index.js
+++ b/src/index.js
@@ -77,24 +77,7 @@ export function expandInputs(indexes, config) {
     return shallow(indexes, config);
   }
 
-  let idxShallow = [];
-  if (config.plugin) {
-    for (const plugin of config.plugin) {
-      if (config._module[plugin].shallow) {
-        idxShallow = idxShallow.concat(
-          indexes.filter(idx =>
-            config._module[plugin].shallow(idx, config, pluginAPI)
-          )
-        );
-      }
-    }
-  }
-  const depsShallow = shallow(idxShallow, config);
-
-  const idxFull = indexes.filter(idx => !idxShallow.includes(idx));
-  const depsFull = dependency(idxFull, config);
-
-  return Promise.all([depsShallow, depsFull]).then(([a, b]) => a.concat(b));
+  return dependency(indexes, config);
 }
 
 function buildInternal(inputsAndConfig) {
diff --git a/src/mock_plugin.js b/src/mock_plugin.js
index f67e8a40a..2e4d25018 100644
--- a/src/mock_plugin.js
+++ b/src/mock_plugin.js
@@ -1,9 +1,8 @@
-let initCb, parseCb, depCb, dummy;
+let initCb, parseCb, dummy;
 
-export function mockInit(init, parse, dep) {
+export function mockInit(init, parse) {
   initCb = init;
   parseCb = parse;
-  depCb = dep;
 }
 
 export async function init() {
@@ -49,9 +48,3 @@ export function parse(file, _config, api) {
     return dummy.map(c => api.parseJSDoc(c.value, c.log, c.context));
   return false;
 }
-
-export function shallow(file) {
-  if (depCb) depCb(...arguments);
-  if (file.includes('plugin.txt')) return true;
-  return false;
-}

From 46cd67b02d17c79d96d7d3bad9475118d6bf3ae3 Mon Sep 17 00:00:00 2001
From: Momtchil Momtchev <momtchil@momtchev.com>
Date: Fri, 3 Dec 2021 17:02:49 +0100
Subject: [PATCH 07/10] correctly sort undefined values

---
 src/sort.js | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/sort.js b/src/sort.js
index a5bde334f..17736017b 100644
--- a/src/sort.js
+++ b/src/sort.js
@@ -110,6 +110,8 @@ function compareCommentsByField(field, a, b) {
   if (akey && bkey) {
     return akey.localeCompare(bkey, undefined, { caseFirst: 'upper' });
   }
+  if (akey) return 1;
+  if (bkey) return -1;
   return 0;
 }
 

From 38bcfcbb9b7a48ebd90d969b38db957b394aa2aa Mon Sep 17 00:00:00 2001
From: Momtchil Momtchev <momtchil@momtchev.com>
Date: Fri, 3 Dec 2021 17:11:35 +0100
Subject: [PATCH 08/10] the new sort order is the correct one

---
 __tests__/lib/__snapshots__/sort.js.snap | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/__tests__/lib/__snapshots__/sort.js.snap b/__tests__/lib/__snapshots__/sort.js.snap
index dee9fc0b0..13fa12e9b 100644
--- a/__tests__/lib/__snapshots__/sort.js.snap
+++ b/__tests__/lib/__snapshots__/sort.js.snap
@@ -156,26 +156,26 @@ exports[`sort toc with files absolute path 3`] = `
 Array [
   Object {
     "context": Object {
-      "sortKey": "a",
+      "sortKey": "b",
     },
-    "kind": "function",
     "memberof": "classB",
-    "name": "apples",
+    "name": "carrot",
   },
   Object {
     "context": Object {
-      "sortKey": "c",
+      "sortKey": "a",
     },
     "kind": "function",
     "memberof": "classB",
-    "name": "bananas",
+    "name": "apples",
   },
   Object {
     "context": Object {
-      "sortKey": "b",
+      "sortKey": "c",
     },
+    "kind": "function",
     "memberof": "classB",
-    "name": "carrot",
+    "name": "bananas",
   },
 ]
 `;

From 257c54ef9771c8b9221daa5e77f32e9f18ffd29c Mon Sep 17 00:00:00 2001
From: Momtchil Momtchev <momtchil@momtchev.com>
Date: Fri, 3 Dec 2021 20:38:34 +0100
Subject: [PATCH 09/10] support sorting by memberof

---
 __tests__/lib/__snapshots__/sort.js.snap | 32 ++++++++++++++++++++++--
 __tests__/lib/sort.js                    |  8 +++++-
 docs/USAGE.md                            |  2 +-
 src/commands/shared_options.js           |  2 +-
 src/sort.js                              |  3 ++-
 5 files changed, 41 insertions(+), 6 deletions(-)

diff --git a/__tests__/lib/__snapshots__/sort.js.snap b/__tests__/lib/__snapshots__/sort.js.snap
index 13fa12e9b..f35e6bf19 100644
--- a/__tests__/lib/__snapshots__/sort.js.snap
+++ b/__tests__/lib/__snapshots__/sort.js.snap
@@ -139,7 +139,7 @@ Array [
       "sortKey": "c",
     },
     "kind": "function",
-    "memberof": "classB",
+    "memberof": "classA",
     "name": "bananas",
   },
   Object {
@@ -174,8 +174,36 @@ Array [
       "sortKey": "c",
     },
     "kind": "function",
-    "memberof": "classB",
+    "memberof": "classA",
+    "name": "bananas",
+  },
+]
+`;
+
+exports[`sort toc with files absolute path 4`] = `
+Array [
+  Object {
+    "context": Object {
+      "sortKey": "c",
+    },
+    "kind": "function",
+    "memberof": "classA",
     "name": "bananas",
   },
+  Object {
+    "context": Object {
+      "sortKey": "b",
+    },
+    "memberof": "classB",
+    "name": "carrot",
+  },
+  Object {
+    "context": Object {
+      "sortKey": "a",
+    },
+    "kind": "function",
+    "memberof": "classB",
+    "name": "apples",
+  },
 ]
 `;
diff --git a/__tests__/lib/sort.js b/__tests__/lib/sort.js
index 76ce57f3c..941b163b1 100644
--- a/__tests__/lib/sort.js
+++ b/__tests__/lib/sort.js
@@ -141,7 +141,7 @@ test('sort toc with files absolute path', function () {
     context: { sortKey: 'c' },
     name: 'bananas',
     kind: 'function',
-    memberof: 'classB'
+    memberof: 'classA'
   };
 
   const snowflake = {
@@ -159,4 +159,10 @@ test('sort toc with files absolute path', function () {
       sortOrder: ['kind', 'alpha']
     })
   ).toMatchSnapshot();
+
+  expect(
+    sort([carrot, apples, bananas], {
+      sortOrder: ['memberof', 'kind', 'alpha']
+    })
+  ).toMatchSnapshot();
 });
diff --git a/docs/USAGE.md b/docs/USAGE.md
index 5fdc6bf03..f801e2568 100644
--- a/docs/USAGE.md
+++ b/docs/USAGE.md
@@ -58,7 +58,7 @@ Options:
                                                       [boolean] [default: false]
   --sort-order               The order to sort the documentation, may be
                              specified multiple times
-                                [choices: "source", "alpha", "kind"]
+                                [choices: "source", "alpha", "kind", "memberof"]
                                                              [default: "source"]
   --output, -o               output location. omit for stdout, otherwise is a
                              filename for single-file outputs and a directory
diff --git a/src/commands/shared_options.js b/src/commands/shared_options.js
index 0379debb8..0e8464dfd 100644
--- a/src/commands/shared_options.js
+++ b/src/commands/shared_options.js
@@ -75,7 +75,7 @@ export const sharedInputOptions = {
   'sort-order': {
     describe: 'The order to sort the documentation',
     array: true,
-    choices: ['source', 'alpha', 'kind', 'access'],
+    choices: ['source', 'alpha', 'kind', 'access', 'memberof'],
     default: ['source']
   },
   resolve: {
diff --git a/src/sort.js b/src/sort.js
index 17736017b..db27b02d5 100644
--- a/src/sort.js
+++ b/src/sort.js
@@ -123,7 +123,8 @@ const sortFns = {
   alpha: compareCommentsByField.bind(null, 'name'),
   source: compareCommentsBySourceLocation,
   kind: compareCommentsByField.bind(null, 'kind'),
-  access: compareCommentsByField.bind(null, 'access')
+  access: compareCommentsByField.bind(null, 'access'),
+  memberof: compareCommentsByField.bind(null, 'memberof')
 };
 
 function sortComments(comments, sortOrder) {

From 2dc9c2f69e87e95e9ce853c26aa212f2ffb0a6f5 Mon Sep 17 00:00:00 2001
From: Momtchil Momtchev <momtchil@momtchev.com>
Date: Sat, 4 Dec 2021 13:13:40 +0100
Subject: [PATCH 10/10] support a custom sort order

---
 __tests__/lib/__snapshots__/sort.js.snap | 28 ++++++++++++++++++++++++
 __tests__/lib/sort.js                    | 26 ++++++++++++++++++++++
 docs/CONFIG.md                           | 28 ++++++++++++++++++++++++
 src/sort.js                              | 14 ++++++++++--
 4 files changed, 94 insertions(+), 2 deletions(-)

diff --git a/__tests__/lib/__snapshots__/sort.js.snap b/__tests__/lib/__snapshots__/sort.js.snap
index f35e6bf19..6a9936c66 100644
--- a/__tests__/lib/__snapshots__/sort.js.snap
+++ b/__tests__/lib/__snapshots__/sort.js.snap
@@ -1,5 +1,33 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`sort by custom order 1`] = `
+Array [
+  Object {
+    "context": Object {
+      "sortKey": "b",
+    },
+    "memberof": "classB",
+    "name": "carrot",
+  },
+  Object {
+    "context": Object {
+      "sortKey": "c",
+    },
+    "kind": "typedef",
+    "memberof": "classA",
+    "name": "bananas",
+  },
+  Object {
+    "context": Object {
+      "sortKey": "a",
+    },
+    "kind": "method",
+    "memberof": "classB",
+    "name": "apples",
+  },
+]
+`;
+
 exports[`sort toc with files 1`] = `
 Array [
   Object {
diff --git a/__tests__/lib/sort.js b/__tests__/lib/sort.js
index 941b163b1..4e0063b72 100644
--- a/__tests__/lib/sort.js
+++ b/__tests__/lib/sort.js
@@ -166,3 +166,29 @@ test('sort toc with files absolute path', function () {
     })
   ).toMatchSnapshot();
 });
+
+test('sort by custom order', function () {
+  const apples = {
+    context: { sortKey: 'a' },
+    name: 'apples',
+    kind: 'method',
+    memberof: 'classB'
+  };
+  const carrot = {
+    context: { sortKey: 'b' },
+    name: 'carrot',
+    memberof: 'classB'
+  };
+  const bananas = {
+    context: { sortKey: 'c' },
+    name: 'bananas',
+    kind: 'typedef',
+    memberof: 'classA'
+  };
+
+  expect(
+    sort([carrot, apples, bananas], {
+      sortOrder: [{ kind: ['typedef', 'method'] }, 'alpha']
+    })
+  ).toMatchSnapshot();
+});
diff --git a/docs/CONFIG.md b/docs/CONFIG.md
index 6dab84d58..1c6da18f2 100644
--- a/docs/CONFIG.md
+++ b/docs/CONFIG.md
@@ -72,3 +72,31 @@ toc:
       - shortestPath
       - salesman
 ```
+
+## Sorting
+
+Sorting options can be specified in the configuration file. Example:
+
+```yml
+sortOrder:
+  - memberof
+  - alpha
+```
+
+Additionally, a custom sort order can be given, which is not possible when using the CLI option. Example:
+
+```yml
+sortOrder:
+  - kind:
+    - namespace
+    - class
+    - interface
+    - typedef
+    - enum
+    - constant
+    - function
+    - property
+    - member
+  - memberof
+  - alpha
+```
diff --git a/src/sort.js b/src/sort.js
index db27b02d5..76551823e 100644
--- a/src/sort.js
+++ b/src/sort.js
@@ -103,11 +103,16 @@ export default function (comments, options) {
   return fixed.concat(unfixed);
 }
 
-function compareCommentsByField(field, a, b) {
+function compareCommentsByField(field, a, b, customOrder) {
   const akey = a[field];
   const bkey = b[field];
 
   if (akey && bkey) {
+    if (customOrder) {
+      const aIdx = customOrder.findIndex(o => o == akey);
+      const bIdx = customOrder.findIndex(o => o == bkey);
+      return aIdx - bIdx;
+    }
     return akey.localeCompare(bkey, undefined, { caseFirst: 'upper' });
   }
   if (akey) return 1;
@@ -130,7 +135,12 @@ const sortFns = {
 function sortComments(comments, sortOrder) {
   return comments.sort((a, b) => {
     for (const sortMethod of sortOrder || ['source']) {
-      const r = sortFns[sortMethod](a, b);
+      const sortMethodName =
+        typeof sortMethod === 'object'
+          ? Object.keys(sortMethod)[0]
+          : sortMethod;
+      const customOrder = sortMethod[sortMethodName];
+      const r = sortFns[sortMethodName](a, b, customOrder);
       if (r !== 0) return r;
     }
     return 0;