From cad55bc299ea1ee780ef8edae4fa8343b74c1346 Mon Sep 17 00:00:00 2001 From: Remy Suen Date: Wed, 23 Aug 2017 09:29:57 +0900 Subject: [PATCH] WIP #162 Implement signature help for instructions Signature help for instruction arguments have been added for: - ARG - SHELL - STOPSIGNAL - USER - WORKDIR Signed-off-by: Remy Suen --- CHANGELOG.md | 15 +- src/dockerPlainText.ts | 44 ++- src/dockerSignatures.ts | 275 +++++++++++++++++- src/parser/dockerfileParser.ts | 3 + src/parser/instructions/onbuild.ts | 2 +- src/parser/instructions/shell.ts | 181 ++++++++++++ src/server.ts | 6 +- test/dockerSignatures.tests.ts | 440 ++++++++++++++++++++++++++++- 8 files changed, 954 insertions(+), 12 deletions(-) create mode 100644 src/parser/instructions/shell.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 79fe2e7..6fbef8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,10 +14,17 @@ All notable changes to this project will be documented in this file. - warn if ENV or LABEL is missing closing quote ([#143](https://github.com/rcjsuen/dockerfile-language-server-nodejs/issues/143)) - warn if FROM's build stage name is invalid ([#132](https://github.com/rcjsuen/dockerfile-language-server-nodejs/issues/132)) - warn if an invalid unit of time is used in a duration flag ([#152](https://github.com/rcjsuen/dockerfile-language-server-nodejs/issues/152)) -- textDocument/signatureHelp ([#147](https://github.com/rcjsuen/dockerfile-language-server-nodejs/issues/147)) - - escape parser directive - - COPY's --from flag - - HEALTHCHECK CMD flags +- textDocument/signatureHelp + - escape parser directive ([#147](https://github.com/rcjsuen/dockerfile-language-server-nodejs/issues/147)) + - instruction flags ([#147](https://github.com/rcjsuen/dockerfile-language-server-nodejs/issues/147)) + - COPY's --from + - HEALTHCHECK CMD flags + - instructions ([#162](https://github.com/rcjsuen/dockerfile-language-server-nodejs/issues/162)) + - ARG + - SHELL + - STOPSIGNAL + - USER + - WORKDIR ### Fixed - correct handling of escaped quotes in ENV variables ([#144](https://github.com/rcjsuen/dockerfile-language-server-nodejs/issues/144)) diff --git a/src/dockerPlainText.ts b/src/dockerPlainText.ts index a5f1098..32e2c7c 100644 --- a/src/dockerPlainText.ts +++ b/src/dockerPlainText.ts @@ -37,9 +37,28 @@ export class PlainTextDocumentation { "signatureEscape": "Sets this Dockerfile's escape character. If unspecified, the default escape character is `\\`.", "signatureEscape_Param": "The character to use to escape characters and newlines in this Dockerfile.", + "signatureArg_Signature0": "Define a variable that users can pass a value to at build-time with `docker build`.", + "signatureArg_Signature0_Param": "The name of the variable.", + "signatureArg_Signature1": "Define a variable with an optional default value that users can override at build-time with `docker build`.", + "signatureArg_Signature1_Param1": "The default value of the variable.", "signatureCopyFlagFrom": "Set the build stage to use as the source location of this copy instruction instead of the build's context.", "signatureCopyFlagFrom_Param": "The build stage or image name to use as the source. Also may be a numeric index.", "signatureHealthcheck": "Define how Docker should test the container to check that it is still working.", + "signatureShell": "Override default shell used for the shell form of commands.", + "signatureShell_Param1": "The shell executable to use.", + "signatureShell_Param2": "The parameters to the shell executable.", + "signatureStopsignal": "Set the system call signal to use to send to the container to exit.", + "signatureStopsignal_Param": "The signal to send to the container to exit. This may be an valid unsigned number or a signal name in the SIGNAME format such as SIGKILL.", + "signatureUser_Signature0": "Set the user name to use for running any RUN, CMD, and ENTRYPOINT instructions that follow.", + "signatureUser_Signature0_Param": "The user name to use.", + "signatureUser_Signature1": "Set the user name and user group to use for running any RUN, CMD, and ENTRYPOINT instructions that follow.", + "signatureUser_Signature1_Param1": "The group name to use.", + "signatureUser_Signature2": "Set the UID to use for running any RUN, CMD, and ENTRYPOINT instructions that follow.", + "signatureUser_Signature2_Param": "The UID to use.", + "signatureUser_Signature3": "Set the UID and GID to use for running any RUN, CMD, and ENTRYPOINT instructions that follow.", + "signatureUser_Signature3_Param1": "The GID to use.", + "signatureWorkdir": "Set the working directory for any ADD, COPY, CMD, ENTRYPOINT, or RUN instructions that follow.", + "signatureWorkdir_Param": "The absolute or relative path to use as the working directory. Will be created if it does not exist.", "proposalArgNameOnly": "Define a variable that users can set at build-time when using `docker build`.\n\n", "proposalArgDefaultValue": "Define a variable with the given default value that users can override at build-time when using `docker build`.\n\n", @@ -165,13 +184,36 @@ export class PlainTextDocumentation { signatureEscape: this.dockerMessages["signatureEscape"], signatureEscape_Param: this.dockerMessages["signatureEscape_Param"], + signatureArg_Signature0: this.dockerMessages["signatureArg_Signature0"], + signatureArg_Signature0_Param: this.dockerMessages["signatureArg_Signature0_Param"], + signatureArg_Signature1: this.dockerMessages["signatureArg_Signature1"], + signatureArg_Signature1_Param0: this.dockerMessages["signatureArg_Signature0_Param"], + signatureArg_Signature1_Param1: this.dockerMessages["signatureArg_Signature1_Param1"], signatureCopyFlagFrom: this.dockerMessages["signatureCopyFlagFrom"], signatureCopyFlagFrom_Param: this.dockerMessages["signatureCopyFlagFrom_Param"], signatureHealthcheck: this.dockerMessages["signatureHealthcheck"], signatureHealthcheckFlagInterval_Param: this.dockerMessages["hoverHealthcheckFlagInterval"], signatureHealthcheckFlagRetries_Param: this.dockerMessages["hoverHealthcheckFlagRetries"], signatureHealthcheckFlagStartPeriod_Param: this.dockerMessages["hoverHealthcheckFlagStartPeriod"], - signatureHealthcheckFlagTimeout_Param: this.dockerMessages["hoverHealthcheckFlagTimeout"] + signatureHealthcheckFlagTimeout_Param: this.dockerMessages["hoverHealthcheckFlagTimeout"], + signatureShell: this.dockerMessages["signatureShell"], + signatureShell_Param1: this.dockerMessages["signatureShell_Param1"], + signatureShell_Param2: this.dockerMessages["signatureShell_Param2"], + signatureShell_Param3: this.dockerMessages["signatureShell_Param2"], + signatureStopsignal: this.dockerMessages["signatureStopsignal"], + signatureStopsignal_Param: this.dockerMessages["signatureStopsignal_Param"], + signatureUser_Signature0: this.dockerMessages["signatureUser_Signature0"], + signatureUser_Signature0_Param: this.dockerMessages["signatureUser_Signature0_Param"], + signatureUser_Signature1: this.dockerMessages["signatureUser_Signature1"], + signatureUser_Signature1_Param0: this.dockerMessages["signatureUser_Signature0"], + signatureUser_Signature1_Param1: this.dockerMessages["signatureUser_Signature1_Param1"], + signatureUser_Signature2: this.dockerMessages["signatureUser_Signature2"], + signatureUser_Signature2_Param: this.dockerMessages["signatureUser_Signature2_Param"], + signatureUser_Signature3: this.dockerMessages["signatureUser_Signature3"], + signatureUser_Signature3_Param0: this.dockerMessages["signatureUser_Signature2_Param"], + signatureUser_Signature3_Param1: this.dockerMessages["signatureUser_Signature3_Param1"], + signatureWorkdir: this.dockerMessages["signatureWorkdir"], + signatureWorkdir_Param: this.dockerMessages["signatureWorkdir_Param"] }; } diff --git a/src/dockerSignatures.ts b/src/dockerSignatures.ts index 0b2f948..f8f0555 100644 --- a/src/dockerSignatures.ts +++ b/src/dockerSignatures.ts @@ -3,12 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ import { - TextDocument, Position, SignatureHelp + TextDocument, Range, Position, SignatureHelp } from 'vscode-languageserver'; import { Dockerfile } from './parser/dockerfile'; import { Instruction } from './parser/instruction'; +import { Arg } from './parser/instructions/arg'; import { Copy } from './parser/instructions/copy'; import { Healthcheck } from './parser/instructions/healthcheck'; +import { Shell } from './parser/instructions/shell'; import { PlainTextDocumentation } from './dockerPlainText'; import { DockerfileParser } from './parser/dockerfileParser'; import { Util, DIRECTIVE_ESCAPE } from './docker'; @@ -43,9 +45,9 @@ export class DockerSignatures { } } - let signatureHelp = this.getInstructionSignatures(dockerfile.getInstructions(), position); + let signatureHelp = this.getInstructionSignatures(document, dockerfile.getInstructions(), position); if (!signatureHelp) { - signatureHelp = this.getInstructionSignatures(dockerfile.getOnbuildTriggers(), position); + signatureHelp = this.getInstructionSignatures(document, dockerfile.getOnbuildTriggers(), position); if (!signatureHelp) { signatureHelp = { signatures: [], @@ -58,9 +60,76 @@ export class DockerSignatures { return signatureHelp; } - private getInstructionSignatures(instructions: Instruction[], position: Position): SignatureHelp { + private getInstructionSignatures(document: TextDocument, instructions: Instruction[], position: Position): SignatureHelp { for (let instruction of instructions) { + if (!Util.isInsideRange(position, instruction.getRange())) { + continue; + } else if (Util.isInsideRange(position, instruction.getInstructionRange())) { + return null; + } + switch (instruction.getKeyword()) { + case "ARG": + let argSignatureHelp: SignatureHelp = { + signatures: [ + { + label: "ARG name", + documentation: this.documentation.getDocumentation("signatureArg_Signature0"), + parameters: [ + { + label: "name", + documentation: this.documentation.getDocumentation("signatureArg_Signature0_Param") + } + ] + }, + { + label: "ARG name=defaultValue", + documentation: this.documentation.getDocumentation("signatureArg_Signature1"), + parameters: [ + { + label: "name", + documentation: this.documentation.getDocumentation("signatureArg_Signature1_Param0") + }, + { + label: "defaultValue", + documentation: this.documentation.getDocumentation("signatureArg_Signature1_Param1") + } + ] + } + ], + activeSignature: 0, + activeParameter: 0 + }; + + let content = instruction.getTextContent(); + let index = content.indexOf('='); + if (index !== -1) { + argSignatureHelp = { + signatures: [ + { + label: "ARG name=defaultValue", + documentation: this.documentation.getDocumentation("signatureArg_Signature1"), + parameters: [ + { + label: "name", + documentation: this.documentation.getDocumentation("signatureArg_Signature1_Param0") + }, + { + label: "defaultValue", + documentation: this.documentation.getDocumentation("signatureArg_Signature1_Param1") + } + ] + } + ], + activeSignature: 0, + activeParameter: 0 + }; + + if (document.offsetAt(position) > document.offsetAt(instruction.getRange().start) + index) { + argSignatureHelp.activeParameter = 1; + } + } + return argSignatureHelp; case "COPY": let flag = (instruction as Copy).getFromFlag(); if (flag !== null) { @@ -163,6 +232,204 @@ export class DockerSignatures { } } break; + case "SHELL": + let shell = instruction as Shell; + let shellSignatureHelp: SignatureHelp = { + signatures: [ + { + label: "SHELL [ \"executable\", \"parameter\", ... ]", + documentation: this.documentation.getDocumentation("signatureShell"), + parameters: [ + { + label: "[" + }, + { + label: "\"executable\"", + documentation: this.documentation.getDocumentation("signatureShell_Param1") + }, + { + label: "\"parameter\"", + documentation: this.documentation.getDocumentation("signatureShell_Param2") + }, + { + label: "...", + documentation: this.documentation.getDocumentation("signatureShell_Param3") + }, + { + label: "]" + } + ] + } + ], + activeSignature: 0, + activeParameter: null + }; + + const closingBracket = shell.getClosingBracket(); + if (closingBracket) { + let range = closingBracket.getRange(); + if (range.end.line === position.line && range.end.character === position.character) { + shellSignatureHelp.activeParameter = 4; + return shellSignatureHelp; + } else if (closingBracket.isBefore(position)) { + return null; + } + } + + const parameter = shell.getParameter(); + if (parameter && parameter.isBefore(position)) { + shellSignatureHelp.activeParameter = 3; + return shellSignatureHelp; + } + + const executable = shell.getExecutable(); + if (executable && executable.isBefore(position)) { + shellSignatureHelp.activeParameter = 2; + return shellSignatureHelp; + } + + const openingBracket = shell.getOpeningBracket(); + if (openingBracket) { + let range = openingBracket.getRange(); + if ((range.end.line === position.line && range.end.character === position.character) || openingBracket.isBefore(position)) { + shellSignatureHelp.activeParameter = 1; + return shellSignatureHelp; + } + } + + shellSignatureHelp.activeParameter = 0; + return shellSignatureHelp; + case "STOPSIGNAL": + return { + signatures: [ + { + label: "STOPSIGNAL signal", + documentation: this.documentation.getDocumentation("signatureStopsignal"), + parameters: [ + { + label: "signal", + documentation: this.documentation.getDocumentation("signatureStopsignal_Param") + } + ] + } + ], + activeSignature: 0, + activeParameter: 0 + }; + case "USER": + let userSignatureHelp = { + signatures: [ + { + label: "USER user", + documentation: this.documentation.getDocumentation("signatureUser_Signature0"), + parameters: [ + { + label: "user", + documentation: this.documentation.getDocumentation("signatureUser_Signature0_Param") + } + ] + }, + { + label: "USER user:group", + documentation: this.documentation.getDocumentation("signatureUser_Signature1"), + parameters: [ + { + label: "user", + documentation: this.documentation.getDocumentation("signatureUser_Signature1_Param0") + }, + { + label: "group", + documentation: this.documentation.getDocumentation("signatureUser_Signature1_Param1") + } + ] + }, + { + label: "USER uid", + documentation: this.documentation.getDocumentation("signatureUser_Signature2"), + parameters: [ + { + label: "uid", + documentation: this.documentation.getDocumentation("signatureUser_Signature2_Param") + } + ] + }, + { + label: "USER uid:gid", + documentation: this.documentation.getDocumentation("signatureUser_Signature3"), + parameters: [ + { + label: "uid", + documentation: this.documentation.getDocumentation("signatureUser_Signature3_Param0") + }, + { + label: "gid", + documentation: this.documentation.getDocumentation("signatureUser_Signature3_Param1") + } + ] + } + ], + activeSignature: 0, + activeParameter: 0 + }; + let userSeparatorIndex = instruction.getTextContent().indexOf(":"); + if (userSeparatorIndex !== -1) { + userSignatureHelp = { + signatures: [ + { + label: "USER user:group", + documentation: this.documentation.getDocumentation("signatureUser_Signature1"), + parameters: [ + { + label: "user", + documentation: this.documentation.getDocumentation("signatureUser_Signature1_Param0") + }, + { + label: "group", + documentation: this.documentation.getDocumentation("signatureUser_Signature1_Param1") + } + ] + }, + { + label: "USER uid:gid", + documentation: this.documentation.getDocumentation("signatureUser_Signature3"), + parameters: [ + { + label: "uid", + documentation: this.documentation.getDocumentation("signatureUser_Signature3_Param0") + }, + { + label: "gid", + documentation: this.documentation.getDocumentation("signatureUser_Signature3_Param1") + } + ] + } + ], + activeSignature: 0, + activeParameter: 0 + }; + + if (document.offsetAt(position) > document.offsetAt(instruction.getRange().start) + userSeparatorIndex) { + userSignatureHelp.activeParameter = 1; + } + } + return userSignatureHelp; + case "WORKDIR": + return { + signatures: [ + { + label: "WORKDIR /the/workdir/path", + documentation: this.documentation.getDocumentation("signatureWorkdir"), + parameters: [ + { + label: "/the/workdir/path", + documentation: this.documentation.getDocumentation("signatureWorkdir_Param") + } + ] + } + ], + activeSignature: 0, + activeParameter: 0 + }; } } return null; diff --git a/src/parser/dockerfileParser.ts b/src/parser/dockerfileParser.ts index d31dc61..d33a50b 100644 --- a/src/parser/dockerfileParser.ts +++ b/src/parser/dockerfileParser.ts @@ -18,6 +18,7 @@ import { From } from './instructions/from'; import { Healthcheck } from './instructions/healthcheck'; import { Label } from './instructions/label'; import { Onbuild } from './instructions/onbuild'; +import { Shell } from './instructions/shell'; import { StopSignal } from './instructions/stopSignal'; import { Workdir } from './instructions/workdir'; import { User } from './instructions/user'; @@ -48,6 +49,8 @@ export class DockerfileParser { return new Label(document, lineRange, escapeChar, instruction, instructionRange); case "ONBUILD": return new Onbuild(document, lineRange, escapeChar, instruction, instructionRange); + case "SHELL": + return new Shell(document, lineRange, escapeChar, instruction, instructionRange); case "STOPSIGNAL": return new StopSignal(document, lineRange, escapeChar, instruction, instructionRange); case "WORKDIR": diff --git a/src/parser/instructions/onbuild.ts b/src/parser/instructions/onbuild.ts index aafc443..7b0b104 100644 --- a/src/parser/instructions/onbuild.ts +++ b/src/parser/instructions/onbuild.ts @@ -35,7 +35,7 @@ export class Onbuild extends Instruction { return DockerfileParser.createInstruction( this.document, this.escapeChar, - Range.create(args[0].getRange().start, args[args.length - 1].getRange().end), + Range.create(args[0].getRange().start, this.getRange().end), this.getTriggerWord(), triggerRange); } diff --git a/src/parser/instructions/shell.ts b/src/parser/instructions/shell.ts new file mode 100644 index 0000000..95d81ed --- /dev/null +++ b/src/parser/instructions/shell.ts @@ -0,0 +1,181 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Remy Suen. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +import { TextDocument, Range } from 'vscode-languageserver'; +import { Argument } from '../argument'; +import { Instruction } from '../instruction'; + +export class Shell extends Instruction { + + private readonly openingBracket: Argument; + private readonly closingBracket: Argument; + private readonly executable: Argument; + private readonly parameter: Argument; + + constructor(document: TextDocument, range: Range, escapeChar: string, instruction: string, instructionRange: Range) { + super(document, range, escapeChar, instruction, instructionRange); + + let argsContent = this.getArgumentsContent(); + if (argsContent === null) { + return; + } + + let args = this.getArguments(); + if (args.length === 1 && args[0].getValue() === "[]") { + let argRange = args[0].getRange(); + this.openingBracket = new Argument("[", Range.create(argRange.start.line, argRange.start.character, argRange.start.line, argRange.start.character + 1)); + this.closingBracket = new Argument("]", Range.create(argRange.start.line, argRange.start.character + 1, argRange.end.line, argRange.end.character)); + return; + } else if (args.length === 2 && args[0].getValue() === '[' && args[1].getValue() === ']') { + this.openingBracket = args[0]; + this.closingBracket = args[1]; + return; + } + + let fullStart = -1; + let rangeStart = -1; + let rangeEnd = -1; + let argsRange = this.getArgumentsRange(); + let argsOffset = document.offsetAt(argsRange.start); + let last = ""; + let quoted = false; + argsCheck: for (let i = 0; i < argsContent.length; i++) { + switch (argsContent.charAt(i)) { + case '[': + if (last === "") { + this.openingBracket = new Argument( + "[", Range.create(document.positionAt(argsOffset + i), document.positionAt(argsOffset + i + 1)) + ); + last = '['; + fullStart = i + 1; + } else if (!quoted) { + break argsCheck; + } + break; + case '"': + if (last === '[' || last === ',') { + quoted = true; + last = '"'; + continue; + } else if (last === '"') { + if (quoted) { + rangeEnd = i + 1; + // quoted string done + quoted = false; + } else { + // should be a , or a ] + break argsCheck; + } + } else { + break argsCheck; + } + break; + case ',': + if (!quoted) { + if (this.executable) { + if (!this.parameter) { + this.parameter = new Argument( + argsContent.substring(fullStart, i), + Range.create(document.positionAt(argsOffset + fullStart), document.positionAt(argsOffset + i)) + ); + } + } else { + this.executable = new Argument( + argsContent.substring(fullStart, i), + Range.create(document.positionAt(argsOffset + fullStart), document.positionAt(argsOffset + i)) + ); + } + fullStart = i + 1; + if (last === '"') { + last = ',' + } else { + break argsCheck; + } + } + break; + case ']': + if (!quoted && last !== "") { + this.closingBracket = new Argument( + "]", Range.create(document.positionAt(argsOffset + i), document.positionAt(argsOffset + i + 1)) + ); + break argsCheck; + } + break; + case ' ': + case '\t': + break; + case '\\': + if (quoted) { + switch (argsContent.charAt(i + 1)) { + case '"': + case '\\': + i++; + continue; + case ' ': + case '\t': + for (let j = i + 2; j < argsContent.length; j++) { + switch (argsContent.charAt(j)) { + case '\r': + if (argsContent.charAt(j + 1) === '\n') { + j++; + } + case '\n': + i = j; + continue argsCheck; + case ' ': + case '\t': + break; + default: + break argsCheck; + } + } + break; + default: + i++; + continue; + } + } else { + for (let j = i + 1; j < argsContent.length; j++) { + switch (argsContent.charAt(j)) { + case '\r': + if (argsContent.charAt(j + 1) === '\n') { + j++; + } + case '\n': + i = j; + continue argsCheck; + case ' ': + case '\t': + break; + default: + break argsCheck; + } + } + } + break; + default: + if (!quoted) { + break argsCheck; + } + break; + } + } + } + + public getOpeningBracket(): Argument | null { + return this.openingBracket; + } + + public getExecutable(): Argument | null { + return this.executable; + } + + public getParameter(): Argument | null { + return this.parameter; + } + + public getClosingBracket(): Argument | null { + return this.closingBracket; + } +} diff --git a/src/server.ts b/src/server.ts index 2206d03..fe8f4d9 100644 --- a/src/server.ts +++ b/src/server.ts @@ -92,7 +92,11 @@ connection.onInitialize((params): InitializeResult => { definitionProvider: true, signatureHelpProvider: { triggerCharacters: [ - "=" + '-', + '[', + ',', + ' ', + '=' ] } } diff --git a/test/dockerSignatures.tests.ts b/test/dockerSignatures.tests.ts index 50e0ec8..10e9137 100644 --- a/test/dockerSignatures.tests.ts +++ b/test/dockerSignatures.tests.ts @@ -107,6 +107,167 @@ function assertHealthcheck_FlagTimeout(signatureHelp: SignatureHelp) { assert.equal(signatureHelp.signatures[0].parameters[0].documentation, docs.getDocumentation("signatureHealthcheckFlagTimeout_Param")); } +function assertArg_Name(signatureHelp: SignatureHelp) { + assert.equal(signatureHelp.activeSignature, 0); + assert.equal(signatureHelp.activeParameter, 0); + assert.equal(signatureHelp.signatures.length, 2); + + assert.equal(signatureHelp.signatures[0].label, "ARG name"); + assert.notEqual(signatureHelp.signatures[0].documentation, null); + assert.equal(signatureHelp.signatures[0].documentation, docs.getDocumentation("signatureArg_Signature0")); + assert.equal(signatureHelp.signatures[0].parameters.length, 1); + assert.equal(signatureHelp.signatures[0].parameters[0].label, "name"); + assert.notEqual(signatureHelp.signatures[0].parameters[0].documentation, null); + assert.equal(signatureHelp.signatures[0].parameters[0].documentation, docs.getDocumentation("signatureArg_Signature0_Param")); + + assert.equal(signatureHelp.signatures[1].label, "ARG name=defaultValue"); + assert.notEqual(signatureHelp.signatures[1].documentation, null); + assert.equal(signatureHelp.signatures[1].documentation, docs.getDocumentation("signatureArg_Signature1")); + assert.equal(signatureHelp.signatures[1].parameters.length, 2); + assert.equal(signatureHelp.signatures[1].parameters[0].label, "name"); + assert.notEqual(signatureHelp.signatures[1].parameters[0].documentation, null); + assert.equal(signatureHelp.signatures[1].parameters[0].documentation, docs.getDocumentation("signatureArg_Signature1_Param0")); + assert.equal(signatureHelp.signatures[1].parameters[1].label, "defaultValue"); + assert.notEqual(signatureHelp.signatures[1].parameters[1].documentation, null); + assert.equal(signatureHelp.signatures[1].parameters[1].documentation, docs.getDocumentation("signatureArg_Signature1_Param1")); +} + +function assertArg_NameDefaultValue(signatureHelp: SignatureHelp, activeParameter: number) { + assert.equal(signatureHelp.activeSignature, 0); + assert.equal(signatureHelp.activeParameter, activeParameter); + assert.equal(signatureHelp.signatures[0].label, "ARG name=defaultValue"); + assert.notEqual(signatureHelp.signatures[0].documentation, null); + assert.equal(signatureHelp.signatures[0].documentation, docs.getDocumentation("signatureArg_Signature1")); + assert.equal(signatureHelp.signatures[0].parameters.length, 2); + assert.equal(signatureHelp.signatures[0].parameters[0].label, "name"); + assert.notEqual(signatureHelp.signatures[0].parameters[0].documentation, null); + assert.equal(signatureHelp.signatures[0].parameters[0].documentation, docs.getDocumentation("signatureArg_Signature1_Param0")); + assert.equal(signatureHelp.signatures[0].parameters[1].label, "defaultValue"); + assert.notEqual(signatureHelp.signatures[0].parameters[1].documentation, null); + assert.equal(signatureHelp.signatures[0].parameters[1].documentation, docs.getDocumentation("signatureArg_Signature1_Param1")); +} + +function assertShell(signatureHelp: SignatureHelp, activeParameter: number) { + assert.equal(signatureHelp.activeSignature, 0); + assert.equal(signatureHelp.activeParameter, activeParameter); + assert.equal(signatureHelp.signatures.length, 1); + assert.equal(signatureHelp.signatures[0].label, "SHELL [ \"executable\", \"parameter\", ... ]"); + assert.notEqual(signatureHelp.signatures[0].documentation, null); + assert.equal(signatureHelp.signatures[0].documentation, docs.getDocumentation("signatureShell")); + assert.equal(signatureHelp.signatures[0].parameters.length, 5); + assert.equal(signatureHelp.signatures[0].parameters[0].label, "["); + assert.equal(signatureHelp.signatures[0].parameters[0].documentation, null); + assert.equal(signatureHelp.signatures[0].parameters[1].label, "\"executable\""); + assert.notEqual(signatureHelp.signatures[0].parameters[1].documentation, null); + assert.equal(signatureHelp.signatures[0].parameters[1].documentation, docs.getDocumentation("signatureShell_Param1")); + assert.equal(signatureHelp.signatures[0].parameters[2].label, "\"parameter\""); + assert.notEqual(signatureHelp.signatures[0].parameters[2].documentation, null); + assert.equal(signatureHelp.signatures[0].parameters[2].documentation, docs.getDocumentation("signatureShell_Param2")); + assert.equal(signatureHelp.signatures[0].parameters[3].label, "..."); + assert.notEqual(signatureHelp.signatures[0].parameters[3].documentation, null); + assert.equal(signatureHelp.signatures[0].parameters[3].documentation, docs.getDocumentation("signatureShell_Param3")); + assert.equal(signatureHelp.signatures[0].parameters[4].label, "]"); + assert.equal(signatureHelp.signatures[0].parameters[4].documentation, null); +} + +function assertStopsignal(signatureHelp: SignatureHelp) { + assert.equal(signatureHelp.activeSignature, 0); + assert.equal(signatureHelp.activeParameter, 0); + assert.equal(signatureHelp.signatures.length, 1); + assert.equal(signatureHelp.signatures[0].label, "STOPSIGNAL signal"); + assert.notEqual(signatureHelp.signatures[0].documentation, null); + assert.equal(signatureHelp.signatures[0].documentation, docs.getDocumentation("signatureStopsignal")); + assert.equal(signatureHelp.signatures[0].parameters.length, 1); + assert.equal(signatureHelp.signatures[0].parameters[0].label, "signal"); + assert.notEqual(signatureHelp.signatures[0].parameters[0].documentation, null); + assert.equal(signatureHelp.signatures[0].parameters[0].documentation, docs.getDocumentation("signatureStopsignal_Param")); +} + +function assertUser_All(signatureHelp: SignatureHelp) { + assert.equal(signatureHelp.activeSignature, 0); + assert.equal(signatureHelp.activeParameter, 0); + assert.equal(signatureHelp.signatures.length, 4); + + assert.equal(signatureHelp.signatures[0].label, "USER user"); + assert.notEqual(signatureHelp.signatures[0].documentation, null); + assert.equal(signatureHelp.signatures[0].documentation, docs.getDocumentation("signatureUser_Signature0")); + assert.equal(signatureHelp.signatures[0].parameters.length, 1); + assert.equal(signatureHelp.signatures[0].parameters[0].label, "user"); + assert.notEqual(signatureHelp.signatures[0].parameters[0].documentation, null); + assert.equal(signatureHelp.signatures[0].parameters[0].documentation, docs.getDocumentation("signatureUser_Signature0_Param")); + + assert.equal(signatureHelp.signatures[1].label, "USER user:group"); + assert.notEqual(signatureHelp.signatures[1].documentation, null); + assert.equal(signatureHelp.signatures[1].documentation, docs.getDocumentation("signatureUser_Signature1")); + assert.equal(signatureHelp.signatures[1].parameters.length, 2); + assert.equal(signatureHelp.signatures[1].parameters[0].label, "user"); + assert.notEqual(signatureHelp.signatures[1].parameters[0].documentation, null); + assert.equal(signatureHelp.signatures[1].parameters[0].documentation, docs.getDocumentation("signatureUser_Signature1_Param0")); + assert.equal(signatureHelp.signatures[1].parameters[1].label, "group"); + assert.notEqual(signatureHelp.signatures[1].parameters[1].documentation, null); + assert.equal(signatureHelp.signatures[1].parameters[1].documentation, docs.getDocumentation("signatureUser_Signature1_Param1")); + + assert.equal(signatureHelp.signatures[2].label, "USER uid"); + assert.notEqual(signatureHelp.signatures[2].documentation, null); + assert.equal(signatureHelp.signatures[2].documentation, docs.getDocumentation("signatureUser_Signature2")); + assert.equal(signatureHelp.signatures[2].parameters.length, 1); + assert.equal(signatureHelp.signatures[2].parameters[0].label, "uid"); + assert.notEqual(signatureHelp.signatures[2].parameters[0].documentation, null); + assert.equal(signatureHelp.signatures[2].parameters[0].documentation, docs.getDocumentation("signatureUser_Signature2_Param")); + + assert.equal(signatureHelp.signatures[3].label, "USER uid:gid"); + assert.notEqual(signatureHelp.signatures[3].documentation, null); + assert.equal(signatureHelp.signatures[3].documentation, docs.getDocumentation("signatureUser_Signature3")); + assert.equal(signatureHelp.signatures[3].parameters.length, 2); + assert.equal(signatureHelp.signatures[3].parameters[0].label, "uid"); + assert.notEqual(signatureHelp.signatures[3].parameters[0].documentation, null); + assert.equal(signatureHelp.signatures[3].parameters[0].documentation, docs.getDocumentation("signatureUser_Signature3_Param0")); + assert.equal(signatureHelp.signatures[3].parameters[1].label, "gid"); + assert.notEqual(signatureHelp.signatures[3].parameters[1].documentation, null); + assert.equal(signatureHelp.signatures[3].parameters[1].documentation, docs.getDocumentation("signatureUser_Signature3_Param1")); +} + +function assertUser_GroupsOnly(signatureHelp: SignatureHelp, activeParameter: number) { + assert.equal(signatureHelp.activeSignature, 0); + assert.equal(signatureHelp.activeParameter, activeParameter); + assert.equal(signatureHelp.signatures.length, 2); + + assert.equal(signatureHelp.signatures[0].label, "USER user:group"); + assert.notEqual(signatureHelp.signatures[0].documentation, null); + assert.equal(signatureHelp.signatures[0].documentation, docs.getDocumentation("signatureUser_Signature1")); + assert.equal(signatureHelp.signatures[0].parameters.length, 2); + assert.equal(signatureHelp.signatures[0].parameters[0].label, "user"); + assert.notEqual(signatureHelp.signatures[0].parameters[0].documentation, null); + assert.equal(signatureHelp.signatures[0].parameters[0].documentation, docs.getDocumentation("signatureUser_Signature1_Param0")); + assert.equal(signatureHelp.signatures[0].parameters[1].label, "group"); + assert.notEqual(signatureHelp.signatures[0].parameters[1].documentation, null); + assert.equal(signatureHelp.signatures[0].parameters[1].documentation, docs.getDocumentation("signatureUser_Signature1_Param1")); + + assert.equal(signatureHelp.signatures[1].label, "USER uid:gid"); + assert.notEqual(signatureHelp.signatures[1].documentation, null); + assert.equal(signatureHelp.signatures[1].documentation, docs.getDocumentation("signatureUser_Signature3")); + assert.equal(signatureHelp.signatures[1].parameters.length, 2); + assert.equal(signatureHelp.signatures[1].parameters[0].label, "uid"); + assert.notEqual(signatureHelp.signatures[1].parameters[0].documentation, null); + assert.equal(signatureHelp.signatures[1].parameters[0].documentation, docs.getDocumentation("signatureUser_Signature3_Param0")); + assert.equal(signatureHelp.signatures[1].parameters[1].label, "gid"); + assert.notEqual(signatureHelp.signatures[1].parameters[1].documentation, null); + assert.equal(signatureHelp.signatures[1].parameters[1].documentation, docs.getDocumentation("signatureUser_Signature3_Param1")); +} + +function assertWorkdir(signatureHelp: SignatureHelp) { + assert.equal(signatureHelp.activeSignature, 0); + assert.equal(signatureHelp.activeParameter, 0); + assert.equal(signatureHelp.signatures.length, 1); + assert.equal(signatureHelp.signatures[0].label, "WORKDIR /the/workdir/path"); + assert.notEqual(signatureHelp.signatures[0].documentation, null); + assert.equal(signatureHelp.signatures[0].documentation, docs.getDocumentation("signatureWorkdir")); + assert.equal(signatureHelp.signatures[0].parameters.length, 1); + assert.equal(signatureHelp.signatures[0].parameters[0].label, "/the/workdir/path"); + assert.notEqual(signatureHelp.signatures[0].parameters[0].documentation, null); + assert.equal(signatureHelp.signatures[0].parameters[0].documentation, docs.getDocumentation("signatureWorkdir_Param")); +} + describe("Dockerfile Signature Tests", function() { describe("directives", function() { describe("escape", function() { @@ -149,6 +310,66 @@ describe("Dockerfile Signature Tests", function() { }); }); + function testArg(trigger: boolean) { + let onbuild = trigger ? "ONBUILD " : ""; + let triggerOffset = trigger ? 8 : 0; + + describe("ARG", function() { + it("name", function() { + let signatureHelp = compute(onbuild + "ARG ", 0, triggerOffset + 4); + assertArg_Name(signatureHelp); + + signatureHelp = compute(onbuild + "ARG name", 0, triggerOffset + 6); + assertArg_Name(signatureHelp); + + signatureHelp = compute(onbuild + "ARG name", 0, triggerOffset + 8); + assertArg_Name(signatureHelp); + }); + + it("name=defaultValue", function() { + let signatureHelp = compute(onbuild + "ARG name=", 0, triggerOffset + 4); + assertArg_NameDefaultValue(signatureHelp, 0); + + signatureHelp = compute(onbuild + "ARG name=", 0, triggerOffset + 6); + assertArg_NameDefaultValue(signatureHelp, 0); + + signatureHelp = compute(onbuild + "ARG name=", 0, triggerOffset + 8); + assertArg_NameDefaultValue(signatureHelp, 0); + + signatureHelp = compute(onbuild + "ARG name=value", 0, triggerOffset + 4); + assertArg_NameDefaultValue(signatureHelp, 0); + + signatureHelp = compute(onbuild + "ARG name=value", 0, triggerOffset + 6); + assertArg_NameDefaultValue(signatureHelp, 0); + + signatureHelp = compute(onbuild + "ARG name=value", 0, triggerOffset + 8); + assertArg_NameDefaultValue(signatureHelp, 0); + + signatureHelp = compute(onbuild + "ARG name=value", 0, triggerOffset + 9); + assertArg_NameDefaultValue(signatureHelp, 1); + + signatureHelp = compute(onbuild + "ARG name=value", 0, triggerOffset + 12); + assertArg_NameDefaultValue(signatureHelp, 1); + + signatureHelp = compute(onbuild + "ARG name=value ", 0, triggerOffset + 15); + assertArg_NameDefaultValue(signatureHelp, 1); + + signatureHelp = compute(onbuild + "ARG name=value space", 0, triggerOffset + 16); + assertArg_NameDefaultValue(signatureHelp, 1); + }); + + it("invalid", function() { + let signatureHelp = compute(onbuild + "ARG ", 0, triggerOffset + 1); + assertNoSignatures(signatureHelp); + + signatureHelp = compute(onbuild + "ARG ", 0, triggerOffset + 3); + assertNoSignatures(signatureHelp); + }); + }); + } + + testArg(false); + function testCopy(trigger: boolean) { let onbuild = trigger ? "ONBUILD " : ""; let triggerOffset = trigger ? 8 : 0; @@ -287,8 +508,225 @@ describe("Dockerfile Signature Tests", function() { testHealthcheck(false); - describe("ONBUILD", function() { + function testShell(trigger: boolean) { + let onbuild = trigger ? "ONBUILD " : ""; + let triggerOffset = trigger ? 8 : 0; + + describe("SHELL", function() { + it("[", function() { + let signatureHelp = compute(onbuild + "SHELL ", 0, triggerOffset + 6); + assertShell(signatureHelp, 0); + + signatureHelp = compute(onbuild + "SHELL ", 0, triggerOffset + 7); + assertShell(signatureHelp, 0); + }); + + it("executable", function() { + let signatureHelp = compute(onbuild + "SHELL [", 0, triggerOffset + 7); + assertShell(signatureHelp, 1); + + signatureHelp = compute(onbuild + "SHELL [\"", 0, triggerOffset + 8); + assertShell(signatureHelp, 1); + + signatureHelp = compute(onbuild + "SHELL [ ", 0, triggerOffset + 8); + assertShell(signatureHelp, 1); + + signatureHelp = compute(onbuild + "SHELL [ \"", 0, triggerOffset + 9); + assertShell(signatureHelp, 1); + + signatureHelp = compute(onbuild + "SHELL [\"cmd\"", 0, triggerOffset + 12); + assertShell(signatureHelp, 1); + + signatureHelp = compute(onbuild + "SHELL [ \"cmd\"", 0, triggerOffset + 13); + assertShell(signatureHelp, 1); + + signatureHelp = compute(onbuild + "SHELL [\"cmd\",", 0, triggerOffset + 12); + assertShell(signatureHelp, 1); + + signatureHelp = compute(onbuild + "SHELL [ \"cmd\",", 0, triggerOffset + 13); + assertShell(signatureHelp, 1); + + signatureHelp = compute(onbuild + "SHELL []", 0, triggerOffset + 7); + assertShell(signatureHelp, 1); + }); + + it("parameter", function() { + let signatureHelp = compute(onbuild + "SHELL [\"cmd\",", 0, triggerOffset + 13); + assertShell(signatureHelp, 2); + + signatureHelp = compute(onbuild + "SHELL [ \"cmd\",", 0, triggerOffset + 14); + assertShell(signatureHelp, 2); + + signatureHelp = compute(onbuild + "SHELL [\"cmd\" ,", 0, triggerOffset + 14); + assertShell(signatureHelp, 2); + + signatureHelp = compute(onbuild + "SHELL [ \"cmd\" ,", 0, triggerOffset + 15); + assertShell(signatureHelp, 2); + + signatureHelp = compute(onbuild + "SHELL [\"cmd\", ", 0, triggerOffset + 14); + assertShell(signatureHelp, 2); + + signatureHelp = compute(onbuild + "SHELL [ \"cmd\", ", 0, triggerOffset + 15); + assertShell(signatureHelp, 2); + + signatureHelp = compute(onbuild + "SHELL [\"cmd\" , ", 0, triggerOffset + 15); + assertShell(signatureHelp, 2); + + signatureHelp = compute(onbuild + "SHELL [ \"cmd\" , ", 0, triggerOffset + 16); + assertShell(signatureHelp, 2); + + signatureHelp = compute(onbuild + "SHELL [ \"cmd\" , \"\"", 0, triggerOffset + 18); + assertShell(signatureHelp, 2); + + signatureHelp = compute(onbuild + "SHELL [ \"cmd\" , \"\",", 0, triggerOffset + 18); + assertShell(signatureHelp, 2); + }); + + it("...", function() { + let signatureHelp = compute(onbuild + "SHELL [\"cmd\", \"/C\",", 0, triggerOffset + 19); + assertShell(signatureHelp, 3); + + signatureHelp = compute(onbuild + "SHELL [\"cmd\", \"/C\", ", 0, triggerOffset + 20); + assertShell(signatureHelp, 3); + + signatureHelp = compute(onbuild + "SHELL [\"cmd\", \"/C\", \"/C\"]", 0, triggerOffset + 24); + assertShell(signatureHelp, 3); + }); + + it("]", function() { + let signatureHelp = compute(onbuild + "SHELL []", 0, triggerOffset + 8); + assertShell(signatureHelp, 4); + + signatureHelp = compute(onbuild + "SHELL [ ]", 0, triggerOffset + 10); + assertShell(signatureHelp, 4); + + signatureHelp = compute(onbuild + "SHELL [ \"cmd\" ]", 0, triggerOffset + 16); + assertShell(signatureHelp, 4); + }); + + it("invalid", function() { + let signatureHelp = compute(onbuild + "SHELL [ \"cmd\" ] ", 0, triggerOffset + 2); + assertNoSignatures(signatureHelp); + + signatureHelp = compute(onbuild + "SHELL [] ", 0, triggerOffset + 9); + assertNoSignatures(signatureHelp); + + signatureHelp = compute(onbuild + "SHELL [ \"cmd\" ] ", 0, triggerOffset + 17); + assertNoSignatures(signatureHelp); + }); + }); + } + + testShell(false); + + function testStopsignal(trigger: boolean) { + let onbuild = trigger ? "ONBUILD " : ""; + let triggerOffset = trigger ? 8 : 0; + + describe("STOPSIGNAL", function() { + it("ok", function() { + let signatureHelp = compute(onbuild + "STOPSIGNAL ", 0, triggerOffset + 11); + assertStopsignal(signatureHelp); + + signatureHelp = compute(onbuild + "STOPSIGNAL SIGKILL", 0, triggerOffset + 14); + assertStopsignal(signatureHelp); + + signatureHelp = compute("WORKDIR /path\n" + onbuild + "STOPSIGNAL SIGKILL", 1, triggerOffset + 14); + assertStopsignal(signatureHelp); + }); + + it("invalid", function() { + let signatureHelp = compute(onbuild + "STOPSIGNAL SIGKILL", 0, triggerOffset + 5); + assertNoSignatures(signatureHelp); + }); + }); + } + + testStopsignal(false); + + function testUser(trigger: boolean) { + let onbuild = trigger ? "ONBUILD " : ""; + let triggerOffset = trigger ? 8 : 0; + + describe("USER", function() { + it("user / uid", function() { + let signatureHelp = compute(onbuild + "USER ", 0, triggerOffset + 5); + assertUser_All(signatureHelp); + + signatureHelp = compute(onbuild + "USER user", 0, triggerOffset + 7); + assertUser_All(signatureHelp); + + signatureHelp = compute(onbuild + "USER user ", 0, triggerOffset + 10); + assertUser_All(signatureHelp); + + signatureHelp = compute(onbuild + "USER user name", 0, triggerOffset + 12); + assertUser_All(signatureHelp); + }); + + it("user:group / uid:gid", function() { + let signatureHelp = compute(onbuild + "USER user:group", 0, triggerOffset + 7); + assertUser_GroupsOnly(signatureHelp, 0); + + signatureHelp = compute(onbuild + "USER user:group", 0, triggerOffset + 9); + assertUser_GroupsOnly(signatureHelp, 0); + + signatureHelp = compute(onbuild + "USER user:group", 0, triggerOffset + 10); + assertUser_GroupsOnly(signatureHelp, 1); + + signatureHelp = compute(onbuild + "USER user:group", 0, triggerOffset + 13); + assertUser_GroupsOnly(signatureHelp, 1); + + signatureHelp = compute(onbuild + "USER user name:group name", 0, triggerOffset + 12); + assertUser_GroupsOnly(signatureHelp, 0); + + signatureHelp = compute(onbuild + "USER user name:group name", 0, triggerOffset + 14); + assertUser_GroupsOnly(signatureHelp, 0); + + signatureHelp = compute(onbuild + "USER user name:group name", 0, triggerOffset + 15); + assertUser_GroupsOnly(signatureHelp, 1); + + signatureHelp = compute(onbuild + "USER user name:group name", 0, triggerOffset + 18); + assertUser_GroupsOnly(signatureHelp, 1); + }); + + it("invalid", function() { + let signatureHelp = compute(onbuild + "USER user", 0, triggerOffset + 2); + assertNoSignatures(signatureHelp); + }); + }); + } + + testUser(false); + + function testWorkdir(trigger: boolean) { + let onbuild = trigger ? "ONBUILD " : ""; + let triggerOffset = trigger ? 8 : 0; + + describe("WORKDIR", function() { + it("ok", function() { + let signatureHelp = compute(onbuild + "WORKDIR ", 0, triggerOffset + 8); + assertWorkdir(signatureHelp); + + signatureHelp = compute(onbuild + "WORKDIR a b", 0, triggerOffset + 11); + assertWorkdir(signatureHelp); + }); + + it("invalid", function() { + let signatureHelp = compute(onbuild + "WORKDIR /path", 0, triggerOffset + 2); + assertNoSignatures(signatureHelp); + }); + }); + } + + testWorkdir(false); + + describe("ONBUILD triggers", function() { + testArg(true); testCopy(true); testHealthcheck(true); + testShell(true); + testStopsignal(true); + testUser(true); + testWorkdir(true); }); });