dockerfile-ast
Version:
Parse a Dockerfile into an array of instructions and comments.
331 lines (330 loc) • 16.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PropertyInstruction = void 0;
const vscode_languageserver_types_1 = require("vscode-languageserver-types");
const instruction_1 = require("./instruction");
const property_1 = require("./property");
const argument_1 = require("./argument");
const util_1 = require("./util");
class PropertyInstruction extends instruction_1.Instruction {
constructor(document, range, dockerfile, escapeChar, instruction, instructionRange) {
super(document, range, dockerfile, escapeChar, instruction, instructionRange);
this.properties = undefined;
}
getProperties() {
if (this.properties === undefined) {
let args = this.getPropertyArguments();
if (args.length === 0) {
this.properties = [];
}
else if (args.length === 1) {
this.properties = [new property_1.Property(this.document, this.escapeChar, args[0])];
}
else if (args.length === 2) {
if (args[0].getValue().indexOf('=') === -1) {
this.properties = [new property_1.Property(this.document, this.escapeChar, args[0], args[1])];
}
else {
this.properties = [
new property_1.Property(this.document, this.escapeChar, args[0]),
new property_1.Property(this.document, this.escapeChar, args[1])
];
}
}
else if (args[0].getValue().indexOf('=') === -1) {
let text = this.document.getText();
let start = args[1].getRange().start;
let end = args[args.length - 1].getRange().end;
text = text.substring(this.document.offsetAt(start), this.document.offsetAt(end));
this.properties = [new property_1.Property(this.document, this.escapeChar, args[0], new argument_1.Argument(text, vscode_languageserver_types_1.Range.create(args[1].getRange().start, args[args.length - 1].getRange().end)))];
}
else {
this.properties = [];
for (let i = 0; i < args.length; i++) {
this.properties.push(new property_1.Property(this.document, this.escapeChar, args[i]));
}
}
}
return this.properties;
}
/**
* Goes from the back of the string and returns the first
* non-whitespace character that is found. If an escape character
* is found with newline characters, the escape character will
* not be considered a non-whitespace character and its index in
* the string will not be returned.
*
* @param content the string to search through
* @return the index in the string for the first non-whitespace
* character when searching from the end of the string
*/
findTrailingNonWhitespace(content) {
// loop back to find the first non-whitespace character
let index = content.length;
whitespaceCheck: for (let i = content.length - 1; i >= 0; i--) {
switch (content.charAt(i)) {
case ' ':
case '\t':
continue;
case '\n':
if (content.charAt(i - 1) === '\r') {
i = i - 1;
}
case '\r':
newlineCheck: for (let j = i - 1; j >= 0; j--) {
switch (content.charAt(j)) {
case ' ':
case '\t':
case '\r':
case '\n':
case this.escapeChar:
continue;
default:
index = j;
break newlineCheck;
}
}
break whitespaceCheck;
default:
index = i;
break whitespaceCheck;
}
}
return index;
}
getPropertyArguments() {
const args = [];
let range = this.getInstructionRange();
let instructionNameEndOffset = this.document.offsetAt(range.end);
let extra = instructionNameEndOffset - this.document.offsetAt(range.start);
let content = this.getTextContent();
let fullArgs = content.substring(extra);
let start = util_1.Util.findLeadingNonWhitespace(fullArgs, this.escapeChar);
if (start === -1) {
// only whitespace found, no arguments
return [];
}
const startPosition = this.document.positionAt(instructionNameEndOffset + start);
// records whether the parser has just processed an escaped newline or not,
// if our starting position is not on the same line as the instruction then
// the start of the content is already on an escaped line
let escaped = range.start.line !== startPosition.line;
// flag to track if the last character was an escape character
let endingEscape = false;
// position before the first escape character was hit
let mark = -1;
let end = this.findTrailingNonWhitespace(fullArgs);
content = fullArgs.substring(start, end + 1);
let argStart = escaped ? -1 : 0;
let spaced = false;
argumentLoop: for (let i = 0; i < content.length; i++) {
let char = content.charAt(i);
switch (char) {
case this.escapeChar:
if (i + 1 === content.length) {
endingEscape = true;
break argumentLoop;
}
if (!escaped) {
mark = i;
}
switch (content.charAt(i + 1)) {
case ' ':
case '\t':
if (!util_1.Util.isWhitespace(content.charAt(i + 2))) {
// space was escaped, continue as normal
i = i + 1;
continue argumentLoop;
}
// whitespace encountered, need to figure out if it extends to EOL
whitespaceCheck: for (let j = i + 2; j < content.length; j++) {
switch (content.charAt(j)) {
case '\r':
// offset one more for \r\n
j++;
case '\n':
// whitespace only, safe to skip
escaped = true;
i = j;
continue argumentLoop;
case ' ':
case '\t':
// ignore whitespace
break;
default:
// whitespace doesn't extend to EOL, create an argument
args.push(new argument_1.Argument(content.substring(argStart, i), vscode_languageserver_types_1.Range.create(this.document.positionAt(instructionNameEndOffset + start + argStart), this.document.positionAt(instructionNameEndOffset + start + i + 2))));
argStart = j;
break whitespaceCheck;
}
}
// go back and start processing the encountered non-whitespace character
i = argStart - 1;
continue argumentLoop;
case '\r':
// offset one more for \r\n
i++;
case '\n':
// immediately followed by a newline, skip the newline
escaped = true;
i = i + 1;
continue argumentLoop;
case this.escapeChar:
// double escape found, skip it and move on
if (argStart === -1) {
argStart = i;
}
i = i + 1;
continue argumentLoop;
default:
if (argStart === -1) {
argStart = i;
}
// non-whitespace encountered, skip the escape and process the
// character normally
continue argumentLoop;
}
case '\'':
case '"':
if (spaced) {
this.createSpacedArgument(argStart, args, content, mark, instructionNameEndOffset, start);
// reset to start a new argument
argStart = i;
spaced = false;
}
if (argStart === -1) {
argStart = i;
}
for (let j = i + 1; j < content.length; j++) {
switch (content.charAt(j)) {
case char:
if (content.charAt(j + 1) !== ' ' && content.charAt(j + 1) !== '') {
// there is more content after this quote,
// continue so that it is all processed as
// one single argument
i = j;
continue argumentLoop;
}
args.push(new argument_1.Argument(content.substring(argStart, j + 1), vscode_languageserver_types_1.Range.create(this.document.positionAt(instructionNameEndOffset + start + argStart), this.document.positionAt(instructionNameEndOffset + start + j + 1))));
i = j;
argStart = -1;
continue argumentLoop;
case this.escapeChar:
j++;
break;
}
}
break argumentLoop;
case ' ':
case '\t':
if (escaped) {
// consider there to be a space only if an argument
// is not spanning multiple lines
if (argStart !== -1) {
spaced = true;
}
}
else if (argStart !== -1) {
args.push(new argument_1.Argument(content.substring(argStart, i), vscode_languageserver_types_1.Range.create(this.document.positionAt(instructionNameEndOffset + start + argStart), this.document.positionAt(instructionNameEndOffset + start + i))));
argStart = -1;
}
break;
case '\r':
// offset one more for \r\n
i++;
case '\n':
spaced = false;
break;
case '#':
if (escaped) {
// a newline was escaped and now there's a comment
for (let j = i + 1; j < content.length; j++) {
switch (content.charAt(j)) {
case '\r':
j++;
case '\n':
i = j;
spaced = false;
continue argumentLoop;
}
}
// went to the end without finding a newline,
// the comment was the last line in the instruction,
// just stop parsing, create an argument if needed
if (argStart !== -1) {
let value = content.substring(argStart, mark);
args.push(new argument_1.Argument(value, vscode_languageserver_types_1.Range.create(this.document.positionAt(instructionNameEndOffset + start + argStart), this.document.positionAt(instructionNameEndOffset + start + mark))));
argStart = -1;
}
break argumentLoop;
}
else if (argStart === -1) {
argStart = i;
}
break;
default:
if (spaced) {
this.createSpacedArgument(argStart, args, content, mark, instructionNameEndOffset, start);
// reset to start a new argument
argStart = i;
spaced = false;
}
escaped = false;
if (argStart === -1) {
argStart = i;
}
// variable detected
if (char === '$' && content.charAt(i + 1) === '{') {
let singleQuotes = false;
let doubleQuotes = false;
let escaped = false;
for (let j = i + 1; j < content.length; j++) {
switch (content.charAt(j)) {
case this.escapeChar:
escaped = true;
break;
case '\r':
case '\n':
break;
case '\'':
singleQuotes = !singleQuotes;
escaped = false;
break;
case '"':
doubleQuotes = !doubleQuotes;
escaped = false;
break;
case ' ':
case '\t':
if (escaped || singleQuotes || doubleQuotes) {
break;
}
i = j - 1;
continue argumentLoop;
case '}':
i = j;
continue argumentLoop;
default:
escaped = false;
break;
}
}
break argumentLoop;
}
break;
}
}
if (argStart !== -1 && argStart !== content.length) {
let end = endingEscape ? content.length - 1 : content.length;
let value = content.substring(argStart, end);
args.push(new argument_1.Argument(value, vscode_languageserver_types_1.Range.create(this.document.positionAt(instructionNameEndOffset + start + argStart), this.document.positionAt(instructionNameEndOffset + start + end))));
}
return args;
}
createSpacedArgument(argStart, args, content, mark, instructionNameEndOffset, start) {
if (argStart !== -1) {
args.push(new argument_1.Argument(content.substring(argStart, mark), vscode_languageserver_types_1.Range.create(this.document.positionAt(instructionNameEndOffset + start + argStart), this.document.positionAt(instructionNameEndOffset + start + mark))));
}
}
}
exports.PropertyInstruction = PropertyInstruction;