Skip to content

[no-explicit-any] support JSDoc #2939

New issue

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

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

Already on GitHub? Sign in to your account

Closed
3 tasks done
Raynos opened this issue Jan 18, 2021 · 13 comments
Closed
3 tasks done

[no-explicit-any] support JSDoc #2939

Raynos opened this issue Jan 18, 2021 · 13 comments
Labels
enhancement: plugin rule option New rule option for an existing eslint-plugin rule locked due to age Please open a new issue if you'd like to say more. See https://typescript-eslint.io/contributing. package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin wontfix This will not be worked on

Comments

@Raynos
Copy link

Raynos commented Jan 18, 2021

This is a feature request to add support for JSDoc to more rules. When linting javascript files using typescript-eslint, some rules work like no-floating-promise because its a rule that works in JS/TS and uses typescript to understand the javascript program ( including JSDoc ).

Other rules dont work because they only work on a typescript AST, for example no-explicit-any looks for the any AST node in a typescript AST.

It could support JSDoc by either parsing JSDoc or by looking at all function AST nodes and asking typescript, does it have any in the return or parameter list and then saying that the javascript function has an explicit or implicit any.

  • I have tried restarting my IDE and the issue persists.
  • I have updated to the latest version of the packages.
  • I have read the FAQ and my problem is not listed.

Repro

{
  "rules": {
    "@typescript-eslint/no-explicit-any": "error"
  }
}
/**
 * @param {object} context
 * @param {string[]} tokens
 * @returns {any}
 */
function checkTokens (context, tokens) {
  // ast.tokens
  // console.log('ast', tokens)
}
exports.checkTokens = checkTokens

Expected Result

Expected it to complain about explicit any.

Actual Result

eslint passed, no failures.

Additional Info

Versions

package version
@typescript-eslint/eslint-plugin 4.13.0
@typescript-eslint/parser 4.13.0
TypeScript 4.1.3
ESLint 7.12.1
node 12.16.3
@Raynos Raynos added package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin triage Waiting for team members to take a look labels Jan 18, 2021
@bradzacher
Copy link
Member

Type-aware lint rules work with JSDoc types because TS parses and interprets the JSDoc types when it computes type information.

For non-type-aware rules (like no-explicit-any), they don't work with JSDoc because JSDoc comments are just comments, and comments are parsed as a single node with free-text content, as per the ESTree spec.

So in order to introduce support, we'd have to introduce JSDoc parsing into the plugin. There are a few ways to do this:

(1) Explicitly encode it in the AST as part of the comment node.

TS does this (see ts-ast-viewer -- change the tree mode to node.getChildren()), so we could build on top of it (relatively) easily.

This has the following drawbacks:

  1. We'd need to create an ESTree AST spec for JSDoc syntax. This is a burden we don't really want to take on. Guarantee that once we go down this path, we'd have people complaining about all of the missing tags that we don't support because TS doesn't support them.

  2. TS parses this with its own parser, so it emits its own types - meaning we'd have weird cases people mightn't expect.
    For example in the comment /** @returns {any} */, the any is emitted as an AnyKeyword, which we would then convert to a TSAnyKeyword node. This would likely cause all sorts of unexpected behaviour in lint rules.

  3. You'd still have to solve the comment attachment problem.
    TS does attach JSDoc comments its AST nodes - however note that:

    • These properties are marked as @internal, meaning they're not explicitly for public consumption. We could consume them (as we have done for a few other props), but TS could happily change those internal implementation details in any release, which would break our tooling.
    • ESTree does not concern itself with JSDoc attachment, so that's another deviation from the spec we'd be implementing. ESTree explicitly does not deal with comment attachment due to its complexity, and we'd only be supporting what TS supports -- which isn't actually documented anywhere, meaning it's hard to encode that in a specification.

(2) Ignore all of that and introduce a JSDoc parser

