Skip to content
This repository has been archived by the owner on Mar 25, 2021. It is now read-only.

Rule: Promise-returning methods must be async #1779

Merged
merged 16 commits into from
Nov 27, 2016
Merged

Conversation

buu700
Copy link
Contributor

@buu700 buu700 commented Nov 23, 2016

PR checklist

What changes did you make?

Added the rule proposed in the linked issue.

Is there anything you'd like reviewers to focus on?

Not sure if my methods of detecting async and Promise are optimal (wasn't sure where to find detailed API documentation on the stuff under ts).

@palantirtech
Copy link
Member

Thanks for your interest in palantir/tslint, @buu700! Before we can accept your pull request, you need to sign our contributor license agreement - just visit https://cla.palantir.com/ and follow the instructions. Once you sign, I'll automatically update this pull request.

}

private test(node: ts.FunctionExpression|ts.MethodDeclaration): void {
const isAsync = node.getText().split(/[\(=]/)[0].match(/(^|\s)async($|\s)/) !== null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check for async using

Lint.hasModifier(node.modifiers, ts.SyntaxKind.AsyncKeyword)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@nchen63
Copy link
Contributor

nchen63 commented Nov 24, 2016

unit tests?


private test(node: ts.FunctionExpression|ts.MethodDeclaration): void {
const isAsync = Lint.hasModifier(node.modifiers, ts.SyntaxKind.AsyncKeyword);
const isPromise = node.type.getText().indexOf("Promise<") === 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

searching the body for Promise< can be pretty flaky. Can you use ApplyWithProgram and get the actual return type?

Copy link
Contributor

@adidahiya adidahiya Nov 24, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks like it's testing just the return type of a function. it would work in some cases, but yeah I generally agree with @nchen63 that this rule should use the type checker (unless maybe if the typedef rule is enabled? in that case you'd be forced to declare : Promise<T> everywhere and a simple syntactic check is sufficient... food for thought)

}

private test(node: ts.FunctionExpression|ts.MethodDeclaration): void {
const isAsync = Lint.hasModifier(node.modifiers, ts.SyntaxKind.AsyncKeyword);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: please avoid aligning = across statements like this; we don't generally do this elsewhere since we find it unmaintainable.


private test(node: ts.FunctionExpression|ts.MethodDeclaration): void {
const isAsync = Lint.hasModifier(node.modifiers, ts.SyntaxKind.AsyncKeyword);
const isPromise = node.type.getText().indexOf("Promise<") === 0;
Copy link
Contributor

@adidahiya adidahiya Nov 24, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks like it's testing just the return type of a function. it would work in some cases, but yeah I generally agree with @nchen63 that this rule should use the type checker (unless maybe if the typedef rule is enabled? in that case you'd be forced to declare : Promise<T> everywhere and a simple syntactic check is sufficient... food for thought)

private test(node: ts.FunctionExpression|ts.MethodDeclaration): void {
const tc = this.getTypeChecker();

const returnType = tc.typeToString(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this + line 64 now the best way to do this, or is something more reliable possible?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm fine with this.

@buu700
Copy link
Contributor Author

buu700 commented Nov 24, 2016

Not sure what's going on here, but it looks like my unit tests are failing because the return type of every Promise-returning function/method is being considered any (the number-returning functions are fine). Maybe a missing type definition?

export class Rule extends Lint.Rules.TypedRule {
/* tslint:disable:object-literal-sort-keys */
public static metadata: Lint.IRuleMetadata = {
ruleName: "promise-functions-async",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rule name doesn't match file name

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, sorry about that. Any preference on whether I change the ruleName or file name, or is there an entirely different name that would better fit your established conventions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer promise-functions-async. @adidahiya?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

promise-function-async sounds good (singular "function") -- although the rule names aren't 100% consistent (#1371), we generally use singular nouns in them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I'll go with that then.

public static metadata: Lint.IRuleMetadata = {
ruleName: "promise-functions-async",
description: "Requires any function or method that returns a promise to be marked async.",
rationale: "Ensures that a function can only either return a rejected promise or throw an Error object.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you give the "why" of the rationale? Why is it bad to violate this rule?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How's this? (Is Markdown syntax supported in these descriptions?)

Ensures that each function is only capable of 1) returning a rejected promise, or 2) throwing an Error object. In contrast, non-async Promise-returning functions are technically capable of either. This rule removes a requirement for consuming code to handle both cases.

@@ -0,0 +1,38 @@
const nonAsyncPromiseFunction = function(p: Promise<void>) { return p; };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you need declare class Promise<T>{} for this to work. Otherwise, the type checker will not know what a Promise<> is and consider it to be any

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that type is provided by lib.es2015.d.ts; I would prefer using that if possible instead of constructing our own type

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be the best way to get those types included in this test?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good question... I don't think this has come up before. perhaps we can add a "compilerOptions" block to our test tslint.json files?

N.B. as you can see from this thread (#1445), it might be a good idea to put "compilerOptions" under a namespace in the test tslint.json so as to clearly indicate it is a private API for use in this repo only and not a public API for tslint users yet.

Copy link
Contributor Author

@buu700 buu700 Nov 26, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thanks. For this PR, mind if I just put declare class Promise<T>{} at the top of the test file as @nchen63 suggested? That at least seems to resolve the immediate issue.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@buu700 yeah that's fine; @nchen63 can look into the longer term solution after this PR merges

super.visitMethodDeclaration(node);
}

private test(node: ts.FunctionExpression|ts.MethodDeclaration): void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change the type to simply ts.SignatureDeclaration

}

class PromiseAsyncWalker extends Lint.ProgramAwareRuleWalker {
public visitFunctionExpression(node: ts.FunctionExpression): void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also need visitArrowFunction and visitFunctionDeclaration

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: prefer omitting the : void return typedef on functions/methods

@@ -0,0 +1,38 @@
const nonAsyncPromiseFunction = function(p: Promise<void>) { return p; };
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0]

Copy link
Contributor

@nchen63 nchen63 Nov 25, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also need cases

x.callback(() => return new Promise<void>());

and


function x() {
    return new Promise<int>();
}

which hit the new visit* cases I suggested in the other file

optionsDescription: "Not configurable.",
options: null,
optionExamples: ["true"],
type: "functionality",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's classify this as a typescript-only rule. I know the categories aren't perfectly mutually exclusive, but I'd like to highlight the fact that you get this lint rule only via usage of the TypeScript compiler services.

options: null,
optionExamples: ["true"],
type: "functionality",
typescriptOnly: false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

despite my comment on the above line, the rule can be used with either TS or JS code, so this line is correct 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, interesting; I'd actually meant to (and forgotten to) change that as part of my last commit. How would a rule that requires type checking work with JS?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TS compiler provides intellisense for JS too, since the syntax is just a subset of TS. For example, this is enabled by default in VSCode. I haven't tested it with the compiler services, but it should work in theory.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah okay, cool.

}

class PromiseAsyncWalker extends Lint.ProgramAwareRuleWalker {
public visitFunctionExpression(node: ts.FunctionExpression): void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: prefer omitting the : void return typedef on functions/methods

@@ -0,0 +1,38 @@
const nonAsyncPromiseFunction = function(p: Promise<void>) { return p; };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good question... I don't think this has come up before. perhaps we can add a "compilerOptions" block to our test tslint.json files?

N.B. as you can see from this thread (#1445), it might be a good idea to put "compilerOptions" under a namespace in the test tslint.json so as to clearly indicate it is a private API for use in this repo only and not a public API for tslint users yet.

const nonAsyncNonPromiseArrowFunction = (n: number) => n;

class Test {
public nonAsyncPromiseMethodA(p: Promise<void>) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for big functions, seems excessive to highlight the whole thing including the body. How about just highlighting the function signature?

@@ -0,0 +1,65 @@
declare class Promise<T>{}

const nonAsyncPromiseFunctionExpressionA = function(p: Promise<void>) { return p; };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for every test that causes a lint failure, you should have a matching test that passes

const isAsync = Lint.hasModifier(node.modifiers, ts.SyntaxKind.AsyncKeyword);
const isPromise = returnType.indexOf("Promise<") === 0;

const signatureEnd = node.getText().split(/(\s+)?[\{=]/)[0].length;
Copy link
Contributor Author

@buu700 buu700 Nov 26, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any idea if there's a better way to do this? (I just put this in as a placeholder that works with the current tests, but it'd break in a lot of common cases.)

I didn't see anything under ts.TypeChecker, ts.Signature, or ts.SignatureDeclaration that looked like it would give me a string representation of the signature or the length of just the signature component of the node, so not sure right now how to go about this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try:

        let declarationLength: number;
        if (node.body == null) {
            declarationLength = node.getWidth();
        } else {
            declarationLength = node.body.getStart() - node.getStart() - 1;
        }
        this.addFailure(this.createFailure(node.getStart(), declarationLength, Rule.FAILURE_STRING));

you'll have to define test's signature as:

    private test(node: ts.SignatureDeclaration & { body?: ts.Node}) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect, thanks!

@nchen63 nchen63 merged commit 5c7cf4a into palantir:master Nov 27, 2016
@nchen63
Copy link
Contributor

nchen63 commented Nov 27, 2016

@buu700 looks good. Thanks!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants