From 23b9d122222790773c8cfcd572af257ff049fe12 Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Mon, 23 May 2016 10:07:55 -0700 Subject: [PATCH] Add TomDoc support to no-deprecated rule --- CHANGELOG.md | 1 + docs/rules/no-deprecated.md | 27 +++++++++- src/core/getExports.js | 84 ++++++++++++++++++++++++++------ tests/files/tomdoc-deprecated.js | 22 +++++++++ tests/src/rules/no-deprecated.js | 38 +++++++++++++++ 5 files changed, 156 insertions(+), 16 deletions(-) create mode 100644 tests/files/tomdoc-deprecated.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 2623abddd..467c68dc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ## [Unreleased] ### Added - Added support for multiple webpack configs ([#181], thanks [@GreenGremlin]) +- Added support TomDoc comments to `no-deprecated` ([#321], thanks [@josh]) ## [Unreleased] ### Fixed diff --git a/docs/rules/no-deprecated.md b/docs/rules/no-deprecated.md index 7f73eab78..989534ac6 100644 --- a/docs/rules/no-deprecated.md +++ b/docs/rules/no-deprecated.md @@ -5,7 +5,9 @@ **NOTE**: this rule is currently a work in progress. There may be "breaking" changes: most likely, additional cases that are flagged. Reports use of a deprecated name, as indicated by a JSDoc block with a `@deprecated` -tag, i.e. +tag or TomDoc `Deprecated: ` comment. + +using a JSDoc `@deprecated` tag: ```js // @file: ./answer.js @@ -30,6 +32,28 @@ function whatever(y, z) { } ``` +or using the TomDoc equivalent: + +```js +// Deprecated: This is what you get when you trust a mouse talk show, need to +// restart the experiment. +// +// Returns a Number nonsense +export function multiply(six, nine) { + return 42 +} +``` + +Only JSDoc is enabled by default. Other documentation styles can be enabled with +the `import/docstyle` setting. + + +```yaml +# .eslintrc.yml +settings: + import/docstyle: ['jsdoc', 'tomdoc'] +``` + ### Worklist - [x] report explicit imports on the import node @@ -39,4 +63,3 @@ function whatever(y, z) { - [x] mark module deprecated if file JSDoc has a @deprecated tag? - [ ] don't flag redeclaration of imported, deprecated names - [ ] flag destructuring - diff --git a/src/core/getExports.js b/src/core/getExports.js index befcc73d7..f0cf67614 100644 --- a/src/core/getExports.js +++ b/src/core/getExports.js @@ -97,6 +97,12 @@ export default class ExportMap { return m // can't continue } + const docstyle = (context.settings && context.settings['import/docstyle']) || ['jsdoc'] + const docStyleParsers = {} + docstyle.forEach(style => { + docStyleParsers[style] = availableDocStyleParsers[style] + }) + // attempt to collect module doc ast.comments.some(c => { if (c.type !== 'Block') return false @@ -143,7 +149,7 @@ export default class ExportMap { ast.body.forEach(function (n) { if (n.type === 'ExportDefaultDeclaration') { - const exportMeta = captureDoc(n) + const exportMeta = captureDoc(docStyleParsers, n) if (n.declaration.type === 'Identifier') { addNamespace(exportMeta, n.declaration) } @@ -174,11 +180,12 @@ export default class ExportMap { case 'FunctionDeclaration': case 'ClassDeclaration': case 'TypeAlias': // flowtype with babel-eslint parser - m.namespace.set(n.declaration.id.name, captureDoc(n)) + m.namespace.set(n.declaration.id.name, captureDoc(docStyleParsers, n)) break case 'VariableDeclaration': n.declaration.declarations.forEach((d) => - recursivePatternCapture(d.id, id => m.namespace.set(id.name, captureDoc(d, n)))) + recursivePatternCapture(d.id, id => + m.namespace.set(id.name, captureDoc(docStyleParsers, d, n)))) break } } @@ -348,33 +355,82 @@ export default class ExportMap { } /** - * parse JSDoc from the first node that has leading comments + * parse docs from the first node that has leading comments * @param {...[type]} nodes [description] * @return {{doc: object}} */ -function captureDoc(...nodes) { +function captureDoc(docStyleParsers, ...nodes) { const metadata = {} // 'some' short-circuits on first 'true' nodes.some(n => { if (!n.leadingComments) return false - // capture XSDoc - n.leadingComments.forEach(comment => { - // skip non-block comments - if (comment.value.slice(0, 4) !== '*\n *') return - try { - metadata.doc = doctrine.parse(comment.value, { unwrap: true }) - } catch (err) { - /* don't care, for now? maybe add to `errors?` */ + for (let name in docStyleParsers) { + const doc = docStyleParsers[name](n.leadingComments) + if (doc) { + metadata.doc = doc } - }) + } + return true }) return metadata } +const availableDocStyleParsers = { + jsdoc: captureJsDoc, + tomdoc: captureTomDoc, +} + +/** + * parse JSDoc from leading comments + * @param {...[type]} comments [description] + * @return {{doc: object}} + */ +function captureJsDoc(comments) { + let doc + + // capture XSDoc + comments.forEach(comment => { + // skip non-block comments + if (comment.value.slice(0, 4) !== '*\n *') return + try { + doc = doctrine.parse(comment.value, { unwrap: true }) + } catch (err) { + /* don't care, for now? maybe add to `errors?` */ + } + }) + + return doc +} + +/** + * parse TomDoc section from comments + */ +function captureTomDoc(comments) { + // collect lines up to first paragraph break + const lines = [] + for (let i = 0; i < comments.length; i++) { + const comment = comments[i] + if (comment.value.match(/^\s*$/)) break + lines.push(comment.value.trim()) + } + + // return doctrine-like object + const statusMatch = lines.join(' ').match(/^(Public|Internal|Deprecated):\s*(.+)/) + if (statusMatch) { + return { + description: statusMatch[2], + tags: [{ + title: statusMatch[1].toLowerCase(), + description: statusMatch[2], + }], + } + } +} + /** * Traverse a pattern/identifier node, calling 'callback' * for each leaf identifier. diff --git a/tests/files/tomdoc-deprecated.js b/tests/files/tomdoc-deprecated.js new file mode 100644 index 000000000..d7b8bb217 --- /dev/null +++ b/tests/files/tomdoc-deprecated.js @@ -0,0 +1,22 @@ +// Deprecated: This function is terrible. +// +// With another line comment in description. +export function fn() { return null } + +// Deprecated: this is awful, +// use NotAsBadClass. +// +// Some other description text. +export default class TerribleClass { + +} + +// Deprecated: Please stop sending/handling this action type. +export const MY_TERRIBLE_ACTION = "ugh" + +// Public: This one is fine. +// +// Returns a String "great!" +export function fine() { return "great!" } + +export function _undocumented() { return "sneaky!" } diff --git a/tests/src/rules/no-deprecated.js b/tests/src/rules/no-deprecated.js index 92660348b..ef6912dd1 100644 --- a/tests/src/rules/no-deprecated.js +++ b/tests/src/rules/no-deprecated.js @@ -13,6 +13,20 @@ ruleTester.run('no-deprecated', rule, { test({ code: "import { fine } from './deprecated'" }), test({ code: "import { _undocumented } from './deprecated'" }), + test({ + code: "import { fn } from './deprecated'", + settings: { 'import/docstyle': ['tomdoc'] } + }), + + test({ + code: "import { fine } from './tomdoc-deprecated'", + settings: { 'import/docstyle': ['tomdoc'] } + }), + test({ + code: "import { _undocumented } from './tomdoc-deprecated'", + settings: { 'import/docstyle': ['tomdoc'] } + }), + // naked namespace is fine test({ code: "import * as depd from './deprecated'" }), test({ code: "import * as depd from './deprecated'; console.log(depd.fine())" }), @@ -44,6 +58,30 @@ ruleTester.run('no-deprecated', rule, { errors: ['Deprecated: please stop sending/handling this action type.'], }), + test({ + code: "import { fn } from './deprecated'", + settings: { 'import/docstyle': ['jsdoc', 'tomdoc'] }, + errors: ["Deprecated: please use 'x' instead."], + }), + + test({ + code: "import { fn } from './tomdoc-deprecated'", + settings: { 'import/docstyle': ['tomdoc'] }, + errors: ["Deprecated: This function is terrible."], + }), + + test({ + code: "import TerribleClass from './tomdoc-deprecated'", + settings: { 'import/docstyle': ['tomdoc'] }, + errors: ['Deprecated: this is awful, use NotAsBadClass.'], + }), + + test({ + code: "import { MY_TERRIBLE_ACTION } from './tomdoc-deprecated'", + settings: { 'import/docstyle': ['tomdoc'] }, + errors: ['Deprecated: Please stop sending/handling this action type.'], + }), + // ignore redeclares test({ code: "import { MY_TERRIBLE_ACTION } from './deprecated'; function shadow(MY_TERRIBLE_ACTION) { console.log(MY_TERRIBLE_ACTION); }",