Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/tsgolint/headless.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ func runHeadless(args []string) int {
rules[i] = linter.ConfiguredRule{
Name: r.Name,
Run: func(ctx rule.RuleContext) rule.RuleListeners {
return r.Run(ctx, nil)
return r.Run(ctx, headlessRule.Options)
},
}
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/tsgolint/payload.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ type headlessConfig struct {
}

type headlessRule struct {
Name string `json:"name"`
Name string `json:"name"`
Options any `json:"options,omitempty"`
}

func deserializePayload(data []byte) (*headlessPayload, error) {
Expand Down
65 changes: 65 additions & 0 deletions e2e/__snapshots__/snapshot.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,34 @@ exports[`TSGoLint E2E Snapshot Tests > should generate consistent diagnostics sn
},
"rule": "require-await",
},
{
"file_path": "fixtures/basic/rules/no-floating-promises/void.ts",
"fixes": [],
"message": {
"description": "This rule requires the \`strictNullChecks\` compiler option to be turned on to function correctly.",
"id": "noStrictNullCheck",
},
"range": {
"end": 0,
"pos": 0,
},
"rule": "no-unnecessary-boolean-literal-compare",
"suggestions": [],
},
{
"file_path": "fixtures/basic/rules/no-floating-promises/void.ts",
"fixes": [],
"message": {
"description": "Function has no 'await' expression.",
"id": "missingAwait",
},
"range": {
"end": 53,
"pos": 0,
},
"rule": "require-await",
"suggestions": [],
},
{
"file_path": "fixtures/basic/rules/no-for-in-array/index.ts",
"kind": 0,
Expand Down Expand Up @@ -4345,3 +4373,40 @@ exports[`TSGoLint E2E Snapshot Tests > should handle tsconfig diagnostics when T
},
]
`;

exports[`TSGoLint E2E Snapshot Tests > supports passing rule config 1`] = `
[
{
"file_path": "fixtures/basic/rules/no-floating-promises/void.ts",
"fixes": [],
"message": {
"description": "Promises must be awaited.",
"help": "The promise must end with a call to .catch, or end with a call to .then with a rejection handler.",
"id": "floating",
},
"range": {
"end": 76,
"pos": 54,
},
"rule": "no-floating-promises",
"suggestions": [
{
"fixes": [
{
"range": {
"end": 58,
"pos": 54,
},
"text": "await",
},
],
"message": {
"description": "Promises must be awaited.",
"help": "The promise must end with a call to .catch, or end with a call to .then with a rejection handler.",
"id": "floating",
},
},
],
},
]
`;
4 changes: 4 additions & 0 deletions e2e/fixtures/basic/rules/no-floating-promises/void.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
async function returnsPromise() {
return 'value';
}
void returnsPromise();
41 changes: 41 additions & 0 deletions e2e/snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@
return allFiles;
}

function resolveTestFilePath(relativePath: string): string {
return join(FIXTURES_DIR, relativePath);
}

function generateConfig(files: string[], rules: readonly (typeof ALL_RULES)[number][] = ALL_RULES): string {
// Headless payload format:
// ```json
Expand Down Expand Up @@ -222,9 +226,46 @@

expect(diagnostics.length).toBeGreaterThan(0);

expect(diagnostics).toMatchSnapshot();

Check failure on line 229 in e2e/snapshot.test.ts

View workflow job for this annotation

GitHub Actions / test

snapshot.test.ts > TSGoLint E2E Snapshot Tests > should generate consistent diagnostics snapshot

Error: Snapshot `TSGoLint E2E Snapshot Tests > should generate consistent diagnostics snapshot 1` mismatched - Expected + Received @@ -665,35 +665,33 @@ }, "rule": "require-await", }, { "file_path": "fixtures/basic/rules/no-floating-promises/void.ts", - "fixes": [], + "kind": 0, "message": { "description": "This rule requires the `strictNullChecks` compiler option to be turned on to function correctly.", "id": "noStrictNullCheck", }, "range": { "end": 0, "pos": 0, }, "rule": "no-unnecessary-boolean-literal-compare", - "suggestions": [], }, { "file_path": "fixtures/basic/rules/no-floating-promises/void.ts", - "fixes": [], + "kind": 0, "message": { "description": "Function has no 'await' expression.", "id": "missingAwait", }, "range": { "end": 53, "pos": 0, }, "rule": "require-await", - "suggestions": [], }, { "file_path": "fixtures/basic/rules/no-for-in-array/index.ts", "kind": 0, "message": { ❯ snapshot.test.ts:229:25
});

