UNPKG

29.7 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3/* --------------------------------------------------------------------------------------------
4 * Copyright (c) Remy Suen. All rights reserved.
5 * Licensed under the MIT License. See License.txt in the project root for license information.
6 * ------------------------------------------------------------------------------------------ */
7const vscode_languageserver_types_1 = require("vscode-languageserver-types");
8const util_1 = require("./util");
9const line_1 = require("./line");
10const argument_1 = require("./argument");
11const variable_1 = require("./variable");
12const main_1 = require("./main");
13class Instruction extends line_1.Line {
14 constructor(document, range, dockerfile, escapeChar, instruction, instructionRange) {
15 super(document, range);
16 this.dockerfile = dockerfile;
17 this.escapeChar = escapeChar;
18 this.instruction = instruction;
19 this.instructionRange = instructionRange;
20 }
21 toString() {
22 let value = this.getKeyword();
23 for (let arg of this.getRawArguments()) {
24 value += ' ';
25 value += arg.getValue();
26 }
27 return value;
28 }
29 getRangeContent(range) {
30 if (range === null) {
31 return null;
32 }
33 return this.document.getText().substring(this.document.offsetAt(range.start), this.document.offsetAt(range.end));
34 }
35 getInstructionRange() {
36 return this.instructionRange;
37 }
38 getInstruction() {
39 return this.instruction;
40 }
41 getKeyword() {
42 return this.getInstruction().toUpperCase();
43 }
44 getArgumentsRange() {
45 let args = this.getArguments();
46 if (args.length === 0) {
47 return null;
48 }
49 return vscode_languageserver_types_1.Range.create(args[0].getRange().start, args[args.length - 1].getRange().end);
50 }
51 getArgumentsRanges() {
52 let args = this.getArguments();
53 if (args.length === 0) {
54 return [];
55 }
56 if (args[0].getRange().start.line === args[args.length - 1].getRange().end.line) {
57 return [vscode_languageserver_types_1.Range.create(args[0].getRange().start, args[args.length - 1].getRange().end)];
58 }
59 let ranges = [];
60 let end = -1;
61 let startPosition = args[0].getRange().start;
62 let range = this.getInstructionRange();
63 let extra = this.document.offsetAt(startPosition) - this.document.offsetAt(range.start);
64 let content = this.getTextContent();
65 let fullArgs = content.substring(extra, this.document.offsetAt(args[args.length - 1].getRange().end) - this.document.offsetAt(range.start));
66 let offset = this.document.offsetAt(range.start) + extra;
67 let start = false;
68 let comment = false;
69 for (let i = 0; i < fullArgs.length; i++) {
70 let char = fullArgs.charAt(i);
71 if (char === this.escapeChar) {
72 let next = fullArgs.charAt(i + 1);
73 if (next === ' ' || next === '\t') {
74 whitespaceCheck: for (let j = i + 2; j < fullArgs.length; j++) {
75 switch (fullArgs.charAt(j)) {
76 case ' ':
77 case '\t':
78 continue;
79 case '\r':
80 j++;
81 case '\n':
82 if (startPosition !== null) {
83 ranges.push(vscode_languageserver_types_1.Range.create(startPosition, this.document.positionAt(offset + end + 1)));
84 }
85 startPosition = null;
86 start = true;
87 comment = false;
88 i = j;
89 break whitespaceCheck;
90 default:
91 break whitespaceCheck;
92 }
93 }
94 }
95 else if (next === '\r') {
96 if (startPosition !== null) {
97 ranges.push(vscode_languageserver_types_1.Range.create(startPosition, this.document.positionAt(offset + end + 1)));
98 startPosition = null;
99 }
100 start = true;
101 comment = false;
102 i += 2;
103 }
104 else if (next === '\n') {
105 if (startPosition !== null) {
106 ranges.push(vscode_languageserver_types_1.Range.create(startPosition, this.document.positionAt(offset + end + 1)));
107 }
108 startPosition = null;
109 start = true;
110 comment = false;
111 i++;
112 }
113 else {
114 i++;
115 }
116 }
117 else if (util_1.Util.isNewline(char)) {
118 if (comment) {
119 startPosition = null;
120 start = true;
121 comment = false;
122 }
123 }
124 else {
125 if (!comment) {
126 if (startPosition === null) {
127 if (char === '#') {
128 comment = true;
129 continue;
130 }
131 let position = this.document.positionAt(offset + i);
132 if (position.character !== 0) {
133 startPosition = vscode_languageserver_types_1.Position.create(position.line, 0);
134 }
135 }
136 end = i;
137 }
138 }
139 }
140 if (startPosition === null) {
141 // should only happen if the last argument is on its own line with
142 // no leading whitespace
143 ranges.push(vscode_languageserver_types_1.Range.create(this.document.positionAt(offset + end), this.document.positionAt(offset + end + 1)));
144 }
145 else {
146 ranges.push(vscode_languageserver_types_1.Range.create(startPosition, this.document.positionAt(offset + end + 1)));
147 }
148 return ranges;
149 }
150 getRawArgumentsContent() {
151 let args = this.getArguments();
152 if (args.length === 0) {
153 return null;
154 }
155 return this.getRangeContent(vscode_languageserver_types_1.Range.create(args[0].getRange().start, args[args.length - 1].getRange().end));
156 }
157 getArgumentsContent() {
158 let args = this.getArguments();
159 if (args.length === 0) {
160 return null;
161 }
162 let content = "";
163 let ranges = this.getArgumentsRanges();
164 let documentText = this.document.getText();
165 for (let range of ranges) {
166 content += documentText.substring(this.document.offsetAt(range.start), this.document.offsetAt(range.end));
167 }
168 return content;
169 }
170 getArguments() {
171 return this.getRawArguments();
172 }
173 getRawArguments() {
174 let args = [];
175 let range = this.getInstructionRange();
176 let extra = this.document.offsetAt(range.end) - this.document.offsetAt(range.start);
177 let content = this.getTextContent();
178 let fullArgs = content.substring(extra);
179 let offset = this.document.offsetAt(range.start) + extra;
180 let start = false;
181 let comment = false;
182 let found = -1;
183 // determines whether the parser has found a space or tab
184 // whitespace character that's a part of an escaped newline sequence
185 let escapedWhitespaceDetected = false;
186 // determines if the parser is currently in an escaped newline sequence
187 let escaping = false;
188 let escapeMarker = -1;
189 let escapedArg = "";
190 for (let i = 0; i < fullArgs.length; i++) {
191 let char = fullArgs.charAt(i);
192 if (util_1.Util.isWhitespace(char)) {
193 if (escaping) {
194 escapedWhitespaceDetected = true;
195 if (util_1.Util.isNewline(char)) {
196 // reached a newline, any previously
197 // detected whitespace should be ignored
198 escapedWhitespaceDetected = false;
199 if (comment) {
200 // reached a newline, no longer in a comment
201 comment = false;
202 start = true;
203 }
204 }
205 continue;
206 }
207 else if (found !== -1) {
208 if (escapeMarker === -1) {
209 args.push(new argument_1.Argument(escapedArg, vscode_languageserver_types_1.Range.create(this.document.positionAt(offset + found), this.document.positionAt(offset + i))));
210 }
211 else {
212 args.push(new argument_1.Argument(escapedArg, vscode_languageserver_types_1.Range.create(this.document.positionAt(offset + found), this.document.positionAt(offset + escapeMarker))));
213 }
214 escapeMarker = -1;
215 escapedArg = "";
216 found = -1;
217 }
218 }
219 else if (char === this.escapeChar) {
220 let next = fullArgs.charAt(i + 1);
221 if (next === ' ' || next === '\t') {
222 whitespaceCheck: for (let j = i + 2; j < fullArgs.length; j++) {
223 let newlineCheck = fullArgs.charAt(j);
224 switch (newlineCheck) {
225 case ' ':
226 case '\t':
227 continue;
228 case '\r':
229 j++;
230 case '\n':
231 comment = false;
232 escaping = true;
233 start = true;
234 if (found !== -1) {
235 escapeMarker = i;
236 }
237 i = j;
238 break whitespaceCheck;
239 default:
240 escapeMarker = i;
241 if (found === -1) {
242 i = j - 1;
243 }
244 break whitespaceCheck;
245 }
246 }
247 }
248 else if (next === '\r') {
249 comment = false;
250 escaping = true;
251 start = true;
252 if (found !== -1 && escapeMarker === -1) {
253 escapeMarker = i;
254 }
255 i += 2;
256 }
257 else if (next === '\n') {
258 comment = false;
259 escaping = true;
260 start = true;
261 if (found !== -1 && escapeMarker === -1) {
262 escapeMarker = i;
263 }
264 i++;
265 }
266 else {
267 if (escapedWhitespaceDetected && escapeMarker !== -1) {
268 args.push(new argument_1.Argument(escapedArg, vscode_languageserver_types_1.Range.create(this.document.positionAt(offset + found), this.document.positionAt(offset + escapeMarker))));
269 escapedArg = "";
270 found = -1;
271 }
272 escapeMarker = -1;
273 escapedWhitespaceDetected = false;
274 escaping = false;
275 if (next === '$') {
276 escapedArg = escapedArg + char + next;
277 }
278 else if (next === '') {
279 // reached EOF, stop processing
280 break;
281 }
282 else {
283 escapedArg = escapedArg + next;
284 }
285 if (found === -1) {
286 found = i;
287 }
288 i++;
289 }
290 }
291 else if (!comment) {
292 if (start && char === '#') {
293 comment = true;
294 }
295 else {
296 if (escapedWhitespaceDetected && escapeMarker !== -1) {
297 args.push(new argument_1.Argument(escapedArg, vscode_languageserver_types_1.Range.create(this.document.positionAt(offset + found), this.document.positionAt(offset + escapeMarker))));
298 escapedArg = "";
299 found = -1;
300 }
301 escapedWhitespaceDetected = false;
302 escaping = false;
303 escapeMarker = -1;
304 escapedArg = escapedArg + char;
305 if (found === -1) {
306 found = i;
307 }
308 }
309 // non-whitespace character detected, reset
310 start = false;
311 }
312 }
313 if (found !== -1) {
314 if (escapeMarker === -1) {
315 args.push(new argument_1.Argument(escapedArg, vscode_languageserver_types_1.Range.create(this.document.positionAt(offset + found), this.document.positionAt(offset + fullArgs.length))));
316 }
317 else {
318 args.push(new argument_1.Argument(escapedArg, vscode_languageserver_types_1.Range.create(this.document.positionAt(offset + found), this.document.positionAt(offset + escapeMarker))));
319 }
320 }
321 return args;
322 }
323 getExpandedArguments() {
324 let args = this.getArguments();
325 for (let i = 0; i < args.length; i++) {
326 const argRange = args[i].getRange();
327 let offset = this.document.offsetAt(argRange.start);
328 const variables = this.parseVariables(offset, args[i].getValue());
329 const swaps = [];
330 let requiresExpansion = false;
331 for (let variable of variables) {
332 const value = this.dockerfile.resolveVariable(variable.getName(), variable.getNameRange().start.line);
333 swaps.push(value);
334 requiresExpansion = requiresExpansion || value !== undefined;
335 }
336 if (requiresExpansion) {
337 let expanded = "";
338 for (let j = 0; j < swaps.length; j++) {
339 const variableRange = variables[j].getRange();
340 const start = this.document.offsetAt(variableRange.start);
341 const end = this.document.offsetAt(variableRange.end);
342 if (swaps[j]) {
343 // replace variable with its resolved value
344 expanded += this.document.getText().substring(offset, start);
345 expanded += swaps[j];
346 offset = end;
347 }
348 else {
349 expanded += this.document.getText().substring(offset, end);
350 offset = end;
351 }
352 }
353 const argEnd = this.document.offsetAt(argRange.end);
354 if (argEnd !== offset) {
355 // if the variable's range doesn't match the argument,
356 // append the remaining text
357 expanded += this.document.getText().substring(offset, argEnd);
358 }
359 args[i] = new argument_1.Argument(expanded, argRange);
360 }
361 }
362 return args;
363 }
364 getVariables() {
365 const variables = [];
366 const args = this.getRawArguments();
367 for (const arg of args) {
368 let range = arg.getRange();
369 let rawValue = this.document.getText().substring(this.document.offsetAt(range.start), this.document.offsetAt(range.end));
370 const parsedVariables = this.parseVariables(this.document.offsetAt(arg.getRange().start), rawValue);
371 for (const parsedVariable of parsedVariables) {
372 variables.push(parsedVariable);
373 }
374 }
375 return variables;
376 }
377 parseVariables(offset, arg) {
378 let variables = [];
379 variableLoop: for (let i = 0; i < arg.length; i++) {
380 switch (arg.charAt(i)) {
381 case this.escapeChar:
382 if (arg.charAt(i + 1) === '$') {
383 i++;
384 }
385 break;
386 case '$':
387 if (arg.charAt(i + 1) === '{') {
388 let escapedString = "${";
389 let escapedName = "";
390 let nameEnd = -1;
391 let escapedSubstitutionParameter = "";
392 let substitutionStart = -1;
393 let substitutionEnd = -1;
394 let modifierRead = -1;
395 nameLoop: for (let j = i + 2; j < arg.length; j++) {
396 let char = arg.charAt(j);
397 switch (char) {
398 case this.escapeChar:
399 for (let k = j + 1; k < arg.length; k++) {
400 switch (arg.charAt(k)) {
401 case ' ':
402 case '\t':
403 case '\r':
404 // ignore whitespace
405 continue;
406 case '\n':
407 // escape this newline
408 j = k;
409 continue nameLoop;
410 }
411 }
412 break;
413 case '}':
414 escapedString += '}';
415 let modifier = null;
416 let modifierRange = null;
417 let substitutionParameter = modifierRead !== -1 ? escapedSubstitutionParameter : null;
418 let substitutionRange = null;
419 if (nameEnd === -1) {
420 nameEnd = j;
421 }
422 else if (nameEnd + 1 === j) {
423 modifier = "";
424 modifierRange = vscode_languageserver_types_1.Range.create(this.document.positionAt(offset + nameEnd + 1), this.document.positionAt(offset + nameEnd + 1));
425 }
426 else {
427 if (substitutionStart === -1) {
428 // no substitution parameter found,
429 // but a modifier character existed,
430 // just offset the range by 1 from
431 // the modifier character
432 substitutionStart = modifierRead + 1;
433 substitutionEnd = modifierRead + 1;
434 }
435 else {
436 // offset one more from the last
437 // character found
438 substitutionEnd = substitutionEnd + 1;
439 }
440 modifier = arg.substring(modifierRead, modifierRead + 1);
441 modifierRange = vscode_languageserver_types_1.Range.create(this.document.positionAt(offset + modifierRead), this.document.positionAt(offset + modifierRead + 1));
442 substitutionRange = vscode_languageserver_types_1.Range.create(this.document.positionAt(offset + substitutionStart), this.document.positionAt(offset + substitutionEnd));
443 }
444 let start = this.document.positionAt(offset + i);
445 variables.push(new variable_1.Variable(escapedName, vscode_languageserver_types_1.Range.create(this.document.positionAt(offset + i + 2), this.document.positionAt(offset + nameEnd)), vscode_languageserver_types_1.Range.create(start, this.document.positionAt(offset + j + 1)), modifier, modifierRange, substitutionParameter, substitutionRange, this.dockerfile.resolveVariable(escapedName, start.line) !== undefined, this.isBuildVariable(escapedName, start.line), escapedString));
446 i = j;
447 continue variableLoop;
448 case ':':
449 if (nameEnd === -1) {
450 nameEnd = j;
451 }
452 else if (modifierRead !== -1) {
453 if (substitutionStart === -1) {
454 substitutionStart = j;
455 substitutionEnd = j;
456 }
457 else {
458 substitutionEnd = j;
459 }
460 escapedSubstitutionParameter += ':';
461 }
462 else {
463 modifierRead = j;
464 }
465 escapedString += ':';
466 break;
467 case '\n':
468 case '\r':
469 case ' ':
470 case '\t':
471 break;
472 default:
473 if (nameEnd === -1) {
474 escapedName += char;
475 }
476 else if (modifierRead !== -1) {
477 if (substitutionStart === -1) {
478 substitutionStart = j;
479 substitutionEnd = j;
480 }
481 else {
482 substitutionEnd = j;
483 }
484 escapedSubstitutionParameter += char;
485 }
486 else {
487 modifierRead = j;
488 }
489 escapedString += char;
490 break;
491 }
492 }
493 // no } found, not a valid variable, stop processing
494 break variableLoop;
495 }
496 else if (util_1.Util.isWhitespace(arg.charAt(i + 1)) || i === arg.length - 1) {
497 // $ followed by whitespace or EOF, ignore this variable
498 continue;
499 }
500 else {
501 let escapedName = "";
502 nameLoop: for (let j = i + 1; j < arg.length; j++) {
503 let char = arg.charAt(j);
504 switch (char) {
505 case '\r':
506 case '\n':
507 case ' ':
508 case '\t':
509 continue;
510 case '$':
511 case '\'':
512 case '"':
513 let varStart = this.document.positionAt(offset + i);
514 variables.push(new variable_1.Variable(escapedName, vscode_languageserver_types_1.Range.create(this.document.positionAt(offset + i + 1), this.document.positionAt(offset + j)), vscode_languageserver_types_1.Range.create(varStart, this.document.positionAt(offset + j)), null, null, null, null, this.dockerfile.resolveVariable(escapedName, varStart.line) !== undefined, this.isBuildVariable(escapedName, varStart.line), '$' + escapedName));
515 i = j - 1;
516 continue variableLoop;
517 case this.escapeChar:
518 for (let k = j + 1; k < arg.length; k++) {
519 switch (arg.charAt(k)) {
520 case ' ':
521 case '\t':
522 case '\r':
523 // ignore whitespace
524 continue;
525 case '\n':
526 // escape this newline
527 j = k;
528 continue nameLoop;
529 }
530 }
531 // reached EOF after an escape character
532 let start = this.document.positionAt(offset + i);
533 variables.push(new variable_1.Variable(escapedName, vscode_languageserver_types_1.Range.create(this.document.positionAt(offset + i + 1), this.document.positionAt(offset + j)), vscode_languageserver_types_1.Range.create(start, this.document.positionAt(offset + j)), null, null, null, null, this.dockerfile.resolveVariable(escapedName, start.line) !== undefined, this.isBuildVariable(escapedName, start.line), '$' + escapedName));
534 break variableLoop;
535 }
536 if (char.match(/^[a-z0-9_]+$/i) === null) {
537 let varStart = this.document.positionAt(offset + i);
538 variables.push(new variable_1.Variable(escapedName, vscode_languageserver_types_1.Range.create(this.document.positionAt(offset + i + 1), this.document.positionAt(offset + j)), vscode_languageserver_types_1.Range.create(varStart, this.document.positionAt(offset + j)), null, null, null, null, this.dockerfile.resolveVariable(escapedName, varStart.line) !== undefined, this.isBuildVariable(escapedName, varStart.line), '$' + escapedName));
539 i = j - 1;
540 continue variableLoop;
541 }
542 escapedName += char;
543 }
544 let start = this.document.positionAt(offset + i);
545 variables.push(new variable_1.Variable(escapedName, vscode_languageserver_types_1.Range.create(this.document.positionAt(offset + i + 1), this.document.positionAt(offset + arg.length)), vscode_languageserver_types_1.Range.create(start, this.document.positionAt(offset + arg.length)), null, null, null, null, this.dockerfile.resolveVariable(escapedName, start.line) !== undefined, this.isBuildVariable(escapedName, start.line), '$' + escapedName));
546 }
547 break variableLoop;
548 }
549 }
550 return variables;
551 }
552 isBuildVariable(variable, line) {
553 if (this.getKeyword() === main_1.Keyword.FROM) {
554 for (const initialArg of this.dockerfile.getInitialARGs()) {
555 const arg = initialArg;
556 const property = arg.getProperty();
557 if (property && variable === property.getName()) {
558 return true;
559 }
560 }
561 return undefined;
562 }
563 let image = this.dockerfile.getContainingImage(vscode_languageserver_types_1.Position.create(line, 0));
564 let envs = image.getENVs();
565 for (let i = envs.length - 1; i >= 0; i--) {
566 if (envs[i].isBefore(line)) {
567 for (let property of envs[i].getProperties()) {
568 if (property.getName() === variable) {
569 return false;
570 }
571 }
572 }
573 }
574 let args = image.getARGs();
575 for (let i = args.length - 1; i >= 0; i--) {
576 if (args[i].isBefore(line)) {
577 let property = args[i].getProperty();
578 if (property && property.getName() === variable) {
579 return true;
580 }
581 }
582 }
583 return undefined;
584 }
585}
586exports.Instruction = Instruction;