-
Notifications
You must be signed in to change notification settings - Fork 885
Rule: Promise
-returning methods must be async
#1779
Conversation
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; |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
Not sure what's going on here, but it looks like my unit tests are failing because the return type of every |
export class Rule extends Lint.Rules.TypedRule { | ||
/* tslint:disable:object-literal-sort-keys */ | ||
public static metadata: Lint.IRuleMetadata = { | ||
ruleName: "promise-functions-async", |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.", |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; }; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
super.visitMethodDeclaration(node); | ||
} | ||
|
||
private test(node: ts.FunctionExpression|ts.MethodDeclaration): void { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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] | |||
|
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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 👍
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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; }; |
There was a problem hiding this comment.
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>) { |
There was a problem hiding this comment.
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; }; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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}) {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perfect, thanks!
@buu700 looks good. Thanks! |
PR checklist
async
forPromise
returning methods. #1699What 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
).