@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.
- package/LICENSE +17 -0
- package/README.md +207 -0
- package/package.json +37 -0
- 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();
|