There are numerous JSDoc parsing libraries out there that we could pull in as a dependency, but that's an added dependency which we'd be bringing in and have to trust (we're actually pretty lightweight in terms of runtime dependencies).

This has the following drawbacks:

  1. This comes at a price, not only at install time, but also at runtime. There's no marker to tell if a comment is a JSDoc comment, so you'd have to run every single block comment through the parser. It's just time that needs to be spent.

  2. Rules would have to explicitly be built to handle this. You wouldn't get it for free because it's not encoded in the AST. Also unless we build some caching infra, then every rule would separately parse the comments, which compounds the runtime to O(n*m) (where n = number of block comments and m = number of rules that care about jsdoc types).

  3. Rules would have to be aware of when the JSDoc type actually matters - which is only in .js/.jsx files. In TS files the @returns {any} is essentially meaningless.
    We purposely do not encode any file extension handling in our lint rules (beyond .d.ts, because TS treats .d.ts uniquely in many cases).


With all that context aside - I have thought about this, for sure, but I thought that the value was not worth the effort given that:

  • TS only respects JSDoc comment types in JS files (this is the biggest reason)
    • Because of this, in the context of TypeScript files, there's no benefit to JSDoc support because they are meaningless.
  • There are plugins designed and built around JSDoc (like eslint-plugin-jsdoc)
    • These plugins are built around JSDoc and have caching etc built in.
    • They can easily have rules built out to validate types as appropriate.

@bradzacher bradzacher added awaiting response Issues waiting for a reply from the OP or another party and removed triage Waiting for team members to take a look labels Jan 18, 2021
@voxpelli
Copy link

Is there a list for all such rules? One could then make a companion module based on top of the JSDoc plugin that asserts those rules against JSDoc comments? (Me and @Raynos are both using more and more JSDoc: https://github.com/voxpelli/types-in-js)

@Raynos
Copy link
Author

Raynos commented Jan 18, 2021

I discussed in passing about this with @voxpelli

perhaps it is indeed out of scope for this package and better handled in the JSdoc package instead.

@bradzacher
Copy link
Member

It would definitely be possible to build rules on top of the tooling provided by this project without requiring any changes at all.
For example - here is some sample code which runs in node.
I couldn't be bothered making it as an eslint rule because it's harder to write an eslint rule as a quick script, but there's little difference in terms of how it'd work as an eslint rule.

import {
  parseAndGenerateServices,
  simpleTraverse,
} from "@typescript-eslint/typescript-estree";
import { TSESTree } from "@typescript-eslint/experimental-utils";
import * as ts from "typescript";

const result = parseAndGenerateServices(
  `
/**
 * @param {object} context
 * @param {string[]} tokens
 * @returns {any}
 */
function checkTokens (context, tokens) {
  // ast.tokens
  // console.log('ast', tokens)
}
`,
  {
    comment: true,
    preserveNodeMaps: true,
    loc: true,
    range: true,
    tokens: true,
  }
);

interface JSDocContainer {
  /* @internal */ jsDoc?: ts.JSDoc[]; // JSDoc that directly precedes this node
}

simpleTraverse(result.ast, {
  FunctionDeclaration(node) {
    const tsNode = result.services.esTreeNodeToTSNodeMap.get(node);
    const leadingCommentsTS = (tsNode as JSDocContainer).jsDoc ?? [];
    for (const comment of leadingCommentsTS) {
      console.log(
        comment.tags?.map((tag) => {
          const type = (() => {
            switch (tag.kind) {
              case ts.SyntaxKind.JSDocParameterTag:
                return (tag as ts.JSDocParameterTag).typeExpression?.type.getText();

              case ts.SyntaxKind.JSDocReturnTag:
                return (tag as ts.JSDocReturnTag).typeExpression?.type.getText();

              default:
                return null;
            }
          })();
          return {
            kindInEnglish: ts.SyntaxKind[tag.kind],
            tagName: tag.tagName.getText(),
            type,
          };
        })
      );
    }
  },
});

Running this code results in the following:

% yarn ts-node -T ./src/test-jsdoc.ts
yarn run v1.22.4
$ /Users/bradzacher/temp/testbed/node_modules/.bin/ts-node -T ./src/test-jsdoc.ts
[
  {
    kindInEnglish: 'JSDocParameterTag',
    tagName: 'param',
    type: 'object'
  },
  {
    kindInEnglish: 'JSDocParameterTag',
    tagName: 'param',
    type: 'string[]'
  },
  { kindInEnglish: 'JSDocReturnTag', tagName: 'returns', type: 'any' }
]
✨  Done in 0.82s.

@Raynos
Copy link
Author

Raynos commented Jan 19, 2021

One seperate question I have is can we ask the typescript folks to make the jsdoc nodes in the AST public and not @internal ?

That would be a small but useful step towards this.

@bradzacher
Copy link
Member

No reason you couldn't!

Probs worth checking to see if there's any reason they couldn't expose it.

@Raynos
Copy link
Author

Raynos commented Jan 19, 2021

Apparently they have implemented a function for this ( https://github.com/ajafff/tsutils/blob/752e8024003afc23c55f1fae8fbfb015068b9c10/util/util.ts#L1226 ).

Does that solve the attachment problem ?

@bradzacher
Copy link
Member

It looks like TS does expose a function for this: ts.getJSDocTags(node).
So you could instead use this function instead of the internal properties.

import {
  parseAndGenerateServices,
  simpleTraverse,
} from "@typescript-eslint/typescript-estree";
import * as ts from "typescript";

const result = parseAndGenerateServices(
  `
/**
 * @param {object} context
 * @param {string[]} tokens
 * @returns {any}
 */
function checkTokens (context, tokens) {
  // ast.tokens
  // console.log('ast', tokens)
}
`,
  {
    comment: true,
    preserveNodeMaps: true,
    loc: true,
    range: true,
    tokens: true,
  }
);

simpleTraverse(result.ast, {
  FunctionDeclaration(node) {
    const tsNode = result.services.esTreeNodeToTSNodeMap.get(node);
    const jsdocTags = ts.getJSDocTags(tsNode);
    for (const tag of jsdocTags) {
      const type = (() => {
        switch (tag.kind) {
          case ts.SyntaxKind.JSDocParameterTag:
            return (tag as ts.JSDocParameterTag).typeExpression?.type.getText();

          case ts.SyntaxKind.JSDocReturnTag:
            return (tag as ts.JSDocReturnTag).typeExpression?.type.getText();

          default:
            return null;
        }
      })();
      console.log({
        kindInEnglish: ts.SyntaxKind[tag.kind],
        tagName: tag.tagName.getText(),
        type,
      });
    }
  },
});
% yarn ts-node -T ./src/test-jsdoc.ts
yarn run v1.22.4
$ /Users/bradzacher/temp/testbed/node_modules/.bin/ts-node -T ./src/test-jsdoc.ts
{
  kindInEnglish: 'JSDocParameterTag',
  tagName: 'param',
  type: 'object'
}
{
  kindInEnglish: 'JSDocParameterTag',
  tagName: 'param',
  type: 'string[]'
}
{ kindInEnglish: 'JSDocReturnTag', tagName: 'returns', type: 'any' }
✨  Done in 0.89s.

@bradzacher bradzacher added enhancement: plugin rule option New rule option for an existing eslint-plugin rule and removed awaiting response Issues waiting for a reply from the OP or another party labels Feb 18, 2021
@JoshuaKGoldberg JoshuaKGoldberg added the accepting prs Go ahead, send a pull request that resolves this issue label Oct 25, 2021
@boneskull
Copy link

I'm one of those who writes a lot of JS with TS types in the docstrings. While this works great in VS Code, it'd be cool if I could use eslint to validate those types instead of having to run tsc to get type errors outside of the VS Code context.

Further, if checkJs is false in the tsconfig.json, I'd want to only type-check JS files that have the // @ts-check pragma/directive. This would help projects which are in the process of migrating from JS to JS-with-typed-docstrings (or whatever it's called) but have not yet fully done so. If a JS file is missing this directive, all of the other non-TS-specific ESLint rules should still apply.

@bradzacher
Copy link
Member

Further, if checkJs is false in the tsconfig.json, I'd want to only type-check JS files that have the // @ts-check pragma/directive.

It is not possible for you to configure ESLint to only check these files, nor is it something we would consider in this project.

We purposely do not deal with transitional codebase states because whilst transitional states are by their nature temporary; supporting them has a high maintenance burden and is permanent.
Our volunteer maintainers don't want to burn valuable and limited cycles building tooling and our community members would rather spend their time actually doing their migration.

I'd suggest instead of going "JS with no checks -> JS with @ts-check -> TS" that you instead skip the middle step and just migrate a file to TS directly. Or if you must maintain the middle step - encode it into the filename. That way you can use ESLint overrides configs to configure rules on/off.

@boneskull
Copy link

I'm not sure where you read that TS was the end-state of the migration. What would be the end-state would be having no directives and just turning checkJs on in the typescript config. Anyway, I understand if you don't want to support the @ts-check

@brettz9
Copy link

brettz9 commented Mar 19, 2022

FWIW, for regular JavaScript users, I've created the experimental @es-joy/jsdoc-eslint-parser (GitHub, npm).

@bradzacher If you do ever end up going the AST route per #2939 (comment) , I would suggest coordinating with us and jsdoc-type-pratt-parser to kind of standardize the AST that is produced.

If you opt for option 2, then you might be interested in jsdoccomment which we use in jsdoc-eslint-parser as well as eslint-plugin-jsdoc for certain AST-aware rules.

(Note, that our use of the AST within eslint-plugin-jsdoc does not give all the advantages of jsdoc-eslint-parser (unless you are actually using jsdoc-eslint-parser as your parser) since eslint-plugin-jsdoc only allows separate additive selector expressions for JavaScript AST and JSDoc AST relative to targeting comments (e.g., targeting specific JSDoc Block AST above specific JavaScript AST), not selectors which combine them in the same expression (e.g., to find all function declarations which have a @param tag above them).)

Anyways, jsdocdoccomment is mostly an adapter of comment-parser that integrates jsdoc-type-pratt-parser for its JSDoc type AST.

Although I know TypeScript's approach is to define separate interfaces for each tag, our approach is generic to tag name, though it does allow targeting tags by name, e.g., JsdocTag[tag="param"] (we use tag instead of name to avoid confusion with the name part of a JSDoc expression). The parsed version of the JSDoc type does have to be supported by jsdoc-type-pratt-parser (which doesn't yet support all of TypeScript types). Another advantage over TypeScript interfaces is that we should be faithfully preserving all whitespace, thus allowing the easier targeting of comment AST based on stylstic features, though without requiring awareness of stylistic features when just targeting the tag, the type, or name, etc.

One particular reason the jsdoc-eslint-parser project is still experimental is that in order to avoid tough questions on the precise node for attachment to which you alluded (and that perhaps should remain available on multiple nodes given the different attachment points users use?), we indiscriminately (and inefficiently) may attach the same JSDoc AST to different nodes, e.g. for:

/** A */
const a = /** B */ function a () {}

...the JSDoc for the function expression might be found at A or B, so in our AST, we add a jsdoc property visitor key to each Node whose comment AST is set to the nearest expected JSDoc block. If there is JSDoc only at point A, the function expression and variable declaration may both have a copy.

We also add a jsdocBlocks property on Program for providing structured JSDoc AST for unattached comments.

Note that even if you don't support JSDoc for your rules, allowing users to choose a JSDoc-aware version of your parser could have the benefit of allowing the likes of eslint-plugin-jsdoc to target specific TypeScript + JSDoc structures, e.g., especially via our multi-purpose no-missing-syntax or no-restricted-syntax rules which allow a project to insist that specific comments are present or are not present.

(And while I'm chiming in, I might just add my thanks here for all your great work on your essential tool!)

@bradzacher bradzacher added wontfix This will not be worked on and removed accepting prs Go ahead, send a pull request that resolves this issue labels May 28, 2024
@bradzacher
Copy link
Member

At this point we're in the state that we don't really want to action this.
Supporting JSDoc is very high cost:

  • we need a way to extract the information from the comments
  • we need to update all our rules to leverage this information
  • we need to ensure we maintain this in perpetuity without introducing regressions

Ultimately the intersection of our userbase and JSDoc-in-JS-as-a-typechecking-style users is pretty small - so it's very low value for us to build out and maintain all of the required infrastructure here.

Additionally parsing JSDoc is high-cost. TS parses JSDoc by default and it slows down lint runs because of it. It's far from trivial to parse all of this. We ideally want to leverage TS's recent JSDocParsingMode flag to turn off JSDoc parsing by default so that we can improve performance (#7906).

Finally this issue has been opened for 3 years and has gathered very few participants + reactions - reinforcing my first point. The cost / value ratio just isn't there for us to consider doing this within this project.

I think it makes more sense for all things JSDoc to live within eslint-plugin-jsdoc so that users can opt-in to the cost of support and the effort of building/maintaining tooling around JSDoc rules/infra can be centralised.

@bradzacher bradzacher closed this as not planned Won't fix, can't repro, duplicate, stale May 28, 2024
@github-actions github-actions bot added the locked due to age Please open a new issue if you'd like to say more. See https://typescript-eslint.io/contributing. label Jun 9, 2024
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jun 9, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement: plugin rule option New rule option for an existing eslint-plugin rule locked due to age Please open a new issue if you'd like to say more. See https://typescript-eslint.io/contributing. package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin wontfix This will not be worked on
Projects
None yet
Development

No branches or pull requests

6 participants