diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e4d1e9..abff6e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog All notable changes to this project will be documented in this file. +## [Unreleased] +### Added +- support navigating the definitions of heredocs ([#122](https://github.com/rcjsuen/dockerfile-language-service/issues/122)) + ## [0.11.0] - 2023-09-10 ### Added - support parsing the new `--start-interval` flag for HEALTHCHECK instructions ([rcjsuen/dockerfile-utils#115](https://github.com/rcjsuen/dockerfile-utils/issues/115)) diff --git a/src/dockerDefinition.ts b/src/dockerDefinition.ts index 213cd42..5a03cf7 100644 --- a/src/dockerDefinition.ts +++ b/src/dockerDefinition.ts @@ -8,7 +8,7 @@ import { Position, Range, Location, TextDocumentIdentifier } from 'vscode-langua import { Util } from './docker'; import { DockerfileParser, Dockerfile, ImageTemplate, - Property, Arg, Env + Arg, Env, Heredoc, Property, Copy, Run } from 'dockerfile-ast'; export class DockerDefinition { @@ -127,6 +127,37 @@ export class DockerDefinition { return DockerDefinition.computeVariableDefinition(image, position); } + private static checkHeredocs(heredocs: Heredoc[], position: Position): Range | null { + for (const heredoc of heredocs) { + const nameRange = heredoc.getNameRange(); + if (Util.isInsideRange(position, nameRange)) { + return Util.isEmpty(nameRange) ? null : nameRange; + } + const delimiterRange = heredoc.getDelimiterRange(); + if (delimiterRange !== null && Util.isInsideRange(position, delimiterRange)) { + return nameRange; + } + } + return null; + } + + private static computeHeredocDefinition(dockerfile: Dockerfile, position: Position): Range | null { + for (const instruction of dockerfile.getInstructions()) { + if (instruction instanceof Copy) { + const range = DockerDefinition.checkHeredocs(instruction.getHeredocs(), position); + if (range !== null) { + return range; + } + } else if (instruction instanceof Run) { + const range = DockerDefinition.checkHeredocs(instruction.getHeredocs(), position); + if (range !== null) { + return range; + } + } + } + return null; + } + private computeVariableDefinition(dockerfile: Dockerfile, position: Position): Range | null { const property = DockerDefinition.findDefinition(dockerfile, position); return property ? property.getNameRange() : null; @@ -148,7 +179,10 @@ export class DockerDefinition { if (range !== null) { return Location.create(textDocument.uri, range); } - + range = DockerDefinition.computeHeredocDefinition(dockerfile, position); + if (range !== null) { + return Location.create(textDocument.uri, range); + } return null; } } diff --git a/test/dockerDefinition.test.ts b/test/dockerDefinition.test.ts index 1abded5..39cc71e 100644 --- a/test/dockerDefinition.test.ts +++ b/test/dockerDefinition.test.ts @@ -4,7 +4,7 @@ * ------------------------------------------------------------------------------------------ */ import * as assert from "assert"; -import { Position, Location, TextDocumentIdentifier } from 'vscode-languageserver-types'; +import { Location, Position, TextDocumentIdentifier } from 'vscode-languageserver-types'; import { DockerfileLanguageServiceFactory } from '../src/main'; const URI = "uri://host/Dockerfile.sample"; @@ -23,6 +23,7 @@ function findDefinition(content: string, line: number, character: number): Locat } function assertLocation(location: Location, startLine: number, startCharacter: number, endLine: number, endCharacter: number) { + assert.notStrictEqual(location, null); assert.strictEqual(location.uri, URI); assert.strictEqual(location.range.start.line, startLine); assert.strictEqual(location.range.start.character, startCharacter); @@ -1816,4 +1817,112 @@ describe("Dockerfile Document Definition tests", function () { }); }); }); + + function createRegularHeredocTests(instruction: string, offset: number) { + const tests = [ + { + testName: `< { + tests.forEach((test) => { + describe(test.testName, () => { + it("definition", () => { + const location = findDefinition(test.content, 1, offset + 11); + assertLocation(location, 1, offset + test.offset, 1, offset + test.offset + 4); + }); + + it("delimiter", () => { + const location = findDefinition(test.content, 3, 2); + assertLocation(location, 1, offset + test.offset, 1, offset + test.offset + 4); + }); + }); + }); + }); + } + + function createEmptyHeredocTests(instruction: string, offset: number) { + const tests = [ + { + testName: `<<`, + content: `FROM alpine\n${instruction} echo <<`, + offset: 8 + }, + { + testName: `<<-`, + content: `FROM alpine\n${instruction} echo <<-`, + offset: 9 + }, + { + testName: `<<''`, + content: `FROM alpine\n${instruction} echo <<''`, + offset: 9 + }, + { + testName: `<<''`, + content: `FROM alpine\n${instruction} echo <<-''`, + offset: 10 + }, + { + testName: `<<""`, + content: `FROM alpine\n${instruction} echo <<""`, + offset: 9 + }, + { + testName: `<<-""`, + content: `FROM alpine\n${instruction} echo <<-""`, + offset: 10 + } + ]; + + describe("empty", () => { + tests.forEach((test) => { + it(test.testName, () => { + const location = findDefinition(test.content, 1, offset + test.offset); + assert.strictEqual(location, null); + }); + }); + }); + } + + function createHeredocTests(instruction: string) { + describe(instruction, () => { + const offset = instruction.length; + createRegularHeredocTests(instruction, offset); + createEmptyHeredocTests(instruction, offset); + }); + } + + describe("Heredoc", () => { + createHeredocTests("COPY"); + createHeredocTests("RUN"); + }); });