UNPKG

dockerfile-utils

Version:

Utilities for formatting and linting a Dockerfile.

250 lines (249 loc) 10.7 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.DockerFormatter = void 0; const vscode_languageserver_types_1 = require("vscode-languageserver-types"); const dockerfile_ast_1 = require("dockerfile-ast"); class DockerFormatter { getIndentation(formattingOptions) { let indentation = "\t"; if (formattingOptions && formattingOptions.insertSpaces) { indentation = ""; for (let i = 0; i < formattingOptions.tabSize; i++) { indentation = indentation + " "; } } return indentation; } /** * Creates a TextEdit for formatting the given document. * * @param document the document being formatted * @param start the start offset of the document's content to be replaced * @param end the end offset of the document's content to be replaced * @param indent true if this block should be replaced with an indentation, false otherwise * @param indentation the string to use for an indentation */ createFormattingEdit(document, start, end, indent, indentation) { if (indent) { return vscode_languageserver_types_1.TextEdit.replace({ start: document.positionAt(start), end: document.positionAt(end) }, indentation); } else { return vscode_languageserver_types_1.TextEdit.del({ start: document.positionAt(start), end: document.positionAt(end) }); } } formatOnType(document, position, ch, options) { const dockerfile = dockerfile_ast_1.DockerfileParser.parse(document.getText()); // check that the inserted character is the escape character if (dockerfile.getEscapeCharacter() === ch) { for (let comment of dockerfile.getComments()) { // ignore if we're in a comment if (comment.getRange().start.line === position.line) { return []; } } const directive = dockerfile.getDirective(); // ignore if we're in the parser directive if (directive && position.line === 0) { return []; } const content = document.getText(); validityCheck: for (let i = document.offsetAt(position); i < content.length; i++) { switch (content.charAt(i)) { case ' ': case '\t': break; case '\r': case '\n': break validityCheck; default: // not escaping a newline, no need to format the next line return []; } } const line = position.line + 1; const indentedLines = []; const skippedLines = []; indentedLines[line] = true; skippedLines[line] = true; const heredocLines = []; if (this.inHeredoc(dockerfile, line)) { heredocLines.push(line); } return this.formatLines(document, document.getText(), [line], indentedLines, skippedLines, heredocLines, options); } return []; } formatRange(document, range, options) { const lines = []; for (let i = range.start.line; i <= range.end.line; i++) { lines.push(i); } return this.format(document, lines, options); } formatDocument(document, options) { const lines = []; for (let i = 0; i < document.lineCount; i++) { lines.push(i); } return this.format(document, lines, options); } inHeredoc(dockerfile, line) { for (const instruction of dockerfile.getInstructions()) { if (instruction instanceof dockerfile_ast_1.Copy || instruction instanceof dockerfile_ast_1.Run) { const lines = this.getHeredocLines(instruction.getHeredocs()); if (lines.indexOf(line) !== -1) { return true; } } } return false; } getHeredocLines(heredocs) { let start = -1; for (let i = 0; i < heredocs.length; i++) { // if there's content, use the first line of the content const contentRange = heredocs[i].getContentRange(); if (contentRange !== null) { start = contentRange.start.line; break; } // there may be a delimiter even if there's no content const delimiterRange = heredocs[i].getDelimiterRange(); if (delimiterRange !== null) { start = delimiterRange.start.line; break; } } if (start === -1) { return []; } let end = -1; for (let i = heredocs.length - 1; i >= 0; i--) { // there may be a delimiter even if there's no content const delimiterRange = heredocs[i].getDelimiterRange(); if (delimiterRange !== null) { end = delimiterRange.end.line; break; } // if there's content, use the first line of the content const contentRange = heredocs[i].getContentRange(); if (contentRange !== null) { end = contentRange.end.line; break; } } let heredocLines = []; for (let i = start; i <= end; i++) { heredocLines.push(i); } return heredocLines; } /** * Formats the specified lines of the given document based on the * provided formatting options. * * @param document the text document to format * @param lines the lines to format * @param options the formatting options to use to perform the format * @return the text edits to apply to format the lines of the document */ format(document, lines, options) { let content = document.getText(); let dockerfile = dockerfile_ast_1.DockerfileParser.parse(content); const indentedLines = []; const skippedLines = []; const heredocLines = []; for (let i = 0; i < document.lineCount; i++) { indentedLines[i] = false; skippedLines[i] = false; } for (let instruction of dockerfile.getInstructions()) { let range = instruction.getRange(); if (range.start.line !== range.end.line) { for (let i = range.start.line + 1; i <= range.end.line; i++) { skippedLines[i] = true; } } if (instruction instanceof dockerfile_ast_1.Copy || instruction instanceof dockerfile_ast_1.Run) { const heredocs = instruction.getHeredocs(); if (heredocs.length > 0) { heredocLines.push(...this.getHeredocLines(heredocs)); } } indentedLines[range.start.line] = false; for (let i = range.start.line + 1; i <= range.end.line; i++) { indentedLines[i] = true; } } return this.formatLines(document, content, lines, indentedLines, skippedLines, heredocLines, options); } formatLines(document, content, lines, indentedLines, skippedLines, heredocLines, options) { const indentation = this.getIndentation(options); const edits = []; lineCheck: for (let i = 0; i < lines.length; i++) { if (options && options.ignoreMultilineInstructions && skippedLines[lines[i]]) { continue; } else if (heredocLines.indexOf(lines[i]) !== -1) { continue; } let startOffset = document.offsetAt(vscode_languageserver_types_1.Position.create(lines[i], 0)); for (let j = startOffset; j < content.length; j++) { switch (content.charAt(j)) { case ' ': case '\t': break; case '\r': case '\n': if (j !== startOffset) { // only whitespace on this line, trim it let edit = vscode_languageserver_types_1.TextEdit.del({ start: document.positionAt(startOffset), end: document.positionAt(j) }); edits.push(edit); } // process the next line continue lineCheck; default: // found a line that should be indented if (indentedLines[lines[i]]) { const originalIndentation = document.getText().substring(startOffset, j); // change the indentation if it's not what we expect if (originalIndentation !== indentation) { const edit = this.createFormattingEdit(document, startOffset, j, indentedLines[lines[i]], indentation); edits.push(edit); } } else if (j !== startOffset) { // non-whitespace character encountered, realign const edit = this.createFormattingEdit(document, startOffset, j, indentedLines[lines[i]], indentation); edits.push(edit); } // process the next line continue lineCheck; } } if (startOffset < content.length) { // only whitespace on the last line, trim it let edit = vscode_languageserver_types_1.TextEdit.del({ start: document.positionAt(startOffset), end: document.positionAt(content.length) }); edits.push(edit); } } return edits; } } exports.DockerFormatter = DockerFormatter;