UNPKG

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
(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; });