Git Product home page Git Product logo

dockerfile-language-server's Introduction

Dockerfile Language Server

Node.js Builds Coverage Status Build Dependencies License: MIT

This is a language server for Dockerfiles powered by Node.js written in TypeScript. To install and run this language server, you will need to have either Node.js or Docker installed on your computer.

Supported features:

  • code actions
  • code completion
  • definition
  • diagnostics
  • document highlight
  • document links
  • document symbols
  • folding ranges
  • formatting
  • hovers
  • prepare rename
  • rename
  • semantic highlighting
  • signature help

Projects that use this language server:

This repository only contains the code necessary for launching a Dockerfile language server that conforms to the language server protocol. The actual code for parsing a Dockerfile and offering editor features such as code completion or hovers is not contained within this repository.

The code for analyzing and processing a Dockerfile is contained in the following three libraries:

All of the language server protocol requests that help create a rich editing experience for the user is forwarded to the dockerfile-language-service library. You can test its features right in the browser. This online editor is a very good representation of what is possible when this language server is connected to an editor that supports the language server protocol.

Development Instructions

If you wish to build and compile this language server, you must first install Node.js if you have not already done so. After you have installed Node.js and cloned the repository with Git, you may now proceed to build and compile the language server with the following commands:

npm install
npm run build
npm test

If you are planning to change the code, use npm run watch to get the TypeScript files transpiled on-the-fly as they are modified.

Once the code has finished compiling, you can connect a language server client to the server via Node IPC, stdio, or sockets.

Installation Instructions

To install this language server onto your computer, please install the dockerfile-language-server-nodejs npm module. The -g flag will install the npm module globally onto your computer.

npm install -g dockerfile-language-server-nodejs

After the installation has completed, you can start the language server with the docker-langserver binary. You should specify the desired method of communicating with the language server via one of the three arguments shown below.

docker-langserver --node-ipc
docker-langserver --stdio
docker-langserver --socket=<port>

Docker Image

The docker-langserver binary is also available as a Docker image under the name rcjsuen/docker-langserver.

Language Server Settings

Clients may send a workspace/didChangeConfiguration notification to notify the server of settings changes.

The settings object that will be included with the notification must conform to the following specification.

interface Settings {
  docker: {
    languageserver: {
      diagnostics?: {
        // string values must be equal to "ignore", "warning", or "error"
        deprecatedMaintainer?: string,
        directiveCasing?: string,
        emptyContinuationLine?: string,
        instructionCasing?: string,
        instructionCmdMultiple?: string,
        instructionEntrypointMultiple?: string,
        instructionHealthcheckMultiple?: string,
        instructionJSONInSingleQuotes?: string
      },
      formatter?: {
        ignoreMultilineInstructions?: boolean,
      }
    }
  }
}

Communicating with the Server

Node IPC

With the child_process API, you can fork() a new Node.js process running the language server and communicate with it using send(message) and on('message', ...).

import * as child_process from "child_process";

let lspProcess = child_process.fork("out/src/server.js", [ "--node-ipc" ]);
let messageId = 1;

function send(method: string, params: object) {
    let message = {
        jsonrpc: "2.0",
        id: messageId++,
        method: method,
        params: params
    };
    lspProcess.send(message);
}

function initialize() {
    send("initialize", {
        rootPath: process.cwd(),
        processId: process.pid,
        capabilities: {
            /* ... */
        }
    });
}


lspProcess.on('message', function (json) {
    console.log(json);
});
initialize();

Standard Input/Output

When writing directly to the process's stdin, the additional Content-Length header must be included. Similarly, when reading from the process's stdout, the header will be included in the response message.

import * as child_process from "child_process";

let lspProcess = child_process.spawn("node", [ "out/src/server.js", "--stdio" ]);
let messageId = 1;

function send(method: string, params: object) {
    let message = {
        jsonrpc: "2.0",
        id: messageId++,
        method: method,
        params: params
    };
    let json = JSON.stringify(message);
    let headers = "Content-Length: " + json.length + "\r\n\r\n";
    lspProcess.stdin.write(headers, "ASCII");
    lspProcess.stdin.write(json, "UTF-8");
}

function initialize() {
    send("initialize", {
        rootPath: process.cwd(),
        processId: process.pid,
        capabilities: {
            /* ... */
        }
    });
}

lspProcess.stdout.on("data", (message) => {
    // "Content-Length: ...\r\n\r\n\" will be included here
    console.log(message.toString());
});

initialize();

vscode-jsonrpc

