dockerfile-utils
Version:
Utilities for formatting and linting a Dockerfile.
907 lines • 96.2 kB
JavaScript
"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