newtests/lsp/code-action/test.js (2,258 lines of code) (raw):
/*
* @flow
* @noformat
*/
import type {Suite} from 'flow-dev-tools/src/test/Suite';
const {suite, test} = require('flow-dev-tools/src/test/Tester');
module.exports = (suite(
({
lspStartAndConnect,
lspStart,
lspRequest,
lspInitializeParams,
lspRequestAndWaitUntilResponse,
addFile,
lspIgnoreStatusAndCancellation,
}) => [
test('initialize with code actions support', [
lspStart({needsFlowServer: false}),
lspRequestAndWaitUntilResponse(
'initialize',
lspInitializeParams,
).verifyAllLSPMessagesInStep(
[
[
'initialize',
'{"codeActionProvider":{"codeActionKinds":["refactor.extract","quickfix"]}}',
],
],
[...lspIgnoreStatusAndCancellation],
),
]),
test('initialize without quickfix support', [
lspStart({needsFlowServer: false}),
lspRequestAndWaitUntilResponse('initialize', {
...lspInitializeParams,
capabilities: {
...lspInitializeParams.capabilities,
textDocument: {
...lspInitializeParams.capabilities.textDocument,
codeAction: {
codeActionLiteralSupport: {
codeActionKind: {
valueSet: ['refactor.extract'],
},
},
},
},
},
}).verifyAllLSPMessagesInStep(
[
[
'initialize',
'{"codeActionProvider":{"codeActionKinds":["refactor.extract"]}}',
],
],
[...lspIgnoreStatusAndCancellation],
),
]),
test('initialize without refactor.extract support', [
lspStart({needsFlowServer: false}),
lspRequestAndWaitUntilResponse('initialize', {
...lspInitializeParams,
capabilities: {
...lspInitializeParams.capabilities,
textDocument: {
...lspInitializeParams.capabilities.textDocument,
codeAction: {
codeActionLiteralSupport: {
codeActionKind: {
valueSet: ['quickfix'],
},
},
},
},
},
}).verifyAllLSPMessagesInStep(
[
[
'initialize',
'{"codeActionProvider":{"codeActionKinds":["quickfix"]}}',
],
],
[...lspIgnoreStatusAndCancellation],
),
]),
test('initialize without any code actions support', [
lspStart({needsFlowServer: false}),
lspRequestAndWaitUntilResponse('initialize', {
...lspInitializeParams,
capabilities: {
...lspInitializeParams.capabilities,
textDocument: {
...lspInitializeParams.capabilities.textDocument,
codeAction: {},
},
},
}).verifyAllLSPMessagesInStep(
[['initialize', '{"codeActionProvider":false}']],
[...lspIgnoreStatusAndCancellation],
),
]),
test('provide quickfix for adding optional chaining', [
addFile('add-optional-chaining.js.ignored', 'add-optional-chaining.js'),
lspStartAndConnect(),
lspRequestAndWaitUntilResponse('textDocument/codeAction', {
textDocument: {
uri: '<PLACEHOLDER_PROJECT_URL>/add-optional-chaining.js',
},
range: {
start: {
line: 3,
character: 4,
},
end: {
line: 3,
character: 7,
},
},
context: {
only: ['quickfix'],
diagnostics: [],
},
}).verifyAllLSPMessagesInStep(
[
{
method: 'textDocument/codeAction',
result: [
{
title: 'Add optional chaining for object that might be `null`',
kind: 'quickfix',
diagnostics: [],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/add-optional-chaining.js': [
{
range: {
start: {
line: 3,
character: 0,
},
end: {
line: 3,
character: 7,
},
},
newText: 'foo?.bar',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'add_optional_chaining',
'Add optional chaining for object that might be `null`',
],
},
},
{
title:
'Add optional chaining for object that might be `undefined`',
kind: 'quickfix',
diagnostics: [],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/add-optional-chaining.js': [
{
range: {
start: {
line: 3,
character: 0,
},
end: {
line: 3,
character: 7,
},
},
newText: 'foo?.bar',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'add_optional_chaining',
'Add optional chaining for object that might be `undefined`',
],
},
},
],
},
],
['textDocument/publishDiagnostics', ...lspIgnoreStatusAndCancellation],
),
lspRequestAndWaitUntilResponse('textDocument/codeAction', {
textDocument: {
uri: '<PLACEHOLDER_PROJECT_URL>/add-optional-chaining.js',
},
range: {
start: {
line: 5,
character: 7,
},
end: {
line: 5,
character: 10,
},
},
context: {
only: ['quickfix'],
diagnostics: [],
},
}).verifyAllLSPMessagesInStep(
[
{
method: 'textDocument/codeAction',
result: [
{
title: 'Add optional chaining for object that might be `null`',
kind: 'quickfix',
diagnostics: [],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/add-optional-chaining.js': [
{
range: {
start: {
line: 5,
character: 0,
},
end: {
line: 5,
character: 10,
},
},
newText: '(nested?.foo)',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'add_optional_chaining',
'Add optional chaining for object that might be `null`',
],
},
},
],
},
],
['textDocument/publishDiagnostics', ...lspIgnoreStatusAndCancellation],
),
]),
test('provide quickfix for PropMissing errors with dot syntax', [
addFile('prop-missing.js.ignored', 'prop-missing.js'),
lspStartAndConnect(),
lspRequestAndWaitUntilResponse('textDocument/codeAction', {
textDocument: {uri: '<PLACEHOLDER_PROJECT_URL>/prop-missing.js'},
range: {
start: {
line: 3,
character: 2,
},
end: {
line: 3,
character: 9,
},
},
context: {
only: ['quickfix'],
diagnostics: [
{
range: {
start: {
line: 3,
character: 2,
},
end: {
line: 3,
character: 9,
},
},
message:
'Cannot get `x.faceboy` because property `faceboy` (did you mean `facebook`?) is missing in object type [1].',
severity: 1,
code: 'InferError',
source: 'Flow',
},
],
},
}).verifyAllLSPMessagesInStep(
[
{
method: 'textDocument/codeAction',
result: [
{
title: 'Replace `faceboy` with `facebook`',
kind: 'quickfix',
diagnostics: [
{
range: {
start: {
line: 3,
character: 2,
},
end: {
line: 3,
character: 9,
},
},
severity: 1,
code: 'InferError',
source: 'Flow',
message:
'Cannot get `x.faceboy` because property `faceboy` (did you mean `facebook`?) is missing in object type [1].',
relatedInformation: [],
relatedLocations: [],
},
],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/prop-missing.js': [
{
range: {
start: {
line: 3,
character: 2,
},
end: {
line: 3,
character: 9,
},
},
newText: 'facebook',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'replace_prop_typo_at_target',
'Replace `faceboy` with `facebook`',
],
},
},
],
},
],
['textDocument/publishDiagnostics', ...lspIgnoreStatusAndCancellation],
),
]),
test('provide quickfix for PropMissing errors with bracket syntax', [
addFile(
'prop-missing-bracket-syntax.js.ignored',
'prop-missing-bracket-syntax.js',
),
lspStartAndConnect(),
lspRequestAndWaitUntilResponse('textDocument/codeAction', {
textDocument: {
uri: '<PLACEHOLDER_PROJECT_URL>/prop-missing-bracket-syntax.js',
},
range: {
start: {
line: 3,
character: 2,
},
end: {
line: 3,
character: 11,
},
},
context: {
only: ['quickfix'],
diagnostics: [
{
range: {
start: {
line: 3,
character: 2,
},
end: {
line: 3,
character: 11,
},
},
message:
'Cannot get `x.faceboy` because property `faceboy` (did you mean `facebook`?) is missing in object type [1].',
severity: 1,
code: 'InferError',
source: 'Flow',
},
],
},
}).verifyAllLSPMessagesInStep(
[
{
method: 'textDocument/codeAction',
result: [
{
title: 'Replace `faceboy` with `facebook`',
kind: 'quickfix',
diagnostics: [
{
range: {
start: {
line: 3,
character: 2,
},
end: {
line: 3,
character: 11,
},
},
severity: 1,
code: 'InferError',
source: 'Flow',
message:
'Cannot get `x.faceboy` because property `faceboy` (did you mean `facebook`?) is missing in object type [1].',
relatedInformation: [],
relatedLocations: [],
},
],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/prop-missing-bracket-syntax.js': [
{
range: {
start: {
line: 3,
character: 2,
},
end: {
line: 3,
character: 11,
},
},
newText: '"facebook"',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'replace_prop_typo_at_target',
'Replace `faceboy` with `facebook`',
],
},
},
],
},
],
['textDocument/publishDiagnostics', ...lspIgnoreStatusAndCancellation],
),
]),
test('provide quickfix for invalid enum member access errors', [
addFile(
'invalid-enum-member-access.js.ignored',
'invalid-enum-member-access.js',
),
lspStartAndConnect(),
lspRequestAndWaitUntilResponse('textDocument/codeAction', {
textDocument: {
uri: '<PLACEHOLDER_PROJECT_URL>/invalid-enum-member-access.js',
},
range: {
start: {
line: 6,
character: 2,
},
end: {
line: 6,
character: 8,
},
},
context: {
only: ['quickfix'],
diagnostics: [
{
range: {
start: {
line: 6,
character: 2,
},
end: {
line: 6,
character: 8,
},
},
message:
'Cannot access property `Foobat` because `Foobat` is not a member of `enum E`. Did you meanthe member `Foobar`?',
severity: 1,
code: 'InferError',
source: 'Flow',
},
],
},
}).verifyAllLSPMessagesInStep(
[
{
method: 'textDocument/codeAction',
result: [
{
title: 'Replace `Foobat` with `Foobar`',
kind: 'quickfix',
diagnostics: [
{
range: {
start: {
line: 6,
character: 2,
},
end: {
line: 6,
character: 8,
},
},
severity: 1,
code: 'InferError',
source: 'Flow',
message:
'Cannot access property `Foobat` because `Foobat` is not a member of `enum E`. Did you meanthe member `Foobar`?',
relatedInformation: [],
relatedLocations: [],
},
],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/invalid-enum-member-access.js': [
{
range: {
start: {
line: 6,
character: 2,
},
end: {
line: 6,
character: 8,
},
},
newText: 'Foobar',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'replace_enum_prop_typo_at_target',
'Replace `Foobat` with `Foobar`',
],
},
},
],
},
],
['textDocument/publishDiagnostics', ...lspIgnoreStatusAndCancellation],
),
]),
test("don't provide quickfixes for object subtyping errors", [
addFile('object-cast.js.ignored', 'object-cast.js'),
lspStartAndConnect(),
lspRequestAndWaitUntilResponse('textDocument/codeAction', {
textDocument: {uri: '<PLACEHOLDER_PROJECT_URL>/object-cast.js'},
range: {
start: {
line: 3,
character: 1,
},
end: {
line: 3,
character: 14,
},
},
context: {
only: ['quickfix'],
diagnostics: [
{
range: {
start: {
line: 3,
character: 1,
},
end: {
line: 3,
character: 14,
},
},
message:
'Cannot cast object literal to `T` because property `floo` (did you mean `foo`?) is missing in `T` [1] but exists in object literal [2].',
severity: 1,
code: 'InferError',
source: 'Flow',
},
{
range: {
start: {
line: 3,
character: 1,
},
end: {
line: 3,
character: 14,
},
},
message:
'Cannot cast object literal to `T` because property `foo` (did you mean `floo`?) is missing in object literal [1] but exists in `T` [2].',
severity: 1,
code: 'InferError',
source: 'Flow',
},
],
},
}).verifyAllLSPMessagesInStep(
[
{
method: 'textDocument/codeAction',
result: [],
},
],
['textDocument/publishDiagnostics', ...lspIgnoreStatusAndCancellation],
),
]),
test('provide quickfix for parse error', [
addFile('parse-error.js.ignored', 'parse-error.js'),
lspStartAndConnect(),
lspRequestAndWaitUntilResponse('textDocument/codeAction', {
textDocument: {uri: '<PLACEHOLDER_PROJECT_URL>/parse-error.js'},
range: {
start: {
line: 4,
character: 6,
},
end: {
line: 4,
character: 6,
},
},
context: {
only: ['quickfix'],
diagnostics: [
{
range: {
start: {
line: 4,
character: 6,
},
end: {
line: 4,
character: 7,
},
},
message: "Unexpected token `>`. Did you mean `{'>'}`?",
severity: 1,
code: 'ParseError',
relatedInformation: [],
source: 'Flow',
},
],
},
}).verifyAllLSPMessagesInStep(
[
{
method: 'textDocument/codeAction',
result: [
{
title: "Replace `>` with `{'>'}`",
kind: 'quickfix',
diagnostics: [
{
range: {
start: {
line: 4,
character: 6,
},
end: {
line: 4,
character: 7,
},
},
severity: 1,
code: 'ParseError',
source: 'Flow',
message: "Unexpected token `>`. Did you mean `{'>'}`?",
relatedInformation: [],
relatedLocations: [],
},
],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/parse-error.js': [
{
range: {
start: {
line: 4,
character: 6,
},
end: {
line: 4,
character: 7,
},
},
newText: "{'>'}",
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'fix_parse_error',
"Replace `>` with `{'>'}`",
],
},
},
],
},
],
['textDocument/publishDiagnostics', ...lspIgnoreStatusAndCancellation],
),
]),
test('provide quickfix for ClassObject errors', [
addFile('class-object-subtype.js.ignored', 'class-object-subtype.js'),
lspStartAndConnect(),
lspRequestAndWaitUntilResponse('textDocument/codeAction', {
textDocument: {
uri: '<PLACEHOLDER_PROJECT_URL>/class-object-subtype.js',
},
range: {
start: {
line: 8,
character: 4,
},
end: {
line: 8,
character: 11,
},
},
context: {
only: ['quickfix'],
diagnostics: [
{
range: {
start: {
line: 8,
character: 4,
},
end: {
line: 8,
character: 11,
},
},
message:
'Cannot call foo with new A() bound to x because cannot subtype class A [1] with object type [2]. Please use an interface instead.',
severity: 1,
code: 'InferError',
source: 'Flow',
},
],
},
}).verifyAllLSPMessagesInStep(
[
{
method: 'textDocument/codeAction',
result: [
{
title: 'Rewrite object type as an interface',
kind: 'quickfix',
diagnostics: [
{
range: {
start: {
line: 8,
character: 4,
},
end: {
line: 8,
character: 11,
},
},
severity: 1,
code: 'InferError',
source: 'Flow',
message:
'Cannot call foo with new A() bound to x because cannot subtype class A [1] with object type [2]. Please use an interface instead.',
relatedInformation: [],
relatedLocations: [],
},
],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/class-object-subtype.js': [
{
range: {
start: {
line: 6,
character: 17,
},
end: {
line: 6,
character: 35,
},
},
newText: 'interface { x: number }',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'replace_obj_with_interface',
'Rewrite object type as an interface',
],
},
},
],
},
],
[
'textDocument/publishDiagnostics',
'window/showStatus',
'$/cancelRequest',
],
),
]),
test('provide quickfix for nested ClassObject errors', [
addFile('class-object-subtype.js.ignored', 'class-object-subtype.js'),
lspStartAndConnect(),
lspRequestAndWaitUntilResponse('textDocument/codeAction', {
textDocument: {
uri: '<PLACEHOLDER_PROJECT_URL>/class-object-subtype.js',
},
range: {
start: {
line: 12,
character: 9,
},
end: {
line: 12,
character: 16,
},
},
context: {
only: ['quickfix'],
diagnostics: [
{
range: {
start: {
line: 12,
character: 9,
},
end: {
line: 12,
character: 16,
},
},
message:
'Cannot call `bar` with object literal bound to `_` because cannot subtype class `A` [1] with object type [2]. Please use an interface instead in property `i`.',
severity: 1,
code: 'InferError',
source: 'Flow',
},
],
},
}).verifyAllLSPMessagesInStep(
[
{
method: 'textDocument/codeAction',
result: [
{
title: 'Rewrite object type as an interface',
kind: 'quickfix',
diagnostics: [
{
range: {
start: {
line: 12,
character: 9,
},
end: {
line: 12,
character: 16,
},
},
severity: 1,
code: 'InferError',
source: 'Flow',
message:
'Cannot call `bar` with object literal bound to `_` because cannot subtype class `A` [1] with object type [2]. Please use an interface instead in property `i`.',
relatedInformation: [],
relatedLocations: [],
},
],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/class-object-subtype.js': [
{
range: {
start: {
line: 10,
character: 23,
},
end: {
line: 10,
character: 39,
},
},
newText: 'interface { x: number }',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'replace_obj_with_interface',
'Rewrite object type as an interface',
],
},
},
],
},
],
[
'textDocument/publishDiagnostics',
'window/showStatus',
'$/cancelRequest',
],
),
]),
test('provide quickfix for aliased ClassObject errors', [
addFile('class-object-subtype.js.ignored', 'class-object-subtype.js'),
lspStartAndConnect(),
lspRequestAndWaitUntilResponse('textDocument/codeAction', {
textDocument: {
uri: '<PLACEHOLDER_PROJECT_URL>/class-object-subtype.js',
},
range: {
start: {
line: 18,
character: 4,
},
end: {
line: 18,
character: 11,
},
},
context: {
only: ['quickfix'],
diagnostics: [
{
range: {
start: {
line: 18,
character: 4,
},
end: {
line: 18,
character: 4,
},
},
message:
'Cannot call `baz` with object literal bound to `_` because cannot subtype class `A` [1] with object type [2]. Please use an interface instead in property `i`.',
severity: 1,
code: 'InferError',
source: 'Flow',
},
],
},
}).verifyAllLSPMessagesInStep(
[
{
method: 'textDocument/codeAction',
result: [
{
title: 'Rewrite `T` as an interface',
kind: 'quickfix',
diagnostics: [
{
range: {
start: {
line: 18,
character: 4,
},
end: {
line: 18,
character: 4,
},
},
severity: 1,
code: 'InferError',
source: 'Flow',
message:
'Cannot call `baz` with object literal bound to `_` because cannot subtype class `A` [1] with object type [2]. Please use an interface instead in property `i`.',
relatedInformation: [],
relatedLocations: [],
},
],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/class-object-subtype.js': [
{
range: {
start: {
line: 14,
character: 9,
},
end: {
line: 14,
character: 27,
},
},
newText: 'interface { x: number }',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'replace_obj_with_interface',
'Rewrite `T` as an interface',
],
},
},
],
},
],
[
'textDocument/publishDiagnostics',
'window/showStatus',
'$/cancelRequest',
],
),
]),
test('provide codeAction for cross-file ClassObject errors', [
addFile('class-object-subtype.js.ignored', 'class-object-subtype.js'),
addFile('lib.js.ignored', 'lib.js'),
lspStartAndConnect(),
lspRequestAndWaitUntilResponse('textDocument/codeAction', {
textDocument: {
uri: '<PLACEHOLDER_PROJECT_URL>/class-object-subtype.js',
},
range: {
start: {
line: 22,
character: 4,
},
end: {
line: 22,
character: 11,
},
},
context: {
only: ['quickfix'],
diagnostics: [
{
range: {
start: {
line: 22,
character: 4,
},
end: {
line: 22,
character: 11,
},
},
message:
'Cannot call `qux` with object literal bound to `_` because cannot subtype class `A` [1] with object type [2]. Please use an interface instead in property `i`.',
severity: 1,
code: 'InferError',
source: 'Flow',
},
],
},
}).verifyAllLSPMessagesInStep(
[
{
method: 'textDocument/codeAction',
result: [
{
title: 'Rewrite object type as an interface',
kind: 'quickfix',
diagnostics: [
{
range: {
start: {
line: 22,
character: 4,
},
end: {
line: 22,
character: 11,
},
},
severity: 1,
code: 'InferError',
source: 'Flow',
message:
'Cannot call `qux` with object literal bound to `_` because cannot subtype class `A` [1] with object type [2]. Please use an interface instead in property `i`.',
relatedInformation: [],
relatedLocations: [],
},
],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/lib.js': [
{
range: {
start: {
line: 2,
character: 24,
},
end: {
line: 2,
character: 42,
},
},
newText: 'interface { x: number }',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'replace_obj_with_interface',
'Rewrite object type as an interface',
],
},
},
],
},
],
[
'textDocument/publishDiagnostics',
'window/showStatus',
'$/cancelRequest',
],
),
]),
test('provide codeAction for MethodUnbinding errors', [
addFile('method-unbinding.js.ignored', 'method-unbinding.js'),
lspStartAndConnect(),
lspRequestAndWaitUntilResponse('textDocument/codeAction', {
textDocument: {
uri: '<PLACEHOLDER_PROJECT_URL>/method-unbinding.js',
},
range: {
start: {
line: 6,
character: 8,
},
end: {
line: 6,
character: 9,
},
},
context: {
only: ['quickfix'],
diagnostics: [
{
range: {
start: {
line: 6,
character: 8,
},
end: {
line: 6,
character: 9,
},
},
message:
'Cannot get `(new A).f` because property `f` [1] cannot be unbound from the context [2] where it was defined.',
severity: 1,
code: 'InferError',
source: 'Flow',
},
],
},
}).verifyAllLSPMessagesInStep(
[
[
'textDocument/codeAction',
JSON.stringify([
{
title: 'Rewrite function as an arrow function',
kind: 'quickfix',
diagnostics: [
{
range: {
start: {line: 6, character: 8},
end: {line: 6, character: 9},
},
severity: 1,
code: 'InferError',
source: 'Flow',
message:
'Cannot get `(new A).f` because property `f` [1] cannot be unbound from the context [2] where it was defined.',
relatedInformation: [],
relatedLocations: [],
},
],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/method-unbinding.js': [
{
range: {
start: {
line: 2,
character: 0,
},
end: {
line: 4,
character: 1,
},
},
newText:
'class A {\n f = (x: number): number => {\n return x;\n };\n}',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'replace_method_with_arrow',
'Rewrite function as an arrow function',
],
},
},
]),
],
],
['textDocument/publishDiagnostics', ...lspIgnoreStatusAndCancellation],
),
]),
test('ignore method unbinding when super is used', [
addFile('method-unbinding.js.ignored', 'method-unbinding.js'),
lspStartAndConnect(),
lspRequestAndWaitUntilResponse('textDocument/codeAction', {
textDocument: {
uri: '<PLACEHOLDER_PROJECT_URL>/method-unbinding.js',
},
range: {
start: {
line: 6,
character: 8,
},
end: {
line: 6,
character: 9,
},
},
context: {
diagnostics: [
{
range: {
start: {
line: 12,
character: 8,
},
end: {
line: 12,
character: 9,
},
},
message:
'Cannot get `(new B).f` because property `f` [1] cannot be unbound from the context [2] where it was defined.',
severity: 1,
code: 'InferError',
source: 'Flow',
},
],
},
}).verifyAllLSPMessagesInStep(
[['textDocument/codeAction', '[]']],
['textDocument/publishDiagnostics', ...lspIgnoreStatusAndCancellation],
),
]),
test('provide codeAction for basic extract function', [
addFile(
'refactor-extract-function-basic.js.ignored',
'refactor-extract-function-basic.js',
),
lspStartAndConnect(),
// Partial selection is not allowed and gives no results.
lspRequestAndWaitUntilResponse('textDocument/codeAction', {
textDocument: {
uri: '<PLACEHOLDER_PROJECT_URL>/refactor-extract-function-basic.js',
},
range: {
start: {
line: 4,
character: 2,
},
end: {
line: 5,
character: 15,
},
},
context: {
only: ['refactor'],
diagnostics: [],
},
}).verifyAllLSPMessagesInStep(
[
{
method: 'textDocument/codeAction',
result: [],
},
],
['textDocument/publishDiagnostics', ...lspIgnoreStatusAndCancellation],
),
// Full selection is allowed and gives one result.
lspRequestAndWaitUntilResponse('textDocument/codeAction', {
textDocument: {
uri: '<PLACEHOLDER_PROJECT_URL>/refactor-extract-function-basic.js',
},
range: {
start: {
line: 4,
character: 2,
},
end: {
line: 5,
character: 21,
},
},
context: {
only: ['refactor'],
diagnostics: [],
},
}).verifyAllLSPMessagesInStep(
[
{
method: 'textDocument/codeAction',
result: [
{
title: 'Extract to function in module scope',
kind: 'refactor.extract',
diagnostics: [],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/refactor-extract-function-basic.js': [
{
range: {
start: {
line: 4,
character: 2,
},
end: {
line: 5,
character: 21,
},
},
newText: 'newFunction();',
},
{
range: {
start: {
line: 7,
character: 1,
},
end: {
line: 7,
character: 1,
},
},
newText:
'\nfunction newFunction(): void {\n console.log("foo");\n console.log("bar");\n}',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'refactor_extract',
'Extract to function in module scope',
],
},
},
{
title: "Extract to inner function in function 'fooBar'",
kind: 'refactor.extract',
diagnostics: [],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/refactor-extract-function-basic.js': [
{
range: {
start: {
line: 4,
character: 2,
},
end: {
line: 5,
character: 21,
},
},
newText: 'newFunction();',
},
{
range: {
start: {
line: 6,
character: 25,
},
end: {
line: 6,
character: 25,
},
},
newText:
'function newFunction(): void {\n console.log("foo");\n console.log("bar");\n }',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'refactor_extract',
"Extract to inner function in function 'fooBar'",
],
},
},
],
},
],
['textDocument/publishDiagnostics', ...lspIgnoreStatusAndCancellation],
),
]),
test('provide codeAction for statements with comments', [
addFile(
'refactor-extract-with-comments.js.ignored',
'refactor-extract-with-comments.js',
),
lspStartAndConnect(),
lspRequestAndWaitUntilResponse('textDocument/codeAction', {
textDocument: {
uri: '<PLACEHOLDER_PROJECT_URL>/refactor-extract-with-comments.js',
},
range: {
start: {
line: 3,
character: 2,
},
end: {
line: 6,
character: 26,
},
},
context: {
only: ['refactor'],
diagnostics: [],
},
}).verifyAllLSPMessagesInStep(
[
{
method: 'textDocument/codeAction',
result: [
{
title: 'Extract to function in module scope',
kind: 'refactor.extract',
diagnostics: [],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/refactor-extract-with-comments.js': [
{
range: {
start: {line: 3, character: 2},
end: {line: 6, character: 26},
},
newText: 'let {a, barr, fooo} = newFunction();',
},
{
range: {
start: {line: 9, character: 1},
end: {
line: 9,
character: 1,
},
},
newText:
'\nfunction newFunction(): {| a: number, barr: number, fooo: number |} {\n // comment before\n let fooo = 3; // selected\n let barr = 4; // selected\n const a = 3; // selected\n return { a, barr, fooo };\n}',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'refactor_extract',
'Extract to function in module scope',
],
},
},
{
title: "Extract to inner function in function 'i_am_a_test'",
kind: 'refactor.extract',
diagnostics: [],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/refactor-extract-with-comments.js': [
{
range: {
start: {line: 3, character: 2},
end: {line: 6, character: 26},
},
newText: 'let {a, barr, fooo} = newFunction();',
},
{
range: {
start: {line: 8, character: 15},
end: {line: 8, character: 15},
},
newText:
'function newFunction(): {| a: number, barr: number, fooo: number |} {\n // comment before\n let fooo = 3; // selected\n let barr = 4; // selected\n const a = 3; // selected\n return { a, barr, fooo };\n }',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'refactor_extract',
"Extract to inner function in function 'i_am_a_test'",
],
},
},
],
},
],
['textDocument/publishDiagnostics', ...lspIgnoreStatusAndCancellation],
),
]),
test('provide codeAction for extract function with type imports', [
addFile(
'refactor-extract-function-type-provider.js.ignored',
'refactor-extract-function-type-provider.js',
),
addFile(
'refactor-extract-function-import-type.js.ignored',
'refactor-extract-function-import-type.js',
),
lspStartAndConnect(),
lspRequestAndWaitUntilResponse('textDocument/codeAction', {
textDocument: {
uri:
'<PLACEHOLDER_PROJECT_URL>/refactor-extract-function-import-type.js',
},
range: {start: {line: 7, character: 2}, end: {line: 7, character: 19}},
context: {
only: ['refactor'],
diagnostics: [],
},
}).verifyAllLSPMessagesInStep(
[
{
method: 'textDocument/codeAction',
result: [
{
title: 'Extract to function in module scope',
kind: 'refactor.extract',
diagnostics: [],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/refactor-extract-function-import-type.js': [
{
range: {
start: {line: 2, character: 0},
end: {line: 2, character: 0},
},
newText:
'import type { Foo } from "./refactor-extract-function-type-provider";\n\n',
},
{
range: {
start: {line: 7, character: 2},
end: {line: 7, character: 13},
},
newText: '(newFunction)',
},
{
range: {
start: {line: 8, character: 1},
end: {line: 8, character: 1},
},
newText:
'\nfunction newFunction(foo: Foo): void {\n console.log(foo);\n}',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'refactor_extract',
'Extract to function in module scope',
],
},
},
{
title: "Extract to inner function in function 'test'",
kind: 'refactor.extract',
diagnostics: [],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/refactor-extract-function-import-type.js': [
{
range: {
start: {line: 7, character: 2},
end: {line: 7, character: 18},
},
newText: 'newFunction()',
},
{
range: {
start: {line: 7, character: 19},
end: {line: 7, character: 19},
},
newText:
'\nfunction newFunction(): void {\n console.log(foo);\n }',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'refactor_extract',
"Extract to inner function in function 'test'",
],
},
},
],
},
],
['textDocument/publishDiagnostics', ...lspIgnoreStatusAndCancellation],
),
lspRequestAndWaitUntilResponse('textDocument/codeAction', {
textDocument: {
uri:
'<PLACEHOLDER_PROJECT_URL>/refactor-extract-function-import-type.js',
},
range: {start: {line: 6, character: 2}, end: {line: 6, character: 24}},
context: {
only: ['refactor'],
diagnostics: [],
},
}).verifyAllLSPMessagesInStep(
[
{
method: 'textDocument/codeAction',
result: [
{
title: 'Extract to function in module scope',
kind: 'refactor.extract',
diagnostics: [],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/refactor-extract-function-import-type.js': [
{
range: {
start: {line: 2, character: 0},
end: {line: 2, character: 0},
},
newText:
'import type { Foo } from "./refactor-extract-function-type-provider";\n\n',
},
{
range: {
start: {line: 6, character: 14},
end: {line: 6, character: 23},
},
newText: 'newFunction(getFoo2)',
},
{
range: {
start: {line: 8, character: 1},
end: {line: 8, character: 1},
},
newText:
'\nfunction newFunction(getFoo2: () => Foo): Foo {\n const foo = getFoo2();\n return foo;\n}',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'refactor_extract',
'Extract to function in module scope',
],
},
},
{
title: "Extract to inner function in function 'test'",
kind: 'refactor.extract',
diagnostics: [],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/refactor-extract-function-import-type.js': [
{
range: {
start: {line: 2, character: 0},
end: {line: 2, character: 0},
},
newText:
'import type { Foo } from "./refactor-extract-function-type-provider";\n\n',
},
{
range: {
start: {line: 6, character: 14},
end: {line: 6, character: 21},
},
newText: 'newFunction',
},
{
range: {
start: {line: 7, character: 19},
end: {line: 7, character: 19},
},
newText:
'function newFunction(): Foo {\n const foo = getFoo2();\n return foo;\n }',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'refactor_extract',
"Extract to inner function in function 'test'",
],
},
},
],
},
],
['textDocument/publishDiagnostics', ...lspIgnoreStatusAndCancellation],
),
]),
test(
'provide codeAction for basic extract method, constant, class fields.',
[
addFile(
'refactor-extract-method.js.ignored',
'refactor-extract-method.js',
),
lspStartAndConnect(),
// Partial selection is not allowed and gives no results.
lspRequestAndWaitUntilResponse('textDocument/codeAction', {
textDocument: {
uri: '<PLACEHOLDER_PROJECT_URL>/refactor-extract-method.js',
},
range: {
start: {line: 4, character: 4},
end: {line: 4, character: 16},
},
context: {
only: ['refactor'],
diagnostics: [],
},
}).verifyAllLSPMessagesInStep(
[
{
method: 'textDocument/codeAction',
result: [
{
title: "Extract to method in class 'Test'",
kind: 'refactor.extract',
diagnostics: [],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/refactor-extract-method.js': [
{
range: {
start: {line: 2, character: 0},
end: {line: 6, character: 1},
},
newText:
'class Test {\n test(): void {\n this.newMethod();\n }\n newMethod(): void {\n this.test();\n }\n}',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'refactor_extract',
"Extract to method in class 'Test'",
],
},
},
],
},
],
[
'textDocument/publishDiagnostics',
...lspIgnoreStatusAndCancellation,
],
),
lspRequestAndWaitUntilResponse('textDocument/codeAction', {
textDocument: {
uri: '<PLACEHOLDER_PROJECT_URL>/refactor-extract-method.js',
},
range: {
start: {line: 4, character: 4},
end: {line: 4, character: 15},
},
context: {
only: ['refactor'],
diagnostics: [],
},
}).verifyAllLSPMessagesInStep(
[
{
method: 'textDocument/codeAction',
result: [
{
title: "Extract to field in class 'Test'",
kind: 'refactor.extract',
diagnostics: [],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/refactor-extract-method.js': [
{
range: {
start: {line: 2, character: 0},
end: {line: 6, character: 1},
},
newText:
'class Test {\n newProperty = this.test();\n \n test(): void {\n this.newProperty;\n }\n}',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'refactor_extract',
"Extract to field in class 'Test'",
],
},
},
{
title: "Extract to constant in method 'test'",
kind: 'refactor.extract',
diagnostics: [],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/refactor-extract-method.js': [
{
range: {
start: {line: 4, character: 4},
end: {line: 4, character: 16},
},
newText:
'const newLocal = this.test();\n \n newLocal;',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'refactor_extract',
"Extract to constant in method 'test'",
],
},
},
],
},
],
[
'textDocument/publishDiagnostics',
...lspIgnoreStatusAndCancellation,
],
),
],
),
test('provide codeAction for basic extract type alias', [
addFile(
'refactor-extract-type-alias.js.ignored',
'refactor-extract-type-alias.js',
),
lspStartAndConnect(),
lspRequestAndWaitUntilResponse('textDocument/codeAction', {
textDocument: {
uri: '<PLACEHOLDER_PROJECT_URL>/refactor-extract-type-alias.js',
},
range: {
start: {line: 3, character: 11},
end: {line: 3, character: 17},
},
context: {
only: ['refactor'],
diagnostics: [],
},
}).verifyAllLSPMessagesInStep(
[
{
method: 'textDocument/codeAction',
result: [
{
title: 'Extract to type alias',
kind: 'refactor.extract',
diagnostics: [],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/refactor-extract-type-alias.js': [
{
range: {
start: {
line: 3,
character: 2,
},
end: {
line: 3,
character: 22,
},
},
newText:
'type NewType = number;\n \n const a: NewType = 3;',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'refactor_extract',
'Extract to type alias',
],
},
},
],
},
],
['textDocument/publishDiagnostics', ...lspIgnoreStatusAndCancellation],
),
]),
test('obey context.only', [
addFile('only-filter.js.ignored', 'only-filter.js'),
lspStartAndConnect(),
// no context.only gets back a quickfix and refactor
lspRequestAndWaitUntilResponse('textDocument/codeAction', {
textDocument: {
uri: '<PLACEHOLDER_PROJECT_URL>/only-filter.js',
},
range: {
start: {
line: 3,
character: 0,
},
end: {
line: 3,
character: 7,
},
},
context: {
diagnostics: [],
},
}).verifyAllLSPMessagesInStep(
[
{
method: 'textDocument/codeAction',
result: [
{
title: 'Organize imports',
kind: 'source.organizeImports.flow',
diagnostics: [],
command: {
title: '',
command:
'source.organizeImports:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
{
uri: '<PLACEHOLDER_PROJECT_URL>/only-filter.js',
},
],
},
},
{
title: 'Extract to constant in module scope',
kind: 'refactor.extract',
diagnostics: [],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/only-filter.js': [
{
range: {
start: {
line: 3,
character: 0,
},
end: {
line: 3,
character: 8,
},
},
newText: 'const newLocal = foo.bar;\n\nnewLocal;',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'refactor_extract',
'Extract to constant in module scope',
],
},
},
{
title:
'Add optional chaining for object that might be `undefined`',
kind: 'quickfix',
diagnostics: [],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/only-filter.js': [
{
range: {
start: {
line: 3,
character: 0,
},
end: {
line: 3,
character: 7,
},
},
newText: 'foo?.bar',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'add_optional_chaining',
'Add optional chaining for object that might be `undefined`',
],
},
},
],
},
],
['textDocument/publishDiagnostics', ...lspIgnoreStatusAndCancellation],
),
// context.only: ["refactor"] only gets the refactor
lspRequestAndWaitUntilResponse('textDocument/codeAction', {
textDocument: {
uri: '<PLACEHOLDER_PROJECT_URL>/only-filter.js',
},
range: {
start: {
line: 3,
character: 0,
},
end: {
line: 3,
character: 7,
},
},
context: {
diagnostics: [],
only: ['refactor'],
},
}).verifyAllLSPMessagesInStep(
[
{
method: 'textDocument/codeAction',
result: [
{
title: 'Extract to constant in module scope',
kind: 'refactor.extract',
diagnostics: [],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/only-filter.js': [
{
range: {
start: {
line: 3,
character: 0,
},
end: {
line: 3,
character: 8,
},
},
newText: 'const newLocal = foo.bar;\n\nnewLocal;',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'refactor_extract',
'Extract to constant in module scope',
],
},
},
],
},
],
['textDocument/publishDiagnostics', ...lspIgnoreStatusAndCancellation],
),
// context.only: ["quickfix"] only gets the quickfix
lspRequestAndWaitUntilResponse('textDocument/codeAction', {
textDocument: {
uri: '<PLACEHOLDER_PROJECT_URL>/only-filter.js',
},
range: {
start: {
line: 3,
character: 0,
},
end: {
line: 3,
character: 7,
},
},
context: {
diagnostics: [],
only: ['quickfix'],
},
}).verifyAllLSPMessagesInStep(
[
{
method: 'textDocument/codeAction',
result: [
{
title:
'Add optional chaining for object that might be `undefined`',
kind: 'quickfix',
diagnostics: [],
edit: {
changes: {
'<PLACEHOLDER_PROJECT_URL>/only-filter.js': [
{
range: {
start: {
line: 3,
character: 0,
},
end: {
line: 3,
character: 7,
},
},
newText: 'foo?.bar',
},
],
},
},
command: {
title: '',
command: 'log:org.flow:<PLACEHOLDER_PROJECT_URL>',
arguments: [
'textDocument/codeAction',
'add_optional_chaining',
'Add optional chaining for object that might be `undefined`',
],
},
},
],
},
],
['textDocument/publishDiagnostics', ...lspIgnoreStatusAndCancellation],
),
]),
test('skip non-@flow files', [
addFile('not_flow.js.ignored', 'not_flow.js'),
lspStartAndConnect(),
lspRequestAndWaitUntilResponse('textDocument/codeAction', {
textDocument: {
uri: '<PLACEHOLDER_PROJECT_URL>/not_flow.js',
},
range: {
start: {
line: 1,
character: 2,
},
end: {
line: 1,
character: 6,
},
},
context: {
diagnostics: [],
},
}).verifyAllLSPMessagesInStep(
[
{
method: 'textDocument/codeAction',
result: [],
},
],
['textDocument/publishDiagnostics', ...lspIgnoreStatusAndCancellation],
),
]),
],
): Suite);