UNPKG

dockerfile-ast

Version:

Parse a Dockerfile into an array of instructions and comments.

445 lines (444 loc) 20.9 kB
/* -------------------------------------------------------------------------------------------- * Copyright (c) Remy Suen. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.Parser = void 0; const vscode_languageserver_textdocument_1 = require("vscode-languageserver-textdocument"); const vscode_languageserver_types_1 = require("vscode-languageserver-types"); const comment_1 = require("./comment"); const parserDirective_1 = require("./parserDirective"); const instruction_1 = require("./instruction"); const add_1 = require("./instructions/add"); const arg_1 = require("./instructions/arg"); const cmd_1 = require("./instructions/cmd"); const copy_1 = require("./instructions/copy"); const env_1 = require("./instructions/env"); const entrypoint_1 = require("./instructions/entrypoint"); const from_1 = require("./instructions/from"); const healthcheck_1 = require("./instructions/healthcheck"); const label_1 = require("./instructions/label"); const onbuild_1 = require("./instructions/onbuild"); const run_1 = require("./instructions/run"); const shell_1 = require("./instructions/shell"); const stopsignal_1 = require("./instructions/stopsignal"); const workdir_1 = require("./instructions/workdir"); const user_1 = require("./instructions/user"); const volume_1 = require("./instructions/volume"); const dockerfile_1 = require("./dockerfile"); const util_1 = require("./util"); const main_1 = require("./main"); class Parser { constructor() { this.escapeChar = null; } static createInstruction(document, dockerfile, escapeChar, lineRange, instruction, instructionRange) { switch (instruction.toUpperCase()) { case "ADD": return new add_1.Add(document, lineRange, dockerfile, escapeChar, instruction, instructionRange); case "ARG": return new arg_1.Arg(document, lineRange, dockerfile, escapeChar, instruction, instructionRange); case "CMD": return new cmd_1.Cmd(document, lineRange, dockerfile, escapeChar, instruction, instructionRange); case "COPY": return new copy_1.Copy(document, lineRange, dockerfile, escapeChar, instruction, instructionRange); case "ENTRYPOINT": return new entrypoint_1.Entrypoint(document, lineRange, dockerfile, escapeChar, instruction, instructionRange); case "ENV": return new env_1.Env(document, lineRange, dockerfile, escapeChar, instruction, instructionRange); case "FROM": return new from_1.From(document, lineRange, dockerfile, escapeChar, instruction, instructionRange); case "HEALTHCHECK": return new healthcheck_1.Healthcheck(document, lineRange, dockerfile, escapeChar, instruction, instructionRange); case "LABEL": return new label_1.Label(document, lineRange, dockerfile, escapeChar, instruction, instructionRange); case "ONBUILD": return new onbuild_1.Onbuild(document, lineRange, dockerfile, escapeChar, instruction, instructionRange); case "RUN": return new run_1.Run(document, lineRange, dockerfile, escapeChar, instruction, instructionRange); case "SHELL": return new shell_1.Shell(document, lineRange, dockerfile, escapeChar, instruction, instructionRange); case "STOPSIGNAL": return new stopsignal_1.Stopsignal(document, lineRange, dockerfile, escapeChar, instruction, instructionRange); case "WORKDIR": return new workdir_1.Workdir(document, lineRange, dockerfile, escapeChar, instruction, instructionRange); case "USER": return new user_1.User(document, lineRange, dockerfile, escapeChar, instruction, instructionRange); case "VOLUME": return new volume_1.Volume(document, lineRange, dockerfile, escapeChar, instruction, instructionRange); } return new instruction_1.Instruction(document, lineRange, dockerfile, escapeChar, instruction, instructionRange); } getParserDirectives(document, buffer) { // reset the escape directive in between runs const directives = []; this.escapeChar = ''; const offset = util_1.Util.isUTF8BOM(buffer.substring(0, 1)) ? 1 : 0; directiveCheck: for (let i = offset; i < buffer.length; i++) { switch (buffer.charAt(i)) { case ' ': case '\t': break; case '\r': case '\n': // blank lines stop the parsing of directives immediately break directiveCheck; case '#': let directiveStart = -1; let directiveEnd = -1; for (let j = i + 1; j < buffer.length; j++) { let char = buffer.charAt(j); switch (char) { case ' ': case '\t': if (directiveStart !== -1 && directiveEnd === -1) { directiveEnd = j; } break; case '\r': case '\n': break directiveCheck; case '=': let valueStart = -1; let valueEnd = -1; if (directiveEnd === -1) { directiveEnd = j; } // assume the line ends with the file let lineEnd = buffer.length; directiveValue: for (let k = j + 1; k < buffer.length; k++) { char = buffer.charAt(k); switch (char) { case '\r': case '\n': if (valueStart !== -1 && valueEnd === -1) { valueEnd = k; } // line break found, reset lineEnd = k; break directiveValue; case '\t': case ' ': if (valueStart !== -1 && valueEnd === -1) { valueEnd = k; } continue; default: if (valueStart === -1) { valueStart = k; } break; } } if (directiveStart === -1) { // no directive, it's a regular comment break directiveCheck; } if (valueStart === -1) { // no non-whitespace characters found, highlight all the characters then valueStart = j + 1; valueEnd = lineEnd; } else if (valueEnd === -1) { // reached EOF valueEnd = buffer.length; } const lineRange = vscode_languageserver_types_1.Range.create(document.positionAt(i), document.positionAt(lineEnd)); const nameRange = vscode_languageserver_types_1.Range.create(document.positionAt(directiveStart), document.positionAt(directiveEnd)); const valueRange = vscode_languageserver_types_1.Range.create(document.positionAt(valueStart), document.positionAt(valueEnd)); directives.push(new parserDirective_1.ParserDirective(document, lineRange, nameRange, valueRange)); directiveStart = -1; if (buffer.charAt(valueEnd) === '\r') { // skip over the \r i = valueEnd + 1; } else { i = valueEnd; } continue directiveCheck; default: if (directiveStart === -1) { directiveStart = j; } break; } } break; default: break directiveCheck; } } return directives; } parse(buffer) { this.document = vscode_languageserver_textdocument_1.TextDocument.create("", "", 0, buffer); this.buffer = buffer; let dockerfile = new dockerfile_1.Dockerfile(this.document); let directives = this.getParserDirectives(this.document, this.buffer); let offset = 0; this.escapeChar = '\\'; if (directives.length > 0) { dockerfile.setDirectives(directives); this.escapeChar = dockerfile.getEscapeCharacter(); // start parsing after the directives offset = this.document.offsetAt(vscode_languageserver_types_1.Position.create(directives.length, 0)); } else if (util_1.Util.isUTF8BOM(buffer.substring(0, 1))) { offset = 1; } for (let i = offset; i < this.buffer.length; i++) { const char = this.buffer.charAt(i); switch (char) { case ' ': case '\t': case '\r': case '\n': break; case '#': i = this.processComment(dockerfile, i); break; default: i = this.processInstruction(dockerfile, char, i); break; } } dockerfile.organizeComments(); return dockerfile; } processInstruction(dockerfile, char, start) { let instruction = char; let instructionEnd = -1; let escapedInstruction = false; instructionCheck: for (let i = start + 1; i < this.buffer.length; i++) { char = this.buffer.charAt(i); switch (char) { case this.escapeChar: escapedInstruction = true; char = this.buffer.charAt(i + 1); if (char === '\r' || char === '\n') { if (instructionEnd === -1) { instructionEnd = i; } i++; } else if (char === ' ' || char === '\t') { for (let j = i + 2; j < this.buffer.length; j++) { switch (this.buffer.charAt(j)) { case ' ': case '\t': break; case '\r': case '\n': i = j; continue instructionCheck; default: // found an argument, mark end of instruction instructionEnd = i + 1; instruction = instruction + this.escapeChar; i = j - 2; continue instructionCheck; } } // reached EOF instructionEnd = i + 1; instruction = instruction + this.escapeChar; break instructionCheck; } else { instructionEnd = i + 1; instruction = instruction + this.escapeChar; // reset and consider it as one contiguous word escapedInstruction = false; } break; case ' ': case '\t': if (escapedInstruction) { // on an escaped newline, need to search for non-whitespace escapeCheck: for (let j = i + 1; j < this.buffer.length; j++) { switch (this.buffer.charAt(j)) { case ' ': case '\t': break; case '\r': case '\n': i = j; continue instructionCheck; default: break escapeCheck; } } escapedInstruction = false; } if (instructionEnd === -1) { instructionEnd = i; } i = this.processArguments(dockerfile, instruction, instructionEnd, start, i); dockerfile.addInstruction(this.createInstruction(dockerfile, instruction, start, instructionEnd, i)); return i; case '\r': case '\n': if (escapedInstruction) { continue; } if (instructionEnd === -1) { instructionEnd = i; } dockerfile.addInstruction(this.createInstruction(dockerfile, instruction, start, i, i)); return i; case '#': if (escapedInstruction) { continue; } default: instructionEnd = i + 1; instruction = instruction + char; escapedInstruction = false; break; } } // reached EOF if (instructionEnd === -1) { instructionEnd = this.buffer.length; } dockerfile.addInstruction(this.createInstruction(dockerfile, instruction, start, instructionEnd, this.buffer.length)); return this.buffer.length; } parseHeredocName(value) { value = value.substring(2); if (value.charAt(0) === '-') { value = value.substring(1); } if (value.charAt(0) === '"' || value.charAt(0) === '\'') { return value.substring(1, value.length - 1); } return value; } processHeredocs(instruction, offset) { let keyword = instruction.getKeyword(); if (keyword === main_1.Keyword.ONBUILD) { instruction = instruction.getTriggerInstruction(); if (instruction === null) { return offset; } keyword = instruction.getKeyword(); } if (keyword !== main_1.Keyword.ADD && keyword !== main_1.Keyword.COPY && keyword !== main_1.Keyword.RUN) { return offset; } const heredocs = []; for (const arg of instruction.getArguments()) { const value = arg.getValue(); if (value.startsWith("<<") && value.length > 2) { heredocs.push(this.parseHeredocName(value)); } } if (heredocs.length > 0) { for (const heredoc of heredocs) { offset = this.parseHeredoc(heredoc, offset); } } return offset; } processArguments(dockerfile, instruction, instructionEnd, start, offset) { let escaped = false; argumentsCheck: for (let i = offset + 1; i < this.buffer.length; i++) { switch (this.buffer.charAt(i)) { case '\r': case '\n': if (escaped) { continue; } return this.processHeredocs(this.createInstruction(dockerfile, instruction, start, instructionEnd, i), i); case this.escapeChar: const next = this.buffer.charAt(i + 1); if (next === '\n' || next === '\r') { escaped = true; i++; } else if (next === ' ' || next === '\t') { for (let j = i + 2; j < this.buffer.length; j++) { switch (this.buffer.charAt(j)) { case ' ': case '\t': break; case '\r': case '\n': escaped = true; default: i = j; continue argumentsCheck; } } // reached EOF return this.buffer.length; } continue; case '#': if (escaped) { i = this.processComment(dockerfile, i); continue argumentsCheck; } break; case ' ': case '\t': break; default: if (escaped) { escaped = false; } break; } } return this.buffer.length; } processComment(dockerfile, start) { let end = this.buffer.length; commentLoop: for (let i = start + 1; i < this.buffer.length; i++) { switch (this.buffer.charAt(i)) { case '\r': case '\n': end = i; break commentLoop; } } const range = vscode_languageserver_types_1.Range.create(this.document.positionAt(start), this.document.positionAt(end)); dockerfile.addComment(new comment_1.Comment(this.document, range)); return end; } parseHeredoc(heredocName, offset) { let startWord = -1; let lineStart = true; for (let i = offset; i < this.buffer.length; i++) { switch (this.buffer.charAt(i)) { case ' ': case '\t': lineStart = false; break; case '\r': case '\n': if (startWord !== -1 && heredocName === this.buffer.substring(startWord, i)) { return i; } startWord = -1; lineStart = true; break; default: if (lineStart) { startWord = i; lineStart = false; } break; } } return this.buffer.length; } createInstruction(dockerfile, instruction, start, instructionEnd, end) { const startPosition = this.document.positionAt(start); const instructionRange = vscode_languageserver_types_1.Range.create(startPosition, this.document.positionAt(instructionEnd)); const lineRange = vscode_languageserver_types_1.Range.create(startPosition, this.document.positionAt(end)); return Parser.createInstruction(this.document, dockerfile, this.escapeChar, lineRange, instruction, instructionRange); } } exports.Parser = Parser;