dockerfile-utils
Version:
Utilities for formatting and linting a Dockerfile.
250 lines (249 loc) • 10.7 kB
JavaScript
/* --------------------------------------------------------------------------------------------
* Copyright (c) Remy Suen. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
;
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;