UNPKG

dockerfile-utils

Version:

Utilities for formatting and linting a Dockerfile.

907 lines 96.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Validator = exports.KEYWORDS = void 0; const vscode_languageserver_types_1 = require("vscode-languageserver-types"); const dockerfile_ast_1 = require("dockerfile-ast"); const main_1 = require("./main"); exports.KEYWORDS = [ "ADD", "ARG", "CMD", "COPY", "ENTRYPOINT", "ENV", "EXPOSE", "FROM", "HEALTHCHECK", "LABEL", "MAINTAINER", "ONBUILD", "RUN", "SHELL", "STOPSIGNAL", "USER", "VOLUME", "WORKDIR" ]; /** * A list of variables that are prepopulated when using the BuildKit * backend. */ const PREDEFINED_VARIABLES = [ "TARGETPLATFORM", "TARGETOS", "TARGETARCH", "TARGETVARIANT", "BUILDPLATFORM", "BUILDOS", "BUILDARCH", "BUILDVARIAN" ]; class Validator { constructor(settings) { this.settings = { deprecatedMaintainer: main_1.ValidationSeverity.WARNING, directiveCasing: main_1.ValidationSeverity.WARNING, emptyContinuationLine: main_1.ValidationSeverity.WARNING, instructionCasing: main_1.ValidationSeverity.WARNING, instructionCmdMultiple: main_1.ValidationSeverity.WARNING, instructionEntrypointMultiple: main_1.ValidationSeverity.WARNING, instructionHealthcheckMultiple: main_1.ValidationSeverity.WARNING, instructionJSONInSingleQuotes: main_1.ValidationSeverity.WARNING, instructionWorkdirRelative: main_1.ValidationSeverity.WARNING }; if (settings) { this.settings = settings; } } checkDirectives(dockerfile, problems) { const duplicatedEscapes = []; for (const directive of dockerfile.getDirectives()) { if (directive.getDirective() === dockerfile_ast_1.Directive.escape) { duplicatedEscapes.push(directive); } } if (duplicatedEscapes.length > 1) { // multiple escape parser directives have been found for (let i = 1; i < duplicatedEscapes.length; i++) { problems.push(Validator.createDuplicatedEscapeDirective(duplicatedEscapes[i].getNameRange().start, duplicatedEscapes[i].getValueRange().end)); } return; } for (const directive of dockerfile.getDirectives()) { const directiveName = directive.getDirective(); if (directiveName === dockerfile_ast_1.Directive.escape) { const value = directive.getValue(); if (value !== '\\' && value !== '`' && value !== "") { // if the directive's value is invalid or isn't the empty string, flag it const range = directive.getValueRange(); problems.push(Validator.createInvalidEscapeDirective(range.start, range.end, value)); } if (directive.getName() !== dockerfile_ast_1.Directive.escape) { const range = directive.getNameRange(); const diagnostic = this.createLowercaseDirective(range.start, range.end); if (diagnostic) { problems.push(diagnostic); } } } } } /** * Checks the arguments of the given instruction. * * @param instruction the instruction to validate * @param problems an array of identified problems in the document * @param expectedArgCount an array of expected number of arguments * for the instruction, if its length is 1 * and its value is -1, any number of * arguments greater than zero is valid * @param validate the function to use to validate an argument * @param createIncompleteDiagnostic the function to use to create a diagnostic * if the number of arguments is incorrect */ checkArguments(instruction, problems, expectedArgCount, validate, createIncompleteDiagnostic) { let args = instruction instanceof dockerfile_ast_1.PropertyInstruction ? instruction.getPropertyArguments() : instruction.getArguments(); if (args.length === 0) { if (instruction.getKeyword() !== dockerfile_ast_1.Keyword.RUN) { // all instructions are expected to have at least one argument const range = instruction.getInstructionRange(); problems.push(Validator.createMissingArgument(range.start.line, range.start, range.end)); } } else if (expectedArgCount[0] === -1) { for (let i = 0; i < args.length; i++) { let createInvalidDiagnostic = validate(i, args[i].getValue(), args[i].getRange()); if (createInvalidDiagnostic) { let range = args[i].getRange(); problems.push(createInvalidDiagnostic(instruction.getInstructionRange().start.line, range.start, range.end, args[i].getValue())); } } } else { for (let i = 0; i < expectedArgCount.length; i++) { if (expectedArgCount[i] === args.length) { for (let j = 0; j < args.length; j++) { let range = args[j].getRange(); let createInvalidDiagnostic = validate(j, args[j].getValue(), range); if (createInvalidDiagnostic instanceof Function) { problems.push(createInvalidDiagnostic(instruction.getInstructionRange().start.line, range.start, range.end, args[j].getValue())); } else if (createInvalidDiagnostic !== null) { problems.push(createInvalidDiagnostic); } } return; } } let range = args[args.length - 1].getRange(); if (createIncompleteDiagnostic) { problems.push(createIncompleteDiagnostic(instruction.getInstructionRange().start.line, range.start, range.end)); } else { problems.push(Validator.createExtraArgument(instruction.getInstructionRange().start.line, range.start, range.end)); } } } checkVariables(instruction, problems) { for (let variable of instruction.getVariables()) { let modifier = variable.getModifier(); if (modifier !== null) { switch (instruction.getKeyword()) { case dockerfile_ast_1.Keyword.CMD: case dockerfile_ast_1.Keyword.ENTRYPOINT: case dockerfile_ast_1.Keyword.RUN: // allow shell expansions to go through for RUN instructions break; default: if (modifier === "") { problems.push(Validator.createVariableUnsupportedModifier(instruction.getRange().start.line, variable.getRange(), variable.toString(), modifier)); } else if (modifier !== '+' && modifier !== '-' && modifier !== '?') { problems.push(Validator.createVariableUnsupportedModifier(instruction.getRange().start.line, variable.getModifierRange(), variable.toString(), modifier)); } break; } } } } checkProperty(document, escapeChar, keyword, instructionLine, property, firstProperty, optionalValue, problems) { let name = property.getName(); if (name === "") { let range = property.getRange(); problems.push(Validator.createSyntaxMissingNames(instructionLine, range.start, range.end, keyword)); } else if (name.indexOf('=') !== -1) { let nameRange = property.getNameRange(); let unescapedName = document.getText(nameRange); let index = unescapedName.indexOf('='); if (unescapedName.charAt(0) === '\'') { problems.push(Validator.createSyntaxMissingSingleQuote(instructionLine, nameRange.start, document.positionAt(document.offsetAt(nameRange.start) + index), unescapedName.substring(0, unescapedName.indexOf('=')))); } else if (unescapedName.charAt(0) === '"') { problems.push(Validator.createSyntaxMissingDoubleQuote(instructionLine, nameRange.start, document.positionAt(document.offsetAt(nameRange.start) + index), unescapedName.substring(0, unescapedName.indexOf('=')))); } return; } let value = property.getValue(); if (value === null) { if (!optionalValue) { let range = property.getNameRange(); if (firstProperty) { problems.push(Validator.createENVRequiresTwoArguments(instructionLine, range.start, range.end)); } else { problems.push(Validator.createSyntaxMissingEquals(instructionLine, range.start, range.end, name)); } } } else if (value.charAt(0) === '"') { let found = false; for (let i = 1; i < value.length; i++) { switch (value.charAt(i)) { case escapeChar: i++; break; case '"': if (i === value.length - 1) { found = true; } break; } } if (!found) { let range = property.getValueRange(); problems.push(Validator.createSyntaxMissingDoubleQuote(instructionLine, range.start, range.end, property.getUnescapedValue())); } } else if (value.charAt(0) === '\'' && value.charAt(value.length - 1) !== '\'') { let range = property.getValueRange(); problems.push(Validator.createSyntaxMissingSingleQuote(instructionLine, range.start, range.end, value)); } } validate(document) { this.document = document; let problems = []; let dockerfile = dockerfile_ast_1.DockerfileParser.parse(document.getText()); this.checkDirectives(dockerfile, problems); let instructions = dockerfile.getInstructions(); if (instructions.length === 0 || dockerfile.getARGs().length === instructions.length) { // no instructions in this file, or only ARGs problems.push(Validator.createNoSourceImage(document.positionAt(0), document.positionAt(0))); } let cmds = []; let entrypoints = []; let healthchecks = []; for (let instruction of instructions) { if (instruction instanceof dockerfile_ast_1.Cmd) { cmds.push(instruction); } else if (instruction instanceof dockerfile_ast_1.Entrypoint) { entrypoints.push(instruction); } else if (instruction instanceof dockerfile_ast_1.Healthcheck) { healthchecks.push(instruction); } else if (instruction instanceof dockerfile_ast_1.From) { this.createDuplicatesDiagnostics(problems, this.settings.instructionCmdMultiple, "CMD", cmds); this.createDuplicatesDiagnostics(problems, this.settings.instructionEntrypointMultiple, "ENTRYPOINT", entrypoints); this.createDuplicatesDiagnostics(problems, this.settings.instructionHealthcheckMultiple, "HEALTHCHECK", healthchecks); cmds = []; entrypoints = []; healthchecks = []; } } this.createDuplicatesDiagnostics(problems, this.settings.instructionCmdMultiple, "CMD", cmds); this.createDuplicatesDiagnostics(problems, this.settings.instructionEntrypointMultiple, "ENTRYPOINT", entrypoints); this.createDuplicatesDiagnostics(problems, this.settings.instructionHealthcheckMultiple, "HEALTHCHECK", healthchecks); this.createDuplicateBuildStageNameDiagnostics(problems, dockerfile.getFROMs()); let escapeChar = dockerfile.getEscapeCharacter(); let hasFrom = false; for (let instruction of dockerfile.getInstructions()) { let keyword = instruction.getKeyword(); if (keyword === "FROM") { hasFrom = true; } else if (!hasFrom && keyword !== "ARG") { // first non-ARG instruction is not a FROM let range = instruction.getInstructionRange(); problems.push(Validator.createNoSourceImage(range.start, range.end)); hasFrom = true; } this.validateInstruction(document, escapeChar, instruction, keyword, false, problems); this.checkVariables(instruction, problems); } for (let instruction of dockerfile.getOnbuildTriggers()) { this.validateInstruction(document, escapeChar, instruction, instruction.getKeyword(), true, problems); } const ignoredLines = []; for (const comment of dockerfile.getComments()) { if (comment.getContent() === "dockerfile-utils: ignore") { ignoredLines.push(comment.getRange().start.line); } } problemsCheck: for (let i = 0; i < problems.length; i++) { if (problems[i].instructionLine !== null) { for (const ignoredLine of ignoredLines) { if (ignoredLine + 1 === problems[i].instructionLine) { problems.splice(i, 1); i--; continue problemsCheck; } } } } return problems; } /** * Retrieves the line numbers that corresponds to the content of * here-documents in the given instruction. The line numbers are * zero-based. * * @param instruction the instruction to check * @returns an array of line numbers where content of * here-documents are defined */ getHeredocLines(instruction) { if (instruction instanceof dockerfile_ast_1.Copy || instruction instanceof dockerfile_ast_1.Run) { const lines = []; for (const heredoc of instruction.getHeredocs()) { const range = heredoc.getContentRange(); if (range !== null) { for (let i = range.start.line; i <= range.end.line; i++) { lines.push(i); } } } return lines; } return []; } validateInstruction(document, escapeChar, instruction, keyword, isTrigger, problems) { if (exports.KEYWORDS.indexOf(keyword) === -1) { let range = instruction.getInstructionRange(); // invalid instruction found problems.push(Validator.createUnknownInstruction(range.start.line, range.start, range.end, keyword)); } else { if (keyword !== instruction.getInstruction()) { let range = instruction.getInstructionRange(); // warn about uppercase convention if the keyword doesn't match the actual instruction let diagnostic = this.createUppercaseInstruction(range.start.line, range.start, range.end); if (diagnostic) { problems.push(diagnostic); } } if (keyword === "MAINTAINER") { let range = instruction.getInstructionRange(); let diagnostic = this.createMaintainerDeprecated(range.start.line, range.start, range.end); if (diagnostic) { problems.push(diagnostic); } } const fullRange = instruction.getRange(); if (fullRange.start.line !== fullRange.end.line && !isTrigger) { // if the instruction spans multiple lines, check for empty newlines const content = document.getText(); const endingLine = fullRange.end.line; const skippedLines = this.getHeredocLines(instruction); let skipIndex = 0; let start = -1; for (let i = fullRange.start.line; i <= endingLine; i++) { if (i === skippedLines[skipIndex]) { skipIndex++; continue; } const lineContent = content.substring(document.offsetAt(vscode_languageserver_types_1.Position.create(i, 0)), document.offsetAt(vscode_languageserver_types_1.Position.create(i + 1, 0))); if (lineContent.trim().length === 0) { if (start === -1) { start = i; continue; } } else if (start !== -1) { const diagnostic = Validator.createEmptyContinuationLine(vscode_languageserver_types_1.Position.create(start, 0), vscode_languageserver_types_1.Position.create(i, 0), this.settings.emptyContinuationLine); if (diagnostic) { problems.push(diagnostic); } start = -1; } } if (start !== -1) { const diagnostic = Validator.createEmptyContinuationLine(vscode_languageserver_types_1.Position.create(start, 0), vscode_languageserver_types_1.Position.create(endingLine + 1, 0), this.settings.emptyContinuationLine); if (diagnostic) { problems.push(diagnostic); } start = -1; } } switch (keyword) { case "CMD": this.checkJSONQuotes(instruction, problems); break; case "ENTRYPOINT": case "RUN": case "VOLUME": this.checkArguments(instruction, problems, [-1], function () { return null; }); this.checkJSONQuotes(instruction, problems); break; case "ARG": this.checkArguments(instruction, problems, [-1], () => null); let arg = instruction; let argProperty = arg.getProperty(); if (argProperty) { this.checkProperty(document, escapeChar, keyword, instruction.getRange().start.line, argProperty, true, true, problems); } break; case "ENV": case "LABEL": this.checkArguments(instruction, problems, [-1], function () { return null; }); let properties = instruction.getProperties(); if (properties.length === 1) { this.checkProperty(document, escapeChar, keyword, instruction.getRange().start.line, properties[0], true, false, problems); } else if (properties.length !== 0) { for (let property of properties) { this.checkProperty(document, escapeChar, keyword, instruction.getRange().start.line, property, false, false, problems); } } break; case "FROM": const fromInstructionRange = instruction.getInstructionRange(); const fromFlags = instruction.getFlags(); for (const flag of fromFlags) { const flagName = flag.getName(); if (flagName !== "platform") { const range = flag.getRange(); problems.push(Validator.createUnknownFromFlag(fromInstructionRange.start.line, range.start, flagName === "" ? range.end : flag.getNameRange().end, flag.getName())); } } this.checkFlagValue(fromInstructionRange.start.line, fromFlags, ["platform"], problems); this.checkArguments(instruction, problems, [1, 3], function (index, argument, range) { switch (index) { case 0: let variables = instruction.getVariables(); if (variables.length > 0) { let variableRange = variables[0].getRange(); if (variableRange.start.line === range.start.line && variableRange.start.character === range.start.character && variableRange.end.line === range.end.line && variableRange.end.character === range.end.character) { if (!variables[0].isDefined()) { if (PREDEFINED_VARIABLES.indexOf(variables[0].getName()) !== -1) { return null; } // the '-' sign suggests a default value so even if the variable is not defined it's okay if (variables[0].getModifier() === '-') { const parameter = variables[0].getSubstitutionParameter(); if (parameter !== "" && parameter !== null) { return null; } } return Validator.createBaseNameEmpty(fromInstructionRange.start.line, variableRange, variables[0].toString()); } } return null; } let from = instruction; let digestRange = from.getImageDigestRange(); const tagRange = from.getImageTagRange(); if (digestRange === null) { if (tagRange === null) { return null; } let tag = document.getText(tagRange); if (tag === "") { // no tag specified, just highlight the whole argument return Validator.createInvalidReferenceFormat(fromInstructionRange.start.line, range); } let tagRegexp = new RegExp(/^[\w][\w.-]{0,127}$/); if (tagRegexp.test(tag)) { return null; } return Validator.createInvalidReferenceFormat(fromInstructionRange.start.line, from.getImageTagRange()); } if (tagRange !== null) { // specified an optional tag with the digest if (tagRange.start.line === tagRange.end.line && tagRange.start.character === tagRange.end.character) { // tag is empty, flag the whole argument as an error return Validator.createInvalidReferenceFormat(fromInstructionRange.start.line, range); } } let digest = document.getText(digestRange); let algorithmIndex = digest.indexOf(':'); if (algorithmIndex === -1) { if (digest === "") { // no digest specified, just highlight the whole argument return Validator.createInvalidReferenceFormat(fromInstructionRange.start.line, range); } return Validator.createInvalidReferenceFormat(fromInstructionRange.start.line, from.getImageDigestRange()); } let algorithmRegexp = new RegExp(/[A-Fa-f0-9_+.-]+/); let algorithm = digest.substring(0, algorithmIndex); if (!algorithmRegexp.test(algorithm)) { return Validator.createInvalidReferenceFormat(fromInstructionRange.start.line, from.getImageDigestRange()); } let hex = digest.substring(algorithmIndex + 1); let hexRegexp = new RegExp(/[A-Fa-f0-9]+/); if (hexRegexp.test(hex)) { return null; } return Validator.createInvalidReferenceFormat(fromInstructionRange.start.line, from.getImageDigestRange()); case 1: return argument.toUpperCase() === "AS" ? null : Validator.createInvalidAs; case 2: argument = argument.toLowerCase(); let regexp = new RegExp(/^[a-z]([a-z0-9_\-.]*)*$/); if (regexp.test(argument)) { return null; } return Validator.createInvalidBuildStageName(fromInstructionRange.start.line, range, argument); ; default: return null; } }, Validator.createRequiresOneOrThreeArguments); break; case "HEALTHCHECK": let args = instruction.getArguments(); const healthcheckInstructionRange = instruction.getInstructionRange(); const healthcheckFlags = instruction.getFlags(); if (args.length === 0) { // all instructions are expected to have at least one argument problems.push(Validator.createHEALTHCHECKRequiresAtLeastOneArgument(healthcheckInstructionRange.start.line, healthcheckInstructionRange)); } else { const value = args[0].getValue(); const uppercase = value.toUpperCase(); if (uppercase === "NONE") { // check that NONE doesn't have any arguments after it if (args.length > 1) { // get the next argument const start = args[1].getRange().start; // get the last argument const end = args[args.length - 1].getRange().end; // highlight everything after the NONE and warn the user problems.push(Validator.createHealthcheckNoneUnnecessaryArgument(healthcheckInstructionRange.start.line, start, end)); } // don't need to validate flags of a NONE break; } else if (uppercase === "CMD") { if (args.length === 1) { // this HEALTHCHECK has a CMD with no arguments const range = args[0].getRange(); problems.push(Validator.createHealthcheckCmdArgumentMissing(healthcheckInstructionRange.start.line, range.start, range.end)); } } else { // unknown HEALTHCHECK type problems.push(Validator.createHealthcheckTypeUnknown(healthcheckInstructionRange.start.line, args[0].getRange(), uppercase)); } } const validFlags = ["interval", "retries", "start-period", "timeout", "start-interval"]; for (const flag of healthcheckFlags) { const flagName = flag.getName(); if (validFlags.indexOf(flagName) === -1) { const range = flag.getRange(); problems.push(Validator.createUnknownHealthcheckFlag(healthcheckInstructionRange.start.line, range.start, flagName === "" ? range.end : flag.getNameRange().end, flag.getName())); } else if (flagName === "retries") { const value = flag.getValue(); if (value) { const valueRange = flag.getValueRange(); const integer = parseInt(value); // test for NaN or numbers with decimals if (isNaN(integer) || value.indexOf('.') !== -1) { problems.push(Validator.createInvalidSyntax(healthcheckInstructionRange.start.line, valueRange.start, valueRange.end, value)); } else if (integer < 1) { problems.push(Validator.createFlagAtLeastOne(healthcheckInstructionRange.start.line, valueRange.start, valueRange.end, "--retries", integer.toString())); } } } } this.checkFlagValue(healthcheckInstructionRange.start.line, healthcheckFlags, validFlags, problems); this.checkFlagDuration(healthcheckInstructionRange.start.line, healthcheckFlags, ["interval", "start-period", "timeout", "start-interval"], problems); this.checkDuplicateFlags(healthcheckInstructionRange.start.line, healthcheckFlags, validFlags, problems); break; case "ONBUILD": this.checkArguments(instruction, problems, [-1], function () { return null; }); let onbuild = instruction; let trigger = onbuild.getTrigger(); switch (trigger) { case "FROM": case "MAINTAINER": problems.push(Validator.createOnbuildTriggerDisallowed(onbuild.getInstructionRange().start.line, onbuild.getTriggerRange(), trigger)); break; case "ONBUILD": problems.push(Validator.createOnbuildChainingDisallowed(onbuild.getInstructionRange().start.line, onbuild.getTriggerRange())); break; } break; case "SHELL": this.checkArguments(instruction, problems, [-1], function () { return null; }); this.checkJSON(document, instruction, problems); break; case "STOPSIGNAL": this.checkArguments(instruction, problems, [1], function (_index, argument) { if (argument.indexOf("SIG") === 0 || argument.indexOf('$') != -1) { return null; } for (var i = 0; i < argument.length; i++) { if ('0' > argument.charAt(i) || '9' < argument.charAt(i)) { return Validator.createInvalidStopSignal; } } return null; }); let stopsignalArgs = instruction.getExpandedArguments(); if (stopsignalArgs.length === 1) { let value = stopsignalArgs[0].getValue(); let variables = instruction.getVariables(); if (variables.length === 0) { if (value.indexOf('$') !== -1) { const instructionRange = instruction.getInstructionRange(); let range = stopsignalArgs[0].getRange(); problems.push(Validator.createInvalidStopSignal(instructionRange.start.line, range.start, range.end, value)); } } else { for (let variable of variables) { let variableRange = variable.getRange(); let variableDefinition = this.document.getText().substring(this.document.offsetAt(variableRange.start), this.document.offsetAt(variableRange.end)); // an un-expanded variable is here if (value.includes(variableDefinition) && !variable.isBuildVariable() && !variable.isDefined()) { const instructionRange = instruction.getInstructionRange(); let range = stopsignalArgs[0].getRange(); problems.push(Validator.createInvalidStopSignal(instructionRange.start.line, range.start, range.end, "")); break; } } } } break; case "EXPOSE": let exposeArgs = instruction.getArguments(); let exposeExpandedArgs = instruction.getExpandedArguments(); if (exposeExpandedArgs.length === 0) { let range = instruction.getInstructionRange(); problems.push(Validator.createMissingArgument(range.start.line, range.start, range.end)); } else { const regex = /^([0-9])+(-[0-9]+)?(:([0-9])+(-[0-9]*)?)?(\/(\w*))?(\/\w*)*$/; argCheck: for (let i = 0; i < exposeExpandedArgs.length; i++) { let value = exposeExpandedArgs[i].getValue(); if (value.charAt(0) === '"' && value.charAt(value.length - 1) === '"') { value = value.substring(1, value.length - 1); } const match = regex.exec(value); if (match) { if (match[7]) { const protocol = match[7].toLowerCase(); if (protocol !== "" && protocol !== "tcp" && protocol !== "udp" && protocol !== "sctp") { const range = exposeExpandedArgs[i].getRange(); const rangeStart = this.document.offsetAt(range.start); const rawArg = this.document.getText().substring(rangeStart, this.document.offsetAt(range.end)); const start = rangeStart + rawArg.indexOf(match[7].substring(0, 1)); const end = protocol.length === 1 ? rangeStart + start + 1 : rangeStart + rawArg.length; problems.push(Validator.createInvalidProto(instruction.getInstructionRange().start.line, this.document.positionAt(start), this.document.positionAt(end), match[7])); } } } else { // see if we're referencing a variable here if (value.charAt(0) === '$') { continue argCheck; } problems.push(Validator.createInvalidPort(instruction.getInstructionRange().start.line, exposeExpandedArgs[i].getRange(), value)); } } } break; case "ADD": const add = instruction; const addFlags = add.getFlags(); const addInstructionRange = instruction.getInstructionRange(); for (let flag of addFlags) { const name = flag.getName(); const flagRange = flag.getRange(); if (name === "") { problems.push(Validator.createUnknownAddFlag(addInstructionRange.start.line, flagRange.start, flagRange.end, name)); } else if (name === "link" || name === "keep-git-dir") { const problem = this.checkFlagBoolean(addInstructionRange.start.line, flag); if (problem !== null) { problems.push(problem); } } else if (name !== "chmod" && name !== "chown" && name !== "checksum" && name !== "exclude") { let range = flag.getNameRange(); problems.push(Validator.createUnknownAddFlag(addInstructionRange.start.line, flagRange.start, range.end, name)); } } const addDestinationDiagnostic = this.checkDestinationIsDirectory(add, Validator.createADDRequiresAtLeastTwoArguments, Validator.createADDDestinationNotDirectory); if (addDestinationDiagnostic !== null) { problems.push(addDestinationDiagnostic); } this.checkFlagValue(addInstructionRange.start.line, addFlags, ["chmod", "chown", "checksum", "exclude"], problems); this.checkDuplicateFlags(addInstructionRange.start.line, addFlags, ["chmod", "chown", "checksum", "keep-git-dir", "link"], problems); this.checkJSONQuotes(instruction, problems); break; case "COPY": const copyInstructionRange = instruction.getInstructionRange(); let copy = instruction; let flags = copy.getFlags(); if (flags.length > 0) { for (let flag of flags) { const name = flag.getName(); const flagRange = flag.getRange(); if (name === "") { problems.push(Validator.createUnknownCopyFlag(copyInstructionRange.start.line, flagRange.start, flagRange.end, name)); } else if (name === "link" || name === "parents") { const problem = this.checkFlagBoolean(copyInstructionRange.start.line, flag); if (problem !== null) { problems.push(problem); } } else if (name !== "chmod" && name !== "chown" && name !== "from" && name !== "exclude" && name !== "parents") { let range = flag.getNameRange(); problems.push(Validator.createUnknownCopyFlag(copyInstructionRange.start.line, flagRange.start, range.end, name)); } } let flag = copy.getFromFlag(); if (flag) { let value = flag.getValue(); if (value !== null) { let regexp = new RegExp(/^[a-zA-Z0-9].*$/); if (!regexp.test(value)) { let range = value === "" ? flag.getRange() : flag.getValueRange(); problems.push(Validator.createFlagInvalidFrom(copyInstructionRange.start.line, range.start, range.end, value)); } } } } const copyDestinationDiagnostic = this.checkDestinationIsDirectory(copy, Validator.createCOPYRequiresAtLeastTwoArguments, Validator.createCOPYDestinationNotDirectory); if (copyDestinationDiagnostic !== null) { problems.push(copyDestinationDiagnostic); } this.checkFlagValue(copyInstructionRange.start.line, flags, ["chmod", "chown", "from", "exclude"], problems); this.checkDuplicateFlags(copyInstructionRange.start.line, flags, ["chmod", "chown", "from", "link", "parents"], problems); this.checkJSONQuotes(instruction, problems); break; case "WORKDIR": this.checkArguments(instruction, problems, [-1], function () { return null; }); let content = instruction.getArgumentsContent(); if (content) { // strip out any surrounding quotes const first = content.substring(0, 1); const last = content.substring(content.length - 1); if ((first === '\'' && last === '\'') || (first === '"' && last === '"')) { content = content.substring(1, content.length - 1); } let regexp = new RegExp(/^(\$|([a-zA-Z](\$|:(\$|\\|\/)))).*$/); if (!content.startsWith('/') && !regexp.test(content)) { let problem = this.createWORKDIRNotAbsolute(instruction.getInstructionRange().start.line, instruction.getArgumentsRange()); if (problem) { problems.push(problem); } } } break; default: this.checkArguments(instruction, problems, [-1], function () { return null; }); break; } } } hasHeredocs(args) { for (const arg of args) { if (arg.getValue().startsWith("<<")) { return true; } } return false; } getDestinationArgument(args) { if (this.hasHeredocs(args)) { const initialLine = args[0].getRange().start.line; let candidate = null; for (let i = 1; i < args.length; i++) { if (args[i].getRange().start.line === initialLine) { candidate = args[i]; } else { // stop searching once we're on another line break; } } return candidate; } return args[args.length - 1]; } checkDestinationIsDirectory(instruction, requiresTwoArgumentsFunction, notDirectoryFunction) { if (instruction.getClosingBracket()) { return this.checkJsonDestinationIsDirectory(instruction, requiresTwoArgumentsFunction, notDirectoryFunction); } const args = instruction.getArguments(); if (args.length === 1) { return requiresTwoArgumentsFunction(instruction.getInstructionRange().start.line, args[0].getRange()); } else if (args.length === 0) { const instructionRange = instruction.getInstructionRange(); return requiresTwoArgumentsFunction(instructionRange.start.line, instructionRange); } else if (args.length > 2) { const lastArg = this.getDestinationArgument(args); if (lastArg === null) { const instructionRange = instruction.getInstructionRange(); return requiresTwoArgumentsFunction(instructionRange.start.line, instructionRange); } else if (this.hasHeredocs(args)) { return null; } const variables = instruction.getVariables(); if (variables.length !== 0) { const lastJsonStringOffset = this.document.offsetAt(lastArg.getRange().end); const lastVarOffset = this.document.offsetAt(variables[variables.length - 1].getRange().end); if (lastJsonStringOffset === lastVarOffset || lastJsonStringOffset - 1 === lastVarOffset) { return null; } } const destination = lastArg.getValue(); const lastChar = destination.charAt(destination.length - 1); if (lastChar !== '\\' && lastChar !== '/') { return notDirectoryFunction(instruction.getInstructionRange().start.line, lastArg.getRange()); } } return null; } createDuplicatesDiagnostics(problems, severity, instruction, instructions) { if (instructions.length > 1) { // decrement length by 1 because we want to ignore the last one for (let i = 0; i < instructions.length - 1; i++) { const instructionRange = instructions[i].getInstructionRange(); const diagnostic = this.createMultipleInstructions(instructionRange.start.line, instructionRange, severity, instruction); if (diagnostic) { problems.push(diagnostic); } } } } createDuplicateBuildStageNameDiagnostics(problems, froms) { const names = {}; for (let from of froms) { let name = from.getBuildStage(); if (name !== null) { name = name.toLowerCase(); if (names[name] === undefined) { names[name] = [from]; } else { names[name].push(from); } } } for (const name in names) { // duplicates found if (names[name].length > 1) { for (const from of names[name]) { problems.push(Validator.createDuplicateBuildStageName(from.getInstructionRange().start.line, from.getBuildStageRange(), name)); } } } } checkJsonDestinationIsDirectory(instruction, requiresTwoArgumentsFunction, notDirectoryFunction) { const jsonStrings = instruction.getJSONStrings(); if (jsonStrings.length === 0) { return requiresTwoArgumentsFunction(instruction.getInstructionRange().start.line, instruction.getArgumentsRange()); } else if (jsonStrings.length === 1) { return requiresTwoArgumentsFunction(instruction.getInstructionRange().start.line, jsonStrings[0].getJSONRange()); } else if (jsonStrings.length > 2) { const lastJsonString = jsonStrings[jsonStrings.length - 1]; const variables = instruction.getVariables(); if (variables.length !== 0) { const lastVar = variables[variables.length - 1]; const lastJsonStringOffset = this.document.offsetAt(lastJsonString.getRange().end); const lastVarOffset = this.document.offsetAt(lastVar.getRange().end); if (lastJsonStringOffset === lastVarOffset || lastJsonStringOffset - 1 === lastVarOffset) { return null; } } const destination = lastJsonString.getValue(); const lastChar = destination.charAt(destination.length - 2); if (lastChar !== '\\' && lastChar !== '/') { return notDirectoryFunction(instruction.getInstructionRange().start.line, jsonStrings[jsonStrings.length - 1].getJSONRange()); } } return null; } checkFlagValue(instructionLine, flags, validFlagNames, problems) { for (let flag of flags) { let flagName = flag.getName(); // only validate flags with the right name if (flag.getValue() === null && validFlagNames.indexOf(flagName) !== -1) { problems.push(Validator.createFlagMissingValue(instructionLine, flag.getNameRange(), flagName)); } } } /** * Checks that the given boolean flag is valid. A boolean flag should * either have no value defined (--flag) or the value should * c