@sankalpmukim/hadolint-lsp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +17 -0
  2. package/README.md +207 -0
  3. package/package.json +37 -0
  4. package/src/index.js +164 -0
package/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2023 Sankalp Mukim
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
package/README.md ADDED
@@ -0,0 +1,207 @@
1
+ # Hadolint LSP
2
+
3
+ A Language Server Protocol implementation for Hadolint, the Dockerfile linter.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npx @sankalpmukim/hadolint-lsp
9
+ ```
10
+
11
+ Or install globally:
12
+
13
+ ```bash
14
+ npm install -g @sankalpmukim/hadolint-lsp
15
+ ```
16
+
17
+ ## OpenCode Integration
18
+
19
+ To use hadolint-lsp with OpenCode, add it to your `opencode.json` configuration file:
20
+
21
+ ### Local Configuration
22
+
23
+ Add to your project-local `opencode.json`:
24
+
25
+ ```json
26
+ {
27
+ "$schema": "https://opencode.ai/config.json",
28
+ "lsp": {
29
+ "hadolint": {
30
+ "command": ["npx", "-y", "@sankalpmukim/hadolint-lsp", "--stdio"],
31
+ "extensions": ["Dockerfile", "dockerfile"]
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ ### Global Configuration
38
+
39
+ Add to your global OpenCode config (usually at `~/.config/opencode/opencode.json`):
40
+
41
+ ```json
42
+ {
43
+ "$schema": "https://opencode.ai/config.json",
44
+ "lsp": {
45
+ "hadolint": {
46
+ "command": ["npx", "-y", "@sankalpmukim/hadolint-lsp", "--stdio"],
47
+ "extensions": ["Dockerfile", "dockerfile"]
48
+ }
49
+ }
50
+ }
51
+ ```
52
+
53
+ If you have installed @sankalpmukim/hadolint-lsp globally, you can use:
54
+
55
+ ```json
56
+ {
57
+ "$schema": "https://opencode.ai/config.json",
58
+ "lsp": {
59
+ "hadolint": {
60
+ "command": ["hadolint-lsp", "--stdio"],
61
+ "extensions": ["Dockerfile", "dockerfile"]
62
+ }
63
+ }
64
+ }
65
+ ```
66
+
67
+ ### Notes
68
+
69
+ - The LSP server communicates via stdio, so the `--stdio` flag is important
70
+ - The `extensions` array specifies which file extensions the LSP should handle
71
+ - OpenCode will automatically start the LSP server when you open a Dockerfile
72
+ - Make sure `hadolint` is available in your PATH (see Requirements section)
73
+
74
+ ## Requirements
75
+
76
+ This LSP server requires [hadolint](https://github.com/hadolint/hadolint) to be installed and available in your PATH.
77
+
78
+ Install hadolint:
79
+
80
+ ```bash
81
+ # On macOS
82
+ brew install hadolint
83
+
84
+ # On Linux
85
+ wget -O /usr/local/bin/hadolint https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64
86
+ chmod +x /usr/local/bin/hadolint
87
+
88
+ # Or using Docker (but LSP will not work with Docker)
89
+ docker pull hadolint/hadolint
90
+ ```
91
+
92
+ ## Usage
93
+
94
+ ### Editor Integration
95
+
96
+ #### VS Code
97
+
98
+ Create or edit `.vscode/settings.json`:
99
+
100
+ ```json
101
+ {
102
+ "dockerfile.languageserver": {
103
+ "dockerfile-language-server-node": {
104
+ "command": "@sankalpmukim/hadolint-lsp"
105
+ }
106
+ }
107
+ }
108
+ ```
109
+
110
+ Or use with Docker extension that supports LSP.
111
+
112
+ #### Vim/Neovim
113
+
114
+ With [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig):
115
+
116
+ ```lua
117
+ require'lspconfig'.hadolint_lsp.setup{
118
+ cmd = { "@sankalpmukim/hadolint-lsp" },
119
+ filetypes = { "dockerfile" },
120
+ root_dir = require('lspconfig').util.root_pattern(".hadolint.yaml"),
121
+ }
122
+ ```
123
+
124
+ #### Emacs
125
+
126
+ With [lsp-mode](https://github.com/emacs-lsp/lsp-mode):
127
+
128
+ ```elisp
129
+ (use-package lsp-mode
130
+ :config
131
+ (lsp-register-client
132
+ (make-lsp-client :new-connection (lsp-stdio-connection "@sankalpmukim/hadolint-lsp")
133
+ :major-modes '(dockerfile-mode)
134
+ :server-id 'hadolint-lsp)))
135
+ ```
136
+
137
+ ### Features
138
+
139
+ - Real-time Dockerfile linting
140
+ - Diagnostic highlighting
141
+ - Error, warning, info, and style level detection
142
+ - Supports all Hadolint rules
143
+ - Respects inline ignore pragmas (`# hadolint ignore=DLxxxx`)
144
+
145
+ ## Configuration
146
+
147
+ The LSP server respects Hadolint configuration files:
148
+ - `.hadolint.yaml` in the project root
149
+ - `$XDG_CONFIG_HOME/hadolint.yaml`
150
+ - `$HOME/.config/hadolint.yaml`
151
+ - `$HOME/.hadolint.yaml`
152
+
153
+ Example `.hadolint.yaml`:
154
+
155
+ ```yaml
156
+ failure-threshold: warning
157
+ ignored:
158
+ - DL3008
159
+ override:
160
+ error:
161
+ - DL3006
162
+ warning:
163
+ - DL3015
164
+ ```
165
+
166
+ ## Testing
167
+
168
+ Run the test suite:
169
+
170
+ ```bash
171
+ npm test
172
+ ```
173
+
174
+ The test suite covers:
175
+ - Hadolint integration
176
+ - Severity mapping
177
+ - Diagnostic generation
178
+ - Package structure validation
179
+ - Executable verification
180
+
181
+ All tests should pass (17 total tests as of v0.1.0).
182
+
183
+ ## Development
184
+
185
+ ```bash
186
+ # Install dependencies
187
+ npm install
188
+
189
+ # Run tests
190
+ npm test
191
+
192
+ # Run in development mode
193
+ node src/index.js --stdio
194
+ ```
195
+
196
+ ## License
197
+
198
+ GPL-3.0
199
+
200
+ ## Contributing
201
+
202
+ Contributions are welcome! Please feel free to submit a Pull Request.
203
+
204
+ ## Related
205
+
206
+ - [Hadolint](https://github.com/hadolint/hadolint) - The Dockerfile linter
207
+ - [Language Server Protocol](https://microsoft.github.io/language-server-protocol/)
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@sankalpmukim/hadolint-lsp",
3
+ "version": "0.1.0",
4
+ "description": "Language Server Protocol implementation for Hadolint",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "hadolint-lsp": "src/index.js"
8
+ },
9
+ "files": [
10
+ "src"
11
+ ],
12
+ "scripts": {
13
+ "test": "node test/run-tests.js",
14
+ "test:coverage": "node test/run-tests.js --coverage"
15
+ },
16
+ "keywords": [
17
+ "lsp",
18
+ "language-server",
19
+ "hadolint",
20
+ "dockerfile",
21
+ "docker"
22
+ ],
23
+ "author": "Sankalp Mukim",
24
+ "license": "GPL-3.0",
25
+ "homepage": "https://github.com/sankalpmukim/hadolint-lsp",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/sankalpmukim/hadolint-lsp.git"
29
+ },
30
+ "engines": {
31
+ "node": ">=16.0.0"
32
+ },
33
+ "dependencies": {
34
+ "vscode-languageserver": "^8.1.0",
35
+ "vscode-languageserver-textdocument": "^1.0.11"
36
+ }
37
+ }
package/src/index.js ADDED
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+ const {
3
+ createConnection,
4
+ TextDocuments,
5
+ Diagnostic,
6
+ DiagnosticSeverity,
7
+ ProposedFeatures,
8
+ InitializeParams,
9
+ DidChangeConfigurationNotification,
10
+ CompletionItem,
11
+ CompletionItemKind,
12
+ TextDocumentPositionParams,
13
+ TextDocumentSyncKind,
14
+ InitializeResult
15
+ } = require('vscode-languageserver/node');
16
+
17
+ const {
18
+ TextDocument
19
+ } = require('vscode-languageserver-textdocument');
20
+
21
+ const { spawn } = require('child_process');
22
+ const { readFileSync, unlinkSync, writeFileSync, existsSync } = require('fs');
23
+ const { tmpdir } = require('os');
24
+ const { join } = require('path');
25
+
26
+ const connection = createConnection(ProposedFeatures.all);
27
+ const documents = new TextDocuments(TextDocument);
28
+
29
+ let hasConfigurationCapability = false;
30
+ let hasWorkspaceFolderCapability = false;
31
+
32
+ connection.onInitialize((params) => {
33
+ const capabilities = params.capabilities;
34
+ hasConfigurationCapability = !!(
35
+ capabilities.workspace && !!capabilities.workspace.configuration
36
+ );
37
+ hasWorkspaceFolderCapability = !!(
38
+ capabilities.workspace && !!capabilities.workspace.workspaceFolders
39
+ );
40
+
41
+ const result = {
42
+ capabilities: {
43
+ textDocumentSync: TextDocumentSyncKind.Incremental,
44
+ completionProvider: {
45
+ resolveProvider: true
46
+ }
47
+ }
48
+ };
49
+ return result;
50
+ });
51
+
52
+ connection.onInitialized(() => {
53
+ if (hasConfigurationCapability) {
54
+ connection.client.register(
55
+ DidChangeConfigurationNotification.type,
56
+ undefined
57
+ );
58
+ }
59
+ });
60
+
61
+ async function runHadolint(content) {
62
+ return new Promise((resolve, reject) => {
63
+ const tmpFile = join(tmpdir(), `hadolint-lsp-${Date.now()}`);
64
+ writeFileSync(tmpFile, content, 'utf-8');
65
+
66
+ const hadolint = spawn('hadolint', ['--format', 'json', tmpFile], {
67
+ stdio: ['ignore', 'pipe', 'pipe']
68
+ });
69
+
70
+ let stdout = '';
71
+ let stderr = '';
72
+
73
+ hadolint.stdout.on('data', (data) => {
74
+ stdout += data.toString();
75
+ });
76
+
77
+ hadolint.stderr.on('data', (data) => {
78
+ stderr += data.toString();
79
+ });
80
+
81
+ hadolint.on('close', (code) => {
82
+ try {
83
+ if (existsSync(tmpFile)) {
84
+ unlinkSync(tmpFile);
85
+ }
86
+ } catch (e) {
87
+ }
88
+
89
+ if (stdout) {
90
+ try {
91
+ const results = JSON.parse(stdout);
92
+ const diagnostics = results.map((result) => {
93
+ const severity = mapSeverity(result.level);
94
+ return {
95
+ range: {
96
+ start: { line: result.line - 1, character: 0 },
97
+ end: { line: result.line - 1, character: 100 }
98
+ },
99
+ severity: severity,
100
+ code: result.code,
101
+ message: result.message,
102
+ source: 'hadolint'
103
+ };
104
+ });
105
+ resolve(diagnostics);
106
+ } catch (e) {
107
+ resolve([]);
108
+ }
109
+ } else {
110
+ resolve([]);
111
+ }
112
+ });
113
+
114
+ hadolint.on('error', (err) => {
115
+ try {
116
+ if (existsSync(tmpFile)) {
117
+ unlinkSync(tmpFile);
118
+ }
119
+ } catch (e) {
120
+ }
121
+ resolve([]);
122
+ });
123
+ });
124
+ }
125
+
126
+ function mapSeverity(level) {
127
+ switch (level.toLowerCase()) {
128
+ case 'error':
129
+ return DiagnosticSeverity.Error;
130
+ case 'warning':
131
+ return DiagnosticSeverity.Warning;
132
+ case 'info':
133
+ return DiagnosticSeverity.Information;
134
+ case 'style':
135
+ return DiagnosticSeverity.Hint;
136
+ default:
137
+ return DiagnosticSeverity.Warning;
138
+ }
139
+ }
140
+
141
+ documents.onDidChangeContent(async (change) => {
142
+ const text = change.document.getText();
143
+ const diagnostics = await runHadolint(text);
144
+ connection.sendDiagnostics({
145
+ uri: change.document.uri,
146
+ diagnostics
147
+ });
148
+ });
149
+
150
+ documents.listen(connection);
151
+
152
+ connection.onCompletion(
153
+ (_textDocumentPosition) => {
154
+ return [];
155
+ }
156
+ );
157
+
158
+ connection.onCompletionResolve(
159
+ (item) => {
160
+ return item;
161
+ }
162
+ );
163
+
164
+ connection.listen();