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 @@ -286,7 +286,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 @@ -28,7 +28,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 @@ -722,6 +722,34 @@ exports[`TSGoLint E2E Snapshot Tests > should generate consistent diagnostics sn
"rule": "require-await",
"suggestions": [],
},
{
"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",
"fixes": [],
Expand Down Expand Up @@ -4480,3 +4508,40 @@ If your function does not access \`this\`, you can annotate it with \`this: void
},
]
`;

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 @@ -145,6 +145,10 @@ async function getTestFiles(testPath: string): Promise<string[]> {
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 @@ -194,6 +198,43 @@ describe('TSGoLint E2E Snapshot Tests', () => {
expect(diagnostics).toMatchSnapshot();
});

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();

// 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
98 changes: 73 additions & 25 deletions internal/rules/no_floating_promises/no_floating_promises.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package no_floating_promises

import (
"github.com/go-json-experiment/json"
"github.com/microsoft/typescript-go/shim/ast"
"github.com/microsoft/typescript-go/shim/checker"
"github.com/microsoft/typescript-go/shim/scanner"
Expand All @@ -9,13 +10,77 @@ import (
)

type NoFloatingPromisesOptions struct {
AllowForKnownSafeCalls []utils.TypeOrValueSpecifier
AllowForKnownSafeCallsInline []string
AllowForKnownSafePromises []utils.TypeOrValueSpecifier
AllowForKnownSafePromisesInline []string
CheckThenables *bool
IgnoreIIFE *bool
IgnoreVoid *bool
AllowForKnownSafeCalls []utils.TypeOrValueSpecifier `json:"allowForKnownSafeCalls"`
AllowForKnownSafeCallsInline []string `json:"allowForKnownSafeCallsInline"`
AllowForKnownSafePromises []utils.TypeOrValueSpecifier `json:"allowForKnownSafePromises"`
AllowForKnownSafePromisesInline []string `json:"allowForKnownSafePromisesInline"`
CheckThenables *bool `json:"checkThenables"`
IgnoreIIFE *bool `json:"ignoreIIFE"`
IgnoreVoid *bool `json:"ignoreVoid"`
}

// parseNoFloatingPromisesOptions converts a loosely-typed options value into a
// fully-populated NoFloatingPromisesOptions struct. Missing fields are filled
// in with defaults, and if the input is not understood, defaults are returned.
func parseNoFloatingPromisesOptions(raw any) NoFloatingPromisesOptions {
// Base defaults
defaults := NoFloatingPromisesOptions{
AllowForKnownSafeCalls: []utils.TypeOrValueSpecifier{},
AllowForKnownSafeCallsInline: []string{},
AllowForKnownSafePromises: []utils.TypeOrValueSpecifier{},
AllowForKnownSafePromisesInline: []string{},
CheckThenables: utils.Ref(false),
IgnoreIIFE: utils.Ref(false),
IgnoreVoid: utils.Ref(true),
}

// Helper to normalize zero-value / nil internals
normalize := func(o NoFloatingPromisesOptions) NoFloatingPromisesOptions {
if o.AllowForKnownSafeCalls == nil {
o.AllowForKnownSafeCalls = defaults.AllowForKnownSafeCalls
}
if o.AllowForKnownSafeCallsInline == nil {
o.AllowForKnownSafeCallsInline = defaults.AllowForKnownSafeCallsInline
}
if o.AllowForKnownSafePromises == nil {
o.AllowForKnownSafePromises = defaults.AllowForKnownSafePromises
}
if o.AllowForKnownSafePromisesInline == nil {
o.AllowForKnownSafePromisesInline = defaults.AllowForKnownSafePromisesInline
}
if o.CheckThenables == nil {
o.CheckThenables = defaults.CheckThenables
}
if o.IgnoreIIFE == nil {
o.IgnoreIIFE = defaults.IgnoreIIFE
}
if o.IgnoreVoid == nil {
o.IgnoreVoid = defaults.IgnoreVoid
}
return o
}

if raw == nil {
return defaults
}

switch v := raw.(type) {
case NoFloatingPromisesOptions:
return normalize(v)
case map[string]any:
// Marshal then unmarshal to leverage JSON tag inference (field names)
b, err := json.Marshal(v)
if err != nil {
return defaults
}
var o NoFloatingPromisesOptions
if err := json.Unmarshal(b, &o); err != nil {
return defaults
}
return normalize(o)
default:
return defaults
}
}

var messageBase = "Promises must be awaited."
Expand Down Expand Up @@ -87,24 +152,7 @@ func buildFloatingVoidMessage() rule.RuleMessage {
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 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)
}
opts := parseNoFloatingPromisesOptions(options)

isHigherPrecedenceThanUnary := func(node *ast.Node) bool {
operator := ast.KindUnknown
Expand Down