it('supports passing rule config', async () => {
const testFile = resolveTestFilePath('basic/rules/no-floating-promises/void.ts');
const config = (ignoreVoid: boolean) => ({
version: 2,
configs: [
{
file_paths: [testFile],
rules: [
{
name: 'no-floating-promises',
options: { ignoreVoid },
},
],
},
],
});

let output: Buffer;
output = execFileSync(TSGOLINT_BIN, ['headless'], {
input: JSON.stringify(config(false)),
});

let diagnostics = parseHeadlessOutput(output);
diagnostics = sortDiagnostics(diagnostics);

expect(diagnostics.length).toBeGreaterThan(0);
expect(diagnostics).toMatchSnapshot();

Check failure on line 258 in e2e/snapshot.test.ts

View workflow job for this annotation

GitHub Actions / test

snapshot.test.ts > TSGoLint E2E Snapshot Tests > supports passing rule config

Error: Snapshot `TSGoLint E2E Snapshot Tests > supports passing rule config 1` mismatched - Expected + Received [ { "file_path": "fixtures/basic/rules/no-floating-promises/void.ts", - "fixes": [], + "kind": 0, "message": { "description": "Promises must be awaited.", "help": "The promise must end with a call to .catch, or end with a call to .then with a rejection handler.", "id": "floating", }, "range": { "end": 76, "pos": 54, }, "rule": "no-floating-promises", - "suggestions": [ - { - "fixes": [ - { - "range": { - "end": 58, - "pos": 54, - }, - "text": "await", - }, - ], - "message": { - "description": "Promises must be awaited.", - "help": "The promise must end with a call to .catch, or end with a call to .then with a rejection handler.", - "id": "floating", - }, - }, - ], }, ] ❯ snapshot.test.ts:258:25

// Re-run with ignoreVoid=true, should have no diagnostics
output = execFileSync(TSGOLINT_BIN, ['headless'], {
input: JSON.stringify(config(true)),
});

diagnostics = parseHeadlessOutput(output);
expect(diagnostics.length).toBe(0);
});

it.runIf(process.platform === 'win32')(
'should not panic with mixed forward/backslash paths from Rust (issue #143)',
async () => {
Expand Down
64 changes: 30 additions & 34 deletions internal/rules/no_floating_promises/no_floating_promises.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
package no_floating_promises

import (
"encoding/json"

Check failure on line 4 in internal/rules/no_floating_promises/no_floating_promises.go

View workflow job for this annotation

GitHub Actions / lint

import 'encoding/json' is not allowed from list 'main': Use "github.com/go-json-experiment/json" instead. (depguard)

"github.com/microsoft/typescript-go/shim/ast"
"github.com/microsoft/typescript-go/shim/checker"
"github.com/microsoft/typescript-go/shim/scanner"
"github.com/typescript-eslint/tsgolint/internal/rule"
"github.com/typescript-eslint/tsgolint/internal/utils"
)

type NoFloatingPromisesOptions struct {
AllowForKnownSafeCalls []utils.TypeOrValueSpecifier
AllowForKnownSafeCallsInline []string
AllowForKnownSafePromises []utils.TypeOrValueSpecifier
AllowForKnownSafePromisesInline []string
CheckThenables *bool
IgnoreIIFE *bool
IgnoreVoid *bool
}
// type NoFloatingPromisesOptions struct {
// AllowForKnownSafeCalls []utils.TypeOrValueSpecifier
// AllowForKnownSafeCallsInline []string
// AllowForKnownSafePromises []utils.TypeOrValueSpecifier
// AllowForKnownSafePromisesInline []string
// CheckThenables *bool
// IgnoreIIFE *bool
// IgnoreVoid *bool
// }

var messageBase = "Promises must be awaited."

Expand Down Expand Up @@ -87,23 +89,19 @@
var NoFloatingPromisesRule = rule.Rule{
Name: "no-floating-promises",
Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
opts, ok := options.(NoFloatingPromisesOptions)
if !ok {
opts = NoFloatingPromisesOptions{
AllowForKnownSafeCalls: []utils.TypeOrValueSpecifier{},
AllowForKnownSafeCallsInline: []string{},
AllowForKnownSafePromises: []utils.TypeOrValueSpecifier{},
AllowForKnownSafePromisesInline: []string{},
// If we already have options of the correct type, use them directly.
var opts NoFloatingPromisesOptions
if castOpts, ok := options.(NoFloatingPromisesOptions); ok {
opts = castOpts
} else {
// Otherwise, attempt to unmarshal from JSON.
optsBytes, err := json.Marshal(options)
if err != nil {
panic("NoFloatingPromisesRule: failed to marshal options: " + err.Error())
}
if err := json.Unmarshal(optsBytes, &opts); err != nil {
panic("NoFloatingPromisesRule: failed to unmarshal options: " + err.Error())
}
}
if opts.CheckThenables == nil {
opts.CheckThenables = utils.Ref(false)
}
if opts.IgnoreIIFE == nil {
opts.IgnoreIIFE = utils.Ref(false)
}
if opts.IgnoreVoid == nil {
opts.IgnoreVoid = utils.Ref(true)
}

isHigherPrecedenceThanUnary := func(node *ast.Node) bool {
Expand Down Expand Up @@ -163,10 +161,9 @@
}

// The highest priority is to allow anything allowlisted
if utils.TypeMatchesSomeSpecifier(
if utils.TypeMatchesSomeSpecifierInterface(
t,
opts.AllowForKnownSafePromises,
opts.AllowForKnownSafePromisesInline,
ctx.Program,
) {
return false
Expand All @@ -181,7 +178,7 @@
}

// ...and only check all Thenables if explicitly told to
if !*opts.CheckThenables {
if !opts.CheckThenables {
return false
}

Expand Down Expand Up @@ -237,10 +234,9 @@

t := ctx.TypeChecker.GetTypeAtLocation(node.AsCallExpression().Expression)

return utils.TypeMatchesSomeSpecifier(
return utils.TypeMatchesSomeSpecifierInterface(
t,
opts.AllowForKnownSafeCalls,
opts.AllowForKnownSafeCallsInline,
ctx.Program,
)
}
Expand Down Expand Up @@ -290,7 +286,7 @@
return isUnhandledPromise(expr.Right)
}

if !*opts.IgnoreVoid && ast.IsVoidExpression(node) {
if !opts.IgnoreVoid && ast.IsVoidExpression(node) {
// Similarly, a `void` expression always returns undefined, so we need to
// see what's inside it without checking the type of the overall expression.
return isUnhandledPromise(node.Expression())
Expand Down Expand Up @@ -379,7 +375,7 @@
ast.KindExpressionStatement: func(node *ast.Node) {
exprStatement := node.AsExpressionStatement()

if *opts.IgnoreIIFE && isAsyncIife(exprStatement) {
if opts.IgnoreIIFE && isAsyncIife(exprStatement) {
return
}

Expand All @@ -396,13 +392,13 @@
}
if promiseArray {
var msg rule.RuleMessage
if *opts.IgnoreVoid {
if opts.IgnoreVoid {
msg = buildFloatingPromiseArrayVoidMessage()
} else {
msg = buildFloatingPromiseArrayMessage()
}
ctx.ReportNode(node, msg)
} else if *opts.IgnoreVoid {
} else if opts.IgnoreVoid {
var msg rule.RuleMessage
if nonFunctionHandler {
msg = buildFloatingUselessRejectionHandlerVoidMessage()
Expand Down
Loading
Loading