The StreamMessageReader and StreamMessageWriter classes from the vscode-jsonrpc module will handle the Content-Length headers for you so you only have to worry about the actual request and response.

import * as child_process from "child_process";
import { StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc";

let lspProcess = child_process.spawn("node", [ "out/src/server.js", "--stdio" ]);
let messageId = 1;

const reader = new StreamMessageReader(lspProcess.stdout);
const writer = new StreamMessageWriter(lspProcess.stdin);

function send(method: string, params: object) {
    let message = {
        jsonrpc: "2.0",
        id: messageId++,
        method: method,
        params: params
    };
    writer.write(message);
}

function initialize() {
    send("initialize", {
        rootPath: process.cwd(),
        processId: process.pid,
        capabilities: {
            /* ... */
        }
    });
}

reader.listen((data) => {
    console.log(data);
})

initialize();

Sockets

To communicate with the langauge server via a socket, a port must be opened up first to listen for incoming connections. After the port is opened, the language server may be started and told to connect to the specified port. Messages can then be read from and written to the socket.

Just like when trying to communicate to the server using stdio, the Content-Length headers must be written and parsed explicitly.

import * as net from "net"
import * as child_process from "child_process"

let messageId = 1;

function send(socket: net.Socket, method: string, params: object) {
    let message = {
        jsonrpc: "2.0",
        id: messageId++,
        method: method,
        params: params
    };
    let json = JSON.stringify(message) + "\n";
    let headers = "Content-Length: " + json.length + "\r\n\r\n";
    socket.write(headers, "ASCII");
    socket.write(json, "UTF-8");
}

function initialize(socket: net.Socket) {
    send(socket, "initialize", {
        rootPath: process.cwd(),
        processId: process.pid,
        capabilities: {
            textDocument: {
                /* ... */
            },
            workspace: {
                /* ... */
            }
        }
    });
}

const server = net.createServer((socket: net.Socket) => {
    server.close();
    socket.on("data", (message) => {
        // "Content-Length: ...\r\n\r\n\" will be included here
        console.log(message.toString());
    });
    initialize(socket);
});

server.listen(3000, () => {
    child_process.spawn("node", [ "out/src/server.js", "--socket=3000" ]);
});

vscode-jsonrpc

The SocketMessageReader and SocketMessageWriter classes from the vscode-jsonrpc module will handle the Content-Length headers for you so you only have to worry about the actual request and response.

import * as net from "net"
import * as child_process from "child_process"
import { SocketMessageReader, SocketMessageWriter } from "vscode-jsonrpc";

let messageId = 1;
let reader: SocketMessageReader = null;
let writer: SocketMessageWriter = null;

function send(method: string, params: object) {
    let message = {
        jsonrpc: "2.0",
        id: messageId++,
        method: method,
        params: params
    };
    writer.write(message);
}

function initialize() {
    send("initialize", {
        rootPath: process.cwd(),
        processId: process.pid,
        capabilities: {
            textDocument: {
                /* ... */
            },
            workspace: {
                /* ... */
            }
        }
    });
}

const server = net.createServer((socket: net.Socket) => {
    server.close();
    reader = new SocketMessageReader(socket);
    reader.listen((data) => {
        console.log(data);
    });
    writer = new SocketMessageWriter(socket);
    initialize();
});

server.listen(3000, () => {
    child_process.spawn("node", [ "out/src/server.js", "--socket=3000" ]);
});

dockerfile-language-server's People

Contributors

adelarsq avatar rcjsuen avatar sasanidas avatar svenefftinge avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

dockerfile-language-server's Issues

Migrate existing code to use the new parser

A new general-purpose parser for creating an AST-like structure for a Dockerfile was implemented in #24. We should migrate our existing parsing code to use this generic parser instead.

Completion items should suggest build stage names

FROM alpine as setup
FROM alpine
COPY --from=

Given this Dockerfile, we should suggest setup as a completion item. Suggesting 1 is questionable as you're not supposed to refer to the current build stage but perhaps it is worth suggesting for the sake of completeness.

Create a validator setting to toggle the deprecated MAINTAINER instruction

The MAINTAINER instruction is currently deprecated so the validator should warn the client about this. However, some clients may wish to ignore the fact that it is deprecated. Alternatively, they may wish to have errors created if such an instruction exists in the Dockerfile. We should introduce a setting in the validator to allow this to be toggled if desired.

Allow FROM instructions to have either one or three arguments

Originally, FROM instructions only took a single argument of the following forms:

  1. FROM <image>
  2. FROM <image>[:<tag>]
  3. FROM <image>[@<digest>]

However, a new AS keyword has been introduced and a FROM instruction can now be defined by something like FROM node AS bootstrap.

As the validator currently validates FROM as an instruction that may only have one argument, we need to update it to to accept either one or three arguments (with the second being an AS).

Add support for textDocument/documentHighlight

If a user has build stages defined (as of Docker 17.06 CE), we should provide highlighting support if the build stage declaration itself in the FROM instruction or the name in the --from=name of the COPY instruction is currently selected.

Rewrite the document symbols code to create an AST

At the moment, every response handler parses a Dockerfile with its own unique code. While a Dockerfile's syntax is not exactly very complicated, a lot of code that is very similar is replicated across the entire codebase. We should rewrite a parser from scratch that will create an AST of sorts that knows where a comment, directive, and instruction line starts and ends (including any escaped newlines in the case of instructions).

To validate that this parser is working as intended, it will first be used to replace dockerSymbols.ts before further refactorings across the entire codebase.

Consolidate validation messages about a missing FROM

When a file does not have a FROM instruction as its first instruction or when the Dockerfile doesn't have any instructions, we should use one common error to notify the user that a source image must be specified.

Ranged formatting returns unnecessary edit on a well-formed line

FROM node

If you try to format this document with a valid range, the server will return one TextEdit that replaces content from Position(0, 0) to Position(0, 0) to the empty string "". This is extraneous and we should simply return an empty TextEdit[] in this case.

Add missing tests for implemented features

For the initial pass, we will need tests that test the actual leg work of each underlying feature. In the future, a separate set of tests should be created to make sure that the server is returning the right responses back to the client.

  • code actions
  • command execution
  • completion
  • diagnostics
  • document symbols
  • formatting
  • hover

Travis CI build has failed

The first build has failed. It seems that it's because the .travis.yml file is referencing an environment variable that doesn't exist.

Fix compilation error in Travis Build #84

Travis Build #84 is broken.

$ npm run build;
> [email protected] build /home/travis/build/rcjsuen/dockerfile-language-server-nodejs
> tsc -p .
parser/dockerfile.ts(10,21): error TS2307: Cannot find module './instructions/arg'.
parser/dockerfileParser.ts(12,21): error TS2307: Cannot find module './instructions/arg'.

textDocument/documentHighlight fails trying to read property of a null object

FROM node AS bootstrap
COPY bootstrap /git/build/

If you have a Dockerfile with COPY instructions that don't specify a --from= flag, an error will be thrown.

[Error - 7:52:20 PM] Request textDocument/documentHighlight failed.
  Message: Request textDocument/documentHighlight failed with message: Cannot read property 'start' of null
  Code: -32603 

Missing arguments from MAINTAINER not flagged as a diagnostic

Travis CI Build #32

  Docker Validator Tests
    missing argument
      ✓ ADD
      ✓ ARG
      ✓ CMD
      ✓ COPY
      ✓ ENTRYPOINT
      ✓ ENV
      ✓ EXPOSE
      ✓ FROM
      ✓ HEALTHCHECK
      ✓ LABEL
      1) MAINTAINER
      ✓ ONBUILD
      ✓ RUN
      ✓ SHELL
      ✓ STOPSIGNAL
      ✓ USER
      ✓ WORKDIR

  1) Docker Validator Tests missing argument MAINTAINER:
      AssertionError [ERR_ASSERTION]: 0 == 3
      + expected - actual
      -0
      +3
      
      at assertInstructionMissingArgument (out/test/dockerValidator.tests.js:1:46748)
      at testMissingArgument (out/test/dockerValidator.tests.js:1:59678)
      at Context.<anonymous> (out/test/dockerValidator.tests.js:1:62522)

The MAINTAINER-specific parsing code only flags the keyword itself as being deprecated. It should also check to see if it's actually followed by an argument and to create a MISSING_ARGUMENT diagnostic if it's not.

ARG instructions may precede a FROM instruction

If the first instruction is not a FROM, the validator will flag it as an error. However, if it seems that FROM instructions can actually be preceded by an ARG.

We need to fix the validator to consider this case so that ARG instruments that appear before a FROM do not get flagged as an error.

Formatter doesn't consider \r characters properly

On ranged selection formatting, if \r or \r\n are used for newlines and are actually escaped, the start of the line is not being calculated correctly and the formatter deems that no formatting is required.

FROM node
EXPOSE 8080\
8081

Here, 8081 should get indented if it is selected but the formatter gets confused and doesn't return a TextEdit for this scenario.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.