dockerfile-language-service
Version:
A language service for Dockerfiles to enable the creation of feature-rich Dockerfile editors.
665 lines (664 loc) • 38.2 kB
JavaScript
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports);
if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "vscode-languageserver-textdocument", "vscode-languageserver-types", "dockerfile-ast", "./docker"], factory);
}
})(function (require, exports) {
/* --------------------------------------------------------------------------------------------
* 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.DockerSemanticTokens = exports.TokensLegend = void 0;
var vscode_languageserver_textdocument_1 = require("vscode-languageserver-textdocument");
var vscode_languageserver_types_1 = require("vscode-languageserver-types");
var dockerfile_ast_1 = require("dockerfile-ast");
var docker_1 = require("./docker");
var TokensLegend = /** @class */ (function () {
function TokensLegend() {
}
TokensLegend.init = function () {
var counter = 0;
this.tokenTypes[vscode_languageserver_types_1.SemanticTokenTypes.keyword] = counter++; // 0
this.tokenTypes[vscode_languageserver_types_1.SemanticTokenTypes.comment] = counter++; // 1
this.tokenTypes[vscode_languageserver_types_1.SemanticTokenTypes.parameter] = counter++; // 2
this.tokenTypes[vscode_languageserver_types_1.SemanticTokenTypes.property] = counter++; // 3
this.tokenTypes[vscode_languageserver_types_1.SemanticTokenTypes.namespace] = counter++; // 4
this.tokenTypes[vscode_languageserver_types_1.SemanticTokenTypes.class] = counter++; // 5
this.tokenTypes[vscode_languageserver_types_1.SemanticTokenTypes.macro] = counter++; // 6
this.tokenTypes[vscode_languageserver_types_1.SemanticTokenTypes.string] = counter++; // 7
this.tokenTypes[vscode_languageserver_types_1.SemanticTokenTypes.variable] = counter++; // 8
this.tokenTypes[vscode_languageserver_types_1.SemanticTokenTypes.operator] = counter++; // 9
this.tokenTypes[vscode_languageserver_types_1.SemanticTokenTypes.modifier] = counter++; // 10
this.tokenModifiers[vscode_languageserver_types_1.SemanticTokenModifiers.declaration] = 1;
this.tokenModifiers[vscode_languageserver_types_1.SemanticTokenModifiers.definition] = 2;
this.tokenModifiers[vscode_languageserver_types_1.SemanticTokenModifiers.deprecated] = 4;
};
TokensLegend.getTokenType = function (type) {
var tokenType = this.tokenTypes[type];
return tokenType;
};
TokensLegend.getTokenModifiers = function (modifiers) {
var bit = 0;
for (var _i = 0, modifiers_1 = modifiers; _i < modifiers_1.length; _i++) {
var modifier = modifiers_1[_i];
bit |= this.tokenModifiers[modifier];
}
return bit;
};
TokensLegend.tokenTypes = {};
TokensLegend.tokenModifiers = {};
return TokensLegend;
}());
exports.TokensLegend = TokensLegend;
TokensLegend.init();
var DockerSemanticTokens = /** @class */ (function () {
function DockerSemanticTokens(content) {
this.currentRange = null;
this.tokens = [];
this.quote = null;
this.escapedQuote = null;
this.content = content;
this.document = vscode_languageserver_textdocument_1.TextDocument.create("", "", 0, content);
this.dockerfile = dockerfile_ast_1.DockerfileParser.parse(content);
this.escapeCharacter = this.dockerfile.getEscapeCharacter();
}
DockerSemanticTokens.prototype.computeSemanticTokens = function () {
var lines = this.dockerfile.getComments();
var instructions = this.dockerfile.getInstructions();
for (var _i = 0, instructions_1 = instructions; _i < instructions_1.length; _i++) {
var instruction = instructions_1[_i];
var range = instruction.getRange();
if (range.start.line !== range.end.line) {
for (var i = 0; i < lines.length; i++) {
var commentRange = lines[i].getRange();
if (range.start.line < commentRange.start.line && commentRange.start.line < range.end.line) {
// this is an embedded comment, remove it
lines.splice(i, 1);
i--;
}
}
}
}
lines = lines.concat(this.dockerfile.getInstructions());
lines.sort(function (a, b) {
return a.getRange().start.line - b.getRange().start.line;
});
for (var _a = 0, _b = this.dockerfile.getDirectives(); _a < _b.length; _a++) {
var directive = _b[_a];
var range = directive.getRange();
var nameRange = directive.getNameRange();
var prefixRange = { start: range.start, end: nameRange.start };
this.createToken(null, prefixRange, vscode_languageserver_types_1.SemanticTokenTypes.comment, [], false);
this.createToken(null, nameRange, vscode_languageserver_types_1.SemanticTokenTypes.property, [], false);
var valueRange = directive.getValueRange();
var operatorRange = {
start: { character: valueRange.start.character - 1, line: valueRange.start.line },
end: { character: valueRange.start.character, line: valueRange.start.line },
};
this.createToken(null, operatorRange, vscode_languageserver_types_1.SemanticTokenTypes.operator, [], false);
if (valueRange.start.character !== valueRange.end.character) {
this.createToken(null, valueRange, vscode_languageserver_types_1.SemanticTokenTypes.parameter, [], false);
}
}
for (var i = 0; i < lines.length; i++) {
if (lines[i] instanceof dockerfile_ast_1.Comment) {
var range = lines[i].getRange();
this.createToken(null, range, vscode_languageserver_types_1.SemanticTokenTypes.comment, [], false);
}
else {
// trailing open quotes should not cause subsequent argument parameters to be flagged as strings
this.quote = null;
this.escapedQuote = null;
this.createTokensForInstruction(lines[i]);
}
}
return {
data: this.tokens
};
};
DockerSemanticTokens.prototype.createTokensForInstruction = function (instruction) {
var instructionRange = instruction.getInstructionRange();
var modifiers = [];
if (instruction.getKeyword() === dockerfile_ast_1.Keyword.MAINTAINER) {
modifiers = [vscode_languageserver_types_1.SemanticTokenModifiers.deprecated];
}
this.createToken(instruction, instructionRange, vscode_languageserver_types_1.SemanticTokenTypes.keyword, modifiers);
if (instruction instanceof dockerfile_ast_1.ModifiableInstruction) {
for (var _i = 0, _a = instruction.getFlags(); _i < _a.length; _i++) {
var flag = _a[_i];
var flagRange = flag.getRange();
var nameRange = flag.getNameRange();
var mergedRange = {
start: flagRange.start,
end: nameRange.end
};
this.createToken(instruction, mergedRange, vscode_languageserver_types_1.SemanticTokenTypes.parameter);
var flagValue = flag.getValue();
if (flagValue !== null) {
if (flag.hasOptions()) {
var operatorRange = {
start: mergedRange.end,
end: {
line: mergedRange.end.line,
character: mergedRange.end.character + 1
}
};
this.createToken(instruction, operatorRange, vscode_languageserver_types_1.SemanticTokenTypes.operator, [], false, false);
for (var _b = 0, _c = flag.getOptions(); _b < _c.length; _b++) {
var option = _c[_b];
nameRange = option.getNameRange();
this.createToken(instruction, nameRange, vscode_languageserver_types_1.SemanticTokenTypes.parameter);
var valueRange = option.getValueRange();
if (valueRange !== null) {
var operatorRange_1 = {
start: nameRange.end,
end: valueRange.start
};
this.createToken(instruction, operatorRange_1, vscode_languageserver_types_1.SemanticTokenTypes.operator, [], false, false);
if (option.getValue() !== "") {
this.createToken(instruction, valueRange, vscode_languageserver_types_1.SemanticTokenTypes.property);
}
}
}
}
else {
var valueRange = flag.getValueRange();
var operatorRange = {
start: mergedRange.end,
end: valueRange.start
};
this.createToken(instruction, operatorRange, vscode_languageserver_types_1.SemanticTokenTypes.operator, [], false, false);
if (flagValue !== "") {
this.createToken(instruction, valueRange, vscode_languageserver_types_1.SemanticTokenTypes.property);
}
}
}
}
}
var args = instruction.getArguments();
if (args.length === 0) {
var range = instruction.getRange();
if (range.start.line !== range.end.line) {
// multiline instruction with no arguments,
// only escaped newlines and possibly comments
this.handleLineChange(instruction, instructionRange.end, range.end);
}
return;
}
switch (instruction.getKeyword()) {
case dockerfile_ast_1.Keyword.ARG:
case dockerfile_ast_1.Keyword.ENV:
var propertyInstruction = instruction;
for (var _d = 0, _e = propertyInstruction.getProperties(); _d < _e.length; _d++) {
var property = _e[_d];
var nameRange = property.getNameRange();
this.createToken(instruction, nameRange, vscode_languageserver_types_1.SemanticTokenTypes.variable, [vscode_languageserver_types_1.SemanticTokenModifiers.declaration], false);
var valueRange = property.getValueRange();
if (valueRange !== null) {
var operatorRange = {
start: nameRange.end,
end: valueRange.start
};
if (this.document.getText(operatorRange).startsWith("=")) {
var nameRangeEnd = this.document.offsetAt(nameRange.end);
operatorRange = { start: nameRange.end, end: this.document.positionAt(nameRangeEnd + 1) };
this.createToken(instruction, operatorRange, vscode_languageserver_types_1.SemanticTokenTypes.operator, [], false, false);
}
this.createToken(instruction, valueRange, vscode_languageserver_types_1.SemanticTokenTypes.parameter, [], true, true);
}
}
return;
case dockerfile_ast_1.Keyword.FROM:
var from = instruction;
this.createToken(instruction, from.getImageNameRange(), vscode_languageserver_types_1.SemanticTokenTypes.class);
var tagRange = from.getImageTagRange();
if (tagRange !== null) {
this.createToken(instruction, tagRange, vscode_languageserver_types_1.SemanticTokenTypes.property);
}
var digestRange = from.getImageDigestRange();
if (digestRange !== null) {
this.createToken(instruction, digestRange, vscode_languageserver_types_1.SemanticTokenTypes.property);
}
var fromArgs = instruction.getArguments();
if (fromArgs.length > 1) {
if (fromArgs[1].getValue().toUpperCase() === "AS") {
var range_1 = fromArgs[1].getRange();
this.createToken(instruction, range_1, vscode_languageserver_types_1.SemanticTokenTypes.keyword);
if (fromArgs.length > 2) {
this.createToken(instruction, fromArgs[2].getRange(), vscode_languageserver_types_1.SemanticTokenTypes.namespace);
if (fromArgs.length > 3) {
this.createArgumentTokens(instruction, fromArgs.slice(3));
}
}
}
else {
this.createArgumentTokens(instruction, fromArgs.slice(1));
}
}
return;
case dockerfile_ast_1.Keyword.HEALTHCHECK:
var healthcheck = instruction;
var range = healthcheck.getSubcommand().getRange();
this.createToken(instruction, range, vscode_languageserver_types_1.SemanticTokenTypes.keyword);
if (args.length > 1) {
this.createArgumentTokens(instruction, args.slice(1));
}
return;
case dockerfile_ast_1.Keyword.ONBUILD:
var onbuild = instruction;
this.createTokensForInstruction(onbuild.getTriggerInstruction());
return;
}
this.createArgumentTokens(instruction, args);
};
DockerSemanticTokens.prototype.createArgumentTokens = function (instruction, args) {
var lastRange = null;
for (var i = 0; i < args.length; i++) {
lastRange = args[i].getRange();
this.createToken(instruction, args[i].getRange(), vscode_languageserver_types_1.SemanticTokenTypes.parameter, [], true, true);
}
var instructionRange = instruction.getRange();
if (lastRange.end.line !== instructionRange.end.line || lastRange.end.character !== instructionRange.end.character) {
this.handleLineChange(instruction, lastRange.end, instructionRange.end);
}
};
DockerSemanticTokens.prototype.handleLineChange = function (instruction, checkStart, checkEnd) {
var comment = -1;
for (var i = this.document.offsetAt(checkStart); i < this.document.offsetAt(checkEnd); i++) {
switch (this.content.charAt(i)) {
case this.escapeCharacter:
// mark the escape character if it's not in a comment
if (comment === -1) {
this.createEscapeToken(instruction, i);
}
break;
case '\r':
case '\n':
if (comment !== -1) {
var commentRange = {
start: this.document.positionAt(comment),
end: this.document.positionAt(i)
};
this.createToken(null, commentRange, vscode_languageserver_types_1.SemanticTokenTypes.comment, [], false);
comment = -1;
}
break;
case '#':
if (comment === -1) {
comment = i;
}
break;
}
}
};
DockerSemanticTokens.prototype.createEscapeToken = function (instruction, offset) {
var escapeRange = {
start: this.document.positionAt(offset),
end: this.document.positionAt(offset + 1),
};
this.createToken(instruction, escapeRange, vscode_languageserver_types_1.SemanticTokenTypes.macro, [], false, false, false);
};
DockerSemanticTokens.prototype.createVariableToken = function (instruction, variable, range) {
var modifierRange = variable.getModifierRange();
if (modifierRange === null) {
this.createToken(instruction, range, vscode_languageserver_types_1.SemanticTokenTypes.variable, [], false);
}
else {
var operatorRange = vscode_languageserver_types_1.Range.create(vscode_languageserver_types_1.Position.create(modifierRange.start.line, modifierRange.start.character - 1), modifierRange.start);
if (range.start.character < operatorRange.start.character) {
// the operator is in the range, handle the content before the operator and the operator
this.createToken(instruction, vscode_languageserver_types_1.Range.create(range.start, operatorRange.start), vscode_languageserver_types_1.SemanticTokenTypes.variable, [], false);
this.createToken(instruction, operatorRange, vscode_languageserver_types_1.SemanticTokenTypes.operator, [], false, false, false);
}
// check if there is more content after the operator to process
if (range.end.character > operatorRange.end.character) {
if (modifierRange.end.character >= range.start.character) {
// only render the modifier if there is one, the variable may be ${var:} which we then want to skip
if (modifierRange.start.character !== modifierRange.end.character) {
this.createToken(instruction, modifierRange, vscode_languageserver_types_1.SemanticTokenTypes.modifier, [], false, false, false);
}
// process the content between the modifier and the end of the range if applicable
if (modifierRange.end.character !== range.end.character) {
this.createToken(instruction, vscode_languageserver_types_1.Range.create(modifierRange.end, range.end), vscode_languageserver_types_1.SemanticTokenTypes.variable, [], false);
}
}
else {
this.createToken(instruction, range, vscode_languageserver_types_1.SemanticTokenTypes.variable, [], false);
}
}
}
};
DockerSemanticTokens.prototype.createToken = function (instruction, range, tokenType, tokenModifiers, checkVariables, checkStrings, checkNewline) {
if (tokenModifiers === void 0) { tokenModifiers = []; }
if (checkVariables === void 0) { checkVariables = true; }
if (checkStrings === void 0) { checkStrings = false; }
if (checkNewline === void 0) { checkNewline = true; }
if (checkNewline && this.currentRange !== null && this.currentRange.end.line !== range.start.line) {
// this implies that there's been a line change between one arg and the next
this.handleLineChange(instruction, this.currentRange.end, range.start);
}
if (checkStrings) {
var startOffset = this.document.offsetAt(range.start);
var quoteStart = startOffset;
var newOffset = -1;
var escaping = false;
var endOffset = this.document.offsetAt(range.end);
stringsCheck: for (var i = startOffset; i < endOffset; i++) {
var ch = this.content.charAt(i);
switch (ch) {
case this.escapeCharacter:
escapeCheck: for (var j = i + 1; j < endOffset; j++) {
var escapedCh = this.content.charAt(j);
switch (escapedCh) {
case ' ':
case '\t':
continue;
case '\r':
j++;
case '\n':
escaping = true;
i = j;
continue stringsCheck;
default:
break escapeCheck;
}
}
escaping = false;
if (startOffset === -1) {
startOffset = i;
}
break;
case '\'':
case '"':
escaping = false;
if (this.quote === null) {
if (this.escapedQuote === null) {
this.quote = ch;
quoteStart = i;
if (startOffset !== -1 && startOffset !== quoteStart) {
var intermediateRange = {
start: this.document.positionAt(startOffset),
end: this.document.positionAt(quoteStart),
};
this.createToken(instruction, intermediateRange, tokenType, tokenModifiers);
}
}
}
else if (this.quote === ch) {
var quoteRange = {
start: this.document.positionAt(quoteStart),
end: this.document.positionAt(i + 1),
};
this.createToken(instruction, quoteRange, vscode_languageserver_types_1.SemanticTokenTypes.string, [], true, false);
newOffset = i + 1;
startOffset = -1;
this.quote = null;
}
break;
case '#':
if (escaping) {
for (var j = i + 1; j < endOffset; j++) {
var escapedCh = this.content.charAt(j);
switch (escapedCh) {
case '\r':
j++;
case '\n':
i = j;
continue stringsCheck;
}
}
break;
}
case ' ':
case '\t':
case '\r':
case '\n':
if (escaping) {
continue;
}
default:
escaping = false;
if (startOffset === -1) {
startOffset = i;
}
break;
}
}
if (this.quote !== null) {
var quoteRange = {
start: this.document.positionAt(quoteStart),
end: this.document.positionAt(endOffset),
};
this.createToken(instruction, quoteRange, vscode_languageserver_types_1.SemanticTokenTypes.string, [], true, false);
return;
}
else if (newOffset !== -1) {
if (newOffset !== endOffset) {
var intermediateRange = {
start: this.document.positionAt(newOffset),
end: this.document.positionAt(endOffset),
};
this.createToken(instruction, intermediateRange, tokenType, tokenModifiers);
}
return;
}
else if (this.quote !== null || this.escapedQuote !== null) {
// there is now an open string, change the token to a string
tokenType = vscode_languageserver_types_1.SemanticTokenTypes.string;
// reset the range to the start of the string
range = {
start: this.document.positionAt(quoteStart),
end: range.end
};
}
}
if (range.start.line !== range.end.line) {
var startOffset = this.document.offsetAt(range.start);
var endOffset = this.document.offsetAt(range.end);
var intermediateAdded = false;
var handleNewlines = true;
var escaping = false;
for (var i = startOffset; i < endOffset; i++) {
var ch = this.content.charAt(i);
switch (ch) {
case '#':
if (escaping) {
var commenting = true;
commentCheck: for (var j = i + 1; j < endOffset; j++) {
switch (this.content.charAt(j)) {
case ' ':
case '\t':
break;
case '\r':
var crComment = {
start: this.document.positionAt(i),
end: this.document.positionAt(j)
};
this.createToken(null, crComment, vscode_languageserver_types_1.SemanticTokenTypes.comment, [], false);
i = j + 1;
startOffset = -1;
commenting = false;
j++;
break;
case '\n':
var lfComment = {
start: this.document.positionAt(i),
end: this.document.positionAt(j)
};
this.createToken(null, lfComment, vscode_languageserver_types_1.SemanticTokenTypes.comment, [], false);
i = j;
startOffset = -1;
commenting = false;
break;
case '#':
if (!commenting) {
i = j;
}
commenting = true;
break;
default:
if (commenting) {
break;
}
i = j - 1;
break commentCheck;
}
}
}
break;
case this.escapeCharacter:
// note whether the intermediate token has been added or not
var added = false;
escapeCheck: for (var j = i + 1; j < endOffset; j++) {
switch (this.content.charAt(j)) {
case ' ':
case '\t':
case '\r':
break;
case '\n':
if (!added) {
if (!intermediateAdded && startOffset !== -1) {
if (i !== startOffset) {
var intermediateRange_1 = {
start: this.document.positionAt(startOffset),
end: this.document.positionAt(i),
};
this.createToken(instruction, intermediateRange_1, tokenType, tokenModifiers);
}
intermediateAdded = true;
}
this.createEscapeToken(instruction, i);
}
// escaped newlines have are being handled here already
handleNewlines = false;
escaping = true;
added = true;
i = j;
startOffset = -1;
break;
case '#':
if (escaping) {
i = j - 1;
break escapeCheck;
}
case '\\':
if (!escaping) {
intermediateAdded = false;
escaping = false;
i = j;
break escapeCheck;
}
i = j;
added = false;
startOffset = j;
break;
default:
if (startOffset === -1) {
intermediateAdded = false;
escaping = false;
startOffset = j;
i = j;
}
break escapeCheck;
}
}
break;
default:
if (startOffset === -1) {
intermediateAdded = false;
escaping = false;
startOffset = i;
}
break;
}
}
if (startOffset === -1) {
// we've processed the intermediate token but there is nothing of interest after it
return;
}
var intermediateRange = {
start: this.document.positionAt(startOffset),
end: this.document.positionAt(endOffset),
};
this.createToken(instruction, intermediateRange, tokenType, tokenModifiers, checkVariables, checkStrings, handleNewlines);
return;
}
if (checkVariables) {
var startPosition = range.start;
var lastVariableRange = null;
for (var _i = 0, _a = instruction.getVariables(); _i < _a.length; _i++) {
var variable = _a[_i];
var variableRange = variable.getRange();
if (docker_1.Util.isInsideRange(range.start, variableRange) && docker_1.Util.isInsideRange(range.end, variableRange)) {
if (tokenType === vscode_languageserver_types_1.SemanticTokenTypes.string) {
break;
}
// the token is completely inside the variable's range, render it as a variable
this.createVariableToken(instruction, variable, range);
return;
}
else if (docker_1.Util.isInsideRange(variableRange.start, range)) {
if (docker_1.Util.positionBefore(startPosition, variableRange.start)) {
// create a parameter token for the characters
// before the variable
this.createToken(instruction, {
start: startPosition,
end: variableRange.start
}, tokenType, tokenModifiers, false);
}
var variableProcessingRange = variableRange;
if (variableRange.end.character > range.end.character) {
variableProcessingRange.end = range.end;
}
this.createVariableToken(instruction, variable, variableProcessingRange);
lastVariableRange = variableRange;
if (docker_1.Util.positionEquals(range.end, variableRange.end)) {
return;
}
startPosition = variableRange.end;
}
}
if (lastVariableRange !== null) {
// alter the range so it is the characters that comes
// after the last matched variable
range = { start: lastVariableRange.end, end: range.end };
}
}
if (this.currentRange === null) {
this.tokens = this.tokens.concat([
range.start.line,
range.start.character,
range.end.character - range.start.character,
TokensLegend.getTokenType(tokenType),
TokensLegend.getTokenModifiers(tokenModifiers)
]);
}
else if (this.currentRange.end.line !== range.start.line) {
this.tokens = this.tokens.concat([
range.start.line - this.currentRange.end.line,
range.start.character,
range.end.character - range.start.character,
TokensLegend.getTokenType(tokenType),
TokensLegend.getTokenModifiers(tokenModifiers)
]);
}
else {
this.tokens = this.tokens.concat([
range.start.line - this.currentRange.start.line,
range.start.character - this.currentRange.start.character,
range.end.character - range.start.character,
TokensLegend.getTokenType(tokenType),
TokensLegend.getTokenModifiers(tokenModifiers)
]);
}
this.currentRange = range;
};
return DockerSemanticTokens;
}());
exports.DockerSemanticTokens = DockerSemanticTokens;
});