diff --git a/CHANGELOG.md b/CHANGELOG.md index a3d4631..b271696 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ All notable changes to this project will be documented in this file. - instructions ([#162](https://github.com/rcjsuen/dockerfile-language-server-nodejs/issues/162)) - ARG - EXPOSE + - FROM - SHELL - STOPSIGNAL - USER diff --git a/src/dockerPlainText.ts b/src/dockerPlainText.ts index e258e52..f277f24 100644 --- a/src/dockerPlainText.ts +++ b/src/dockerPlainText.ts @@ -45,6 +45,13 @@ export class PlainTextDocumentation { "signatureCopyFlagFrom_Param": "The build stage or image name to use as the source. Also may be a numeric index.", "signatureExpose": "Define network ports for this container to listen on at runtime.", "signatureExpose_Param0": "The port that this container should listen on.", + "signatureFrom_Signature0": "Set the base image to use for any subsequent instructions that follow.", + "signatureFrom_Signature0_Param": "The name of the base image to use.", + "signatureFrom_Signature1_Param1": "The tag of the base image to use.", + "signatureFrom_Signature2_Param1": "The digest of the base image to use.", + "signatureFrom_Signature3": "Set the base image to use for any subsequent instructions that follow and also give this build stage a name.", + "signatureFrom_Signature3_Param2": "The name of this build stage.", + "signatureFrom_Param2": "The name of this build stage.", "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.", @@ -196,6 +203,25 @@ export class PlainTextDocumentation { signatureExpose: this.dockerMessages["signatureExpose"], signatureExpose_Param0: this.dockerMessages["signatureExpose_Param0"], signatureExpose_Param1: this.dockerMessages["signatureExpose_Param0"], + signatureFrom_Signature0: this.dockerMessages["signatureFrom_Signature0"], + signatureFrom_Signature0_Param: this.dockerMessages["signatureFrom_Signature0_Param"], + signatureFrom_Signature1: this.dockerMessages["signatureFrom_Signature0"], + signatureFrom_Signature1_Param0: this.dockerMessages["signatureFrom_Signature0_Param"], + signatureFrom_Signature1_Param1: this.dockerMessages["signatureFrom_Signature1_Param1"], + signatureFrom_Signature2: this.dockerMessages["signatureFrom_Signature0"], + signatureFrom_Signature2_Param0: this.dockerMessages["signatureFrom_Signature0_Param"], + signatureFrom_Signature2_Param1: this.dockerMessages["signatureFrom_Signature2_Param1"], + signatureFrom_Signature3: this.dockerMessages["signatureFrom_Signature3"], + signatureFrom_Signature3_Param0: this.dockerMessages["signatureFrom_Signature0_Param"], + signatureFrom_Signature3_Param2: this.dockerMessages["signatureFrom_Signature3_Param2"], + signatureFrom_Signature4: this.dockerMessages["signatureFrom_Signature3"], + signatureFrom_Signature4_Param0: this.dockerMessages["signatureFrom_Signature0_Param"], + signatureFrom_Signature4_Param1: this.dockerMessages["signatureFrom_Signature1_Param1"], + signatureFrom_Signature4_Param3: this.dockerMessages["signatureFrom_Signature3_Param2"], + signatureFrom_Signature5: this.dockerMessages["signatureFrom_Signature3"], + signatureFrom_Signature5_Param0: this.dockerMessages["signatureFrom_Signature0_Param"], + signatureFrom_Signature5_Param1: this.dockerMessages["signatureFrom_Signature2_Param1"], + signatureFrom_Signature5_Param3: this.dockerMessages["signatureFrom_Signature3_Param2"], signatureHealthcheck: this.dockerMessages["signatureHealthcheck"], signatureHealthcheckFlagInterval_Param: this.dockerMessages["hoverHealthcheckFlagInterval"], signatureHealthcheckFlagRetries_Param: this.dockerMessages["hoverHealthcheckFlagRetries"], diff --git a/src/dockerSignatures.ts b/src/dockerSignatures.ts index abc59d4..0257ef5 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, Range, Position, SignatureHelp + TextDocument, Range, Position, SignatureHelp, SignatureInformation } from 'vscode-languageserver'; import { Dockerfile } from './parser/dockerfile'; +import { Argument } from './parser/argument'; import { Instruction } from './parser/instruction'; import { Arg } from './parser/instructions/arg'; import { Copy } from './parser/instructions/copy'; +import { From } from './parser/instructions/from'; import { Healthcheck } from './parser/instructions/healthcheck'; import { Shell } from './parser/instructions/shell'; import { PlainTextDocumentation } from './dockerPlainText'; @@ -180,6 +182,8 @@ export class DockerSignatures { exposeSignatureHelp.activeParameter = 1; } return exposeSignatureHelp; + case "FROM": + return this.getFromSignatureHelp(position, instruction as From); case "HEALTHCHECK": let flags = (instruction as Healthcheck).getFlags(); for (let flag of flags) { @@ -460,4 +464,164 @@ export class DockerSignatures { } return null; } + + private getFromSignatureHelp(position: Position, from: From): SignatureHelp { + let baseImage = { + label: "FROM baseImage", + documentation: this.documentation.getDocumentation("signatureFrom_Signature0"), + parameters: [ + { + label: "baseImage", + documentation: this.documentation.getDocumentation("signatureFrom_Signature0_Param") + } + ] + }; + let baseImageTag = { + label: "FROM baseImage:tag", + documentation: this.documentation.getDocumentation("signatureFrom_Signature1"), + parameters: [ + { + label: "baseImage", + documentation: this.documentation.getDocumentation("signatureFrom_Signature1_Param0") + }, + { + label: "tag", + documentation: this.documentation.getDocumentation("signatureFrom_Signature1_Param1") + } + ] + }; + let baseImageDigest = { + label: "FROM baseImage@digest", + documentation: this.documentation.getDocumentation("signatureFrom_Signature2"), + parameters: [ + { + label: "baseImage", + documentation: this.documentation.getDocumentation("signatureFrom_Signature2_Param0") + }, + { + label: "digest", + documentation: this.documentation.getDocumentation("signatureFrom_Signature2_Param1") + } + ] + }; + let baseImageStage = { + label: "FROM baseImage AS stage", + documentation: this.documentation.getDocumentation("signatureFrom_Signature3"), + parameters: [ + { + label: "baseImage", + documentation: this.documentation.getDocumentation("signatureFrom_Signature3_Param0") + }, + { + label: "AS", + }, + { + label: "stage", + documentation: this.documentation.getDocumentation("signatureFrom_Signature3_Param2") + } + ] + }; + let baseImageTagStage = { + label: "FROM baseImage:tag AS stage", + documentation: this.documentation.getDocumentation("signatureFrom_Signature4"), + parameters: [ + { + label: "baseImage", + documentation: this.documentation.getDocumentation("signatureFrom_Signature4_Param0") + }, + { + label: "tag", + documentation: this.documentation.getDocumentation("signatureFrom_Signature4_Param1") + }, + { + label: "AS", + }, + { + label: "stage", + documentation: this.documentation.getDocumentation("signatureFrom_Signature4_Param3") + } + ] + }; + let baseImageDigestStage = { + label: "FROM baseImage@digest AS stage", + documentation: this.documentation.getDocumentation("signatureFrom_Signature5"), + parameters: [ + { + label: "baseImage", + documentation: this.documentation.getDocumentation("signatureFrom_Signature5_Param0") + }, + { + label: "digest", + documentation: this.documentation.getDocumentation("signatureFrom_Signature5_Param1") + }, + { + label: "AS", + }, + { + label: "stage", + documentation: this.documentation.getDocumentation("signatureFrom_Signature5_Param3") + } + ] + }; + let fromSignatures = [ + baseImage, baseImageTag, baseImageDigest, + baseImageStage, baseImageTagStage, baseImageDigestStage + ]; + + const args = from.getArguments(); + if (args.length >= 3 && args[2].isBefore(position)) { + return null; + } else if (args.length === 0) { + return { + signatures: fromSignatures, + activeSignature: 0, + activeParameter: 0 + }; + } + + const image = args[0].getValue(); + const digest = image.indexOf('@') !== -1; + const tag = !digest && image.indexOf(':') !== -1; + const stagesOnly = args.length > 1 || args[0].isBefore(position); + return { + signatures: this.getFromSignatures(fromSignatures, tag, digest, stagesOnly), + activeSignature: 0, + activeParameter: this.getFromActiveParameter(position, from, image, tag, digest, args) + }; + } + + private getFromSignatures(fromSignatures: SignatureInformation[], tag: boolean, digest: boolean, stagesOnly: boolean): SignatureInformation[] { + if (digest) { + return stagesOnly ? [ fromSignatures[5] ] : [ fromSignatures[2], fromSignatures[5] ]; + } else if (tag) { + return stagesOnly ? [ fromSignatures[4] ] : [ fromSignatures[1], fromSignatures[4] ]; + } + return stagesOnly ? [ fromSignatures[3], fromSignatures[4], fromSignatures[5] ] : fromSignatures; + } + + private getFromActiveParameter(position: Position, from: From, image: string, tag: boolean, digest: boolean, args: Argument[]): number { + const inTag = tag && Util.isInsideRange(position, from.getImageTagRange()); + const inDigest = digest && Util.isInsideRange(position, from.getImageDigestRange()); + const inImage = !inTag && !inDigest && Util.isInsideRange(position, args[0].getRange()); + if (args.length === 1) { + if (args[0].isBefore(position)) { + return tag || digest ? 2 : 1; + } + return inTag || inDigest ? 1 : 0; + } else if (args.length === 2) { + if (args[1].isBefore(position)) { + return tag || digest ? 3 : 2; + } else if (Util.isInsideRange(position, args[1].getRange()) || args[0].isBefore(position)) { + return tag || digest ? 2 : 1; + } + return inTag || inDigest ? 1 : 0; + } + + if (Util.isInsideRange(position, args[2].getRange()) || args[1].isBefore(position)) { + return tag || digest ? 3 : 2; + } else if (Util.isInsideRange(position, args[1].getRange()) || args[0].isBefore(position)) { + return tag || digest ? 2 : 1; + } + return inTag || inDigest ? 1 : 0; + } } \ No newline at end of file diff --git a/src/parser/argument.ts b/src/parser/argument.ts index 9eb170c..cabd647 100644 --- a/src/parser/argument.ts +++ b/src/parser/argument.ts @@ -22,6 +22,13 @@ export class Argument { return this.value; } + public isAfter(position: Position): boolean { + if (this.range.end.line < position.line) { + return true; + } + return this.range.start.line < position.line ? true : this.range.start.character > position.character; + } + public isBefore(position: Position): boolean { if (this.range.start.line < position.line) { return true; diff --git a/src/parser/instructions/from.ts b/src/parser/instructions/from.ts index 92d9ecc..f8816c9 100644 --- a/src/parser/instructions/from.ts +++ b/src/parser/instructions/from.ts @@ -65,6 +65,25 @@ export class From extends Instruction { return null; } + /** + * Returns the range in the document that the digest of the base + * image encompasses. + * + * @return the base image's digest's range in the document, or null + * if no digest has been specified + */ + public getImageDigestRange(): Range | null { + let range = this.getImageRange(); + if (range) { + let content = this.getRangeContent(range); + let index = content.lastIndexOf('@'); + if (index !== -1) { + return Range.create(range.start.line, range.start.character + index + 1, range.end.line, range.end.character); + } + } + return null; + } + public getBuildStage(): string | null { let range = this.getBuildStageRange(); return range === null ? null : this.getRangeContent(range); diff --git a/test/dockerSignatures.tests.ts b/test/dockerSignatures.tests.ts index 5e6ddf7..eb7e12a 100644 --- a/test/dockerSignatures.tests.ts +++ b/test/dockerSignatures.tests.ts @@ -5,7 +5,7 @@ import * as assert from "assert"; import { - TextDocument, Position, Range, SignatureHelp + TextDocument, Position, Range, SignatureHelp, SignatureInformation } from 'vscode-languageserver'; import { PlainTextDocumentation } from '../src/dockerPlainText'; import { KEYWORDS, DIRECTIVE_ESCAPE } from '../src/docker'; @@ -163,6 +163,144 @@ function assertExpose(signatureHelp: SignatureHelp, activeParameter: number) { assert.equal(signatureHelp.signatures[0].parameters[1].documentation, docs.getDocumentation("signatureExpose_Param1")); } +function assertFrom_Image(signature: SignatureInformation) { + assert.equal(signature.label, "FROM baseImage"); + assert.notEqual(signature.documentation, null); + assert.equal(signature.documentation, docs.getDocumentation("signatureFrom_Signature0")); + assert.equal(signature.parameters.length, 1); + assert.equal(signature.parameters[0].label, "baseImage"); + assert.notEqual(signature.parameters[0].documentation, null); + assert.equal(signature.parameters[0].documentation, docs.getDocumentation("signatureFrom_Signature0_Param")); +} + +function assertFrom_ImageTag(signature: SignatureInformation) { + assert.equal(signature.label, "FROM baseImage:tag"); + assert.notEqual(signature.documentation, null); + assert.equal(signature.documentation, docs.getDocumentation("signatureFrom_Signature1")); + assert.equal(signature.parameters.length, 2); + assert.equal(signature.parameters[0].label, "baseImage"); + assert.notEqual(signature.parameters[0].documentation, null); + assert.equal(signature.parameters[0].documentation, docs.getDocumentation("signatureFrom_Signature1_Param0")); + assert.equal(signature.parameters[1].label, "tag"); + assert.notEqual(signature.parameters[1].documentation, null); + assert.equal(signature.parameters[1].documentation, docs.getDocumentation("signatureFrom_Signature1_Param1")); +} + +function assertFrom_ImageDigest(signature: SignatureInformation) { + assert.equal(signature.label, "FROM baseImage@digest"); + assert.notEqual(signature.documentation, null); + assert.equal(signature.documentation, docs.getDocumentation("signatureFrom_Signature2")); + assert.equal(signature.parameters.length, 2); + assert.equal(signature.parameters[0].label, "baseImage"); + assert.notEqual(signature.parameters[0].documentation, null); + assert.equal(signature.parameters[0].documentation, docs.getDocumentation("signatureFrom_Signature2_Param0")); + assert.equal(signature.parameters[1].label, "digest"); + assert.notEqual(signature.parameters[1].documentation, null); + assert.equal(signature.parameters[1].documentation, docs.getDocumentation("signatureFrom_Signature2_Param1")); +} + +function assertFrom_Image_BuildStage(signature: SignatureInformation) { + assert.equal(signature.label, "FROM baseImage AS stage"); + assert.notEqual(signature.documentation, null); + assert.equal(signature.documentation, docs.getDocumentation("signatureFrom_Signature3")); + assert.equal(signature.parameters.length, 3); + assert.equal(signature.parameters[0].label, "baseImage"); + assert.notEqual(signature.parameters[0].documentation, null); + assert.equal(signature.parameters[0].documentation, docs.getDocumentation("signatureFrom_Signature3_Param0")); + assert.equal(signature.parameters[1].label, "AS"); + assert.equal(signature.parameters[1].documentation, null); + assert.equal(signature.parameters[2].label, "stage"); + assert.notEqual(signature.parameters[2].documentation, null); + assert.equal(signature.parameters[2].documentation, docs.getDocumentation("signatureFrom_Signature3_Param2")); +} + +function assertFrom_ImageTag_BuildStage(signature: SignatureInformation) { + assert.equal(signature.label, "FROM baseImage:tag AS stage"); + assert.notEqual(signature.documentation, null); + assert.equal(signature.documentation, docs.getDocumentation("signatureFrom_Signature4")); + assert.equal(signature.parameters.length, 4); + assert.equal(signature.parameters[0].label, "baseImage"); + assert.notEqual(signature.parameters[0].documentation, null); + assert.equal(signature.parameters[0].documentation, docs.getDocumentation("signatureFrom_Signature4_Param0")); + assert.equal(signature.parameters[1].label, "tag"); + assert.notEqual(signature.parameters[1].documentation, null); + assert.equal(signature.parameters[1].documentation, docs.getDocumentation("signatureFrom_Signature4_Param1")); + assert.equal(signature.parameters[2].label, "AS"); + assert.equal(signature.parameters[2].documentation, null); + assert.equal(signature.parameters[3].label, "stage"); + assert.notEqual(signature.parameters[3].documentation, null); + assert.equal(signature.parameters[3].documentation, docs.getDocumentation("signatureFrom_Signature4_Param3")); +} + +function assertFrom_ImageDigest_BuildStage(signature: SignatureInformation) { + assert.equal(signature.label, "FROM baseImage@digest AS stage"); + assert.notEqual(signature.documentation, null); + assert.equal(signature.documentation, docs.getDocumentation("signatureFrom_Signature5")); + assert.equal(signature.parameters.length, 4); + assert.equal(signature.parameters[0].label, "baseImage"); + assert.notEqual(signature.parameters[0].documentation, null); + assert.equal(signature.parameters[0].documentation, docs.getDocumentation("signatureFrom_Signature5_Param0")); + assert.equal(signature.parameters[1].label, "digest"); + assert.notEqual(signature.parameters[1].documentation, null); + assert.equal(signature.parameters[1].documentation, docs.getDocumentation("signatureFrom_Signature5_Param1")); + assert.equal(signature.parameters[2].label, "AS"); + assert.equal(signature.parameters[2].documentation, null); + assert.equal(signature.parameters[3].label, "stage"); + assert.notEqual(signature.parameters[3].documentation, null); + assert.equal(signature.parameters[3].documentation, docs.getDocumentation("signatureFrom_Signature5_Param3")); +} + +function assertFrom(signatureHelp: SignatureHelp) { + assert.equal(signatureHelp.signatures.length, 6); + assert.equal(signatureHelp.activeSignature, 0); + assert.equal(signatureHelp.activeParameter, 0); + assertFrom_Image(signatureHelp.signatures[0]); + assertFrom_ImageTag(signatureHelp.signatures[1]); + assertFrom_ImageDigest(signatureHelp.signatures[2]); + assertFrom_Image_BuildStage(signatureHelp.signatures[3]); + assertFrom_ImageTag_BuildStage(signatureHelp.signatures[4]); + assertFrom_ImageDigest_BuildStage(signatureHelp.signatures[5]); +} + +function assertFrom_Tags(signatureHelp: SignatureHelp, activeParameter: number) { + assert.equal(signatureHelp.signatures.length, 2); + assert.equal(signatureHelp.activeSignature, 0); + assert.equal(signatureHelp.activeParameter, activeParameter); + assertFrom_ImageTag(signatureHelp.signatures[0]); + assertFrom_ImageTag_BuildStage(signatureHelp.signatures[1]); +} + +function assertFrom_Digests(signatureHelp: SignatureHelp, activeParameter: number) { + assert.equal(signatureHelp.signatures.length, 2); + assert.equal(signatureHelp.activeSignature, 0); + assert.equal(signatureHelp.activeParameter, activeParameter); + assertFrom_ImageDigest(signatureHelp.signatures[0]); + assertFrom_ImageDigest_BuildStage(signatureHelp.signatures[1]); +} + +function assertFrom_BuildStages(signatureHelp: SignatureHelp, activeParameter: number) { + assert.equal(signatureHelp.signatures.length, 3); + assert.equal(signatureHelp.activeSignature, 0); + assert.equal(signatureHelp.activeParameter, activeParameter); + assertFrom_Image_BuildStage(signatureHelp.signatures[0]); + assertFrom_ImageTag_BuildStage(signatureHelp.signatures[1]); + assertFrom_ImageDigest_BuildStage(signatureHelp.signatures[2]); +} + +function assertFrom_Tags_BuildStages_Only(signatureHelp: SignatureHelp, activeParameter: number) { + assert.equal(signatureHelp.signatures.length, 1); + assert.equal(signatureHelp.activeSignature, 0); + assert.equal(signatureHelp.activeParameter, activeParameter); + assertFrom_ImageTag_BuildStage(signatureHelp.signatures[0]); +} + +function assertFrom_Digests_BuildStages_Only(signatureHelp: SignatureHelp, activeParameter: number) { + assert.equal(signatureHelp.signatures.length, 1); + assert.equal(signatureHelp.activeSignature, 0); + assert.equal(signatureHelp.activeParameter, activeParameter); + assertFrom_ImageDigest_BuildStage(signatureHelp.signatures[0]); +} + function assertShell(signatureHelp: SignatureHelp, activeParameter: number) { assert.equal(signatureHelp.activeSignature, 0); assert.equal(signatureHelp.activeParameter, activeParameter); @@ -476,6 +614,161 @@ describe("Dockerfile Signature Tests", function() { testExpose(false); + describe("FROM", function() { + it("all", function() { + assertFrom(compute("FROM ", 0, 5)); + assertFrom(compute("FROM node", 0, 7)); + assertFrom(compute("FROM node", 0, 9)); + assertFrom(compute("FROM node", 0, 5)); + }); + + it("tags", function() { + assertFrom_Tags(compute("FROM node:", 0, 7), 0); + assertFrom_Tags(compute("FROM node:", 0, 10), 1); + assertFrom_Tags(compute("FROM node:latest", 0, 12), 1); + assertFrom_Tags(compute("FROM node:latest", 0, 16), 1); + }); + + it("digests", function() { + assertFrom_Digests(compute("FROM node@", 0, 7), 0); + assertFrom_Digests(compute("FROM node@", 0, 10), 1); + assertFrom_Digests(compute("FROM node@sha256:613685c22f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700", 0, 12), 1); + assertFrom_Digests(compute("FROM node@sha256:613685c22f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700", 0, 16), 1); + assertFrom_Digests(compute("FROM node@sha256:613685c22f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700", 0, 17), 1); + assertFrom_Digests(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700", 0, 20), 1); + assertFrom_Digests(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700", 0, 80), 1); + }); + + it("stages", function() { + assertFrom_BuildStages(compute("FROM node ", 0, 10), 1); + + assertFrom_BuildStages(compute("FROM node AS", 0, 5), 0); + assertFrom_BuildStages(compute("FROM node AS", 0, 7), 0); + assertFrom_BuildStages(compute("FROM node AS", 0, 9), 0); + assertFrom_BuildStages(compute("FROM node AS", 0, 10), 1); + assertFrom_BuildStages(compute("FROM node AS", 0, 11), 1); + assertFrom_BuildStages(compute("FROM node AS", 0, 12), 1); + + assertFrom_BuildStages(compute("FROM node AS ", 0, 13), 2); + + assertFrom_BuildStages(compute("FROM node AS js", 0, 5), 0); + assertFrom_BuildStages(compute("FROM node AS js", 0, 7), 0); + assertFrom_BuildStages(compute("FROM node AS js", 0, 9), 0); + assertFrom_BuildStages(compute("FROM node AS js", 0, 10), 1); + assertFrom_BuildStages(compute("FROM node AS js", 0, 11), 1); + assertFrom_BuildStages(compute("FROM node AS js", 0, 12), 1); + assertFrom_BuildStages(compute("FROM node AS js", 0, 13), 2); + assertFrom_BuildStages(compute("FROM node AS js", 0, 14), 2); + assertFrom_BuildStages(compute("FROM node AS js", 0, 15), 2); + }); + + it("tags and stages", function() { + assertFrom_Tags_BuildStages_Only(compute("FROM node: ", 0, 11), 2); + + assertFrom_Tags_BuildStages_Only(compute("FROM node: AS", 0, 5), 0); + assertFrom_Tags_BuildStages_Only(compute("FROM node: AS", 0, 7), 0); + assertFrom_Tags_BuildStages_Only(compute("FROM node: AS", 0, 9), 0); + assertFrom_Tags_BuildStages_Only(compute("FROM node: AS", 0, 10), 1); + assertFrom_Tags_BuildStages_Only(compute("FROM node: AS", 0, 11), 2); + assertFrom_Tags_BuildStages_Only(compute("FROM node: AS", 0, 12), 2); + assertFrom_Tags_BuildStages_Only(compute("FROM node: AS", 0, 13), 2); + + assertFrom_Tags_BuildStages_Only(compute("FROM node: AS ", 0, 14), 3); + + assertFrom_Tags_BuildStages_Only(compute("FROM node: AS js", 0, 5), 0); + assertFrom_Tags_BuildStages_Only(compute("FROM node: AS js", 0, 7), 0); + assertFrom_Tags_BuildStages_Only(compute("FROM node: AS js", 0, 9), 0); + assertFrom_Tags_BuildStages_Only(compute("FROM node: AS js", 0, 10), 1); + assertFrom_Tags_BuildStages_Only(compute("FROM node: AS js", 0, 11), 2); + assertFrom_Tags_BuildStages_Only(compute("FROM node: AS js", 0, 12), 2); + assertFrom_Tags_BuildStages_Only(compute("FROM node: AS js", 0, 13), 2); + assertFrom_Tags_BuildStages_Only(compute("FROM node: AS js", 0, 14), 3); + assertFrom_Tags_BuildStages_Only(compute("FROM node: AS js", 0, 15), 3); + assertFrom_Tags_BuildStages_Only(compute("FROM node: AS js", 0, 16), 3); + + assertFrom_Tags_BuildStages_Only(compute("FROM node:latest AS", 0, 5), 0); + assertFrom_Tags_BuildStages_Only(compute("FROM node:latest AS", 0, 7), 0); + assertFrom_Tags_BuildStages_Only(compute("FROM node:latest AS", 0, 9), 0); + assertFrom_Tags_BuildStages_Only(compute("FROM node:latest AS", 0, 10), 1); + assertFrom_Tags_BuildStages_Only(compute("FROM node:latest AS", 0, 13), 1); + assertFrom_Tags_BuildStages_Only(compute("FROM node:latest AS", 0, 16), 1); + assertFrom_Tags_BuildStages_Only(compute("FROM node:latest AS", 0, 17), 2); + assertFrom_Tags_BuildStages_Only(compute("FROM node:latest AS", 0, 18), 2); + assertFrom_Tags_BuildStages_Only(compute("FROM node:latest AS", 0, 19), 2); + assertFrom_Tags_BuildStages_Only(compute("FROM node:latest AS ", 0, 20), 3); + assertFrom_Tags_BuildStages_Only(compute("FROM node:latest AS js", 0, 5), 0); + assertFrom_Tags_BuildStages_Only(compute("FROM node:latest AS js", 0, 7), 0); + assertFrom_Tags_BuildStages_Only(compute("FROM node:latest AS js", 0, 9), 0); + assertFrom_Tags_BuildStages_Only(compute("FROM node:latest AS js", 0, 10), 1); + assertFrom_Tags_BuildStages_Only(compute("FROM node:latest AS js", 0, 13), 1); + assertFrom_Tags_BuildStages_Only(compute("FROM node:latest AS js", 0, 16), 1); + assertFrom_Tags_BuildStages_Only(compute("FROM node:latest AS js", 0, 17), 2); + assertFrom_Tags_BuildStages_Only(compute("FROM node:latest AS js", 0, 18), 2); + assertFrom_Tags_BuildStages_Only(compute("FROM node:latest AS js", 0, 19), 2); + assertFrom_Tags_BuildStages_Only(compute("FROM node:latest AS js", 0, 20), 3); + assertFrom_Tags_BuildStages_Only(compute("FROM node:latest AS js", 0, 21), 3); + assertFrom_Tags_BuildStages_Only(compute("FROM node:latest AS js", 0, 22), 3); + }); + + it("digests and stages", function() { + assertFrom_Digests_BuildStages_Only(compute("FROM node@ ", 0, 11), 2); + + assertFrom_Digests_BuildStages_Only(compute("FROM node@ AS", 0, 5), 0); + assertFrom_Digests_BuildStages_Only(compute("FROM node@ AS", 0, 7), 0); + assertFrom_Digests_BuildStages_Only(compute("FROM node@ AS", 0, 9), 0); + assertFrom_Digests_BuildStages_Only(compute("FROM node@ AS", 0, 10), 1); + assertFrom_Digests_BuildStages_Only(compute("FROM node@ AS", 0, 11), 2); + assertFrom_Digests_BuildStages_Only(compute("FROM node@ AS", 0, 12), 2); + assertFrom_Digests_BuildStages_Only(compute("FROM node@ AS", 0, 13), 2); + + assertFrom_Digests_BuildStages_Only(compute("FROM node@ AS ", 0, 14), 3); + + assertFrom_Digests_BuildStages_Only(compute("FROM node@ AS js", 0, 5), 0); + assertFrom_Digests_BuildStages_Only(compute("FROM node@ AS js", 0, 7), 0); + assertFrom_Digests_BuildStages_Only(compute("FROM node@ AS js", 0, 9), 0); + assertFrom_Digests_BuildStages_Only(compute("FROM node@ AS js", 0, 10), 1); + assertFrom_Digests_BuildStages_Only(compute("FROM node@ AS js", 0, 11), 2); + assertFrom_Digests_BuildStages_Only(compute("FROM node@ AS js", 0, 12), 2); + assertFrom_Digests_BuildStages_Only(compute("FROM node@ AS js", 0, 13), 2); + assertFrom_Digests_BuildStages_Only(compute("FROM node@ AS js", 0, 14), 3); + assertFrom_Digests_BuildStages_Only(compute("FROM node@ AS js", 0, 15), 3); + assertFrom_Digests_BuildStages_Only(compute("FROM node@ AS js", 0, 16), 3); + + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS", 0, 5), 0); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS", 0, 7), 0); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS", 0, 9), 0); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS", 0, 10), 1); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS", 0, 13), 1); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS", 0, 16), 1); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS", 0, 17), 1); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS", 0, 35), 1); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS", 0, 80), 1); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS", 0, 81), 2); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS", 0, 82), 2); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS", 0, 83), 2); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS ", 0, 84), 3); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS js", 0, 5), 0); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS js", 0, 7), 0); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS js", 0, 9), 0); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS js", 0, 10), 1); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS js", 0, 13), 1); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS js", 0, 16), 1); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS js", 0, 17), 1); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS js", 0, 35), 1); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS js", 0, 80), 1); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS js", 0, 81), 2); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS js", 0, 82), 2); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS js", 0, 83), 2); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS js", 0, 84), 3); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS js", 0, 85), 3); + assertFrom_Digests_BuildStages_Only(compute("FROM node@sha256:61368522f65d01f2264bdd49b8a336488e14faf29f3ff9b6bf76a4da23c4700 AS js", 0, 86), 3); + }); + + it("invalid", function() { + assertNoSignatures(compute("FROM node AS js ", 0, 16)); + }); + }); + function testHealthcheck(trigger: boolean) { let onbuild = trigger ? "ONBUILD " : ""; let triggerOffset = trigger ? 8 : 0;