Skip to content

Commit 8952a69

Browse files
feat: Run and report diagnostics from protoc upon save (#62)
Closes #28
1 parent ce4b8c0 commit 8952a69

14 files changed

+203
-47
lines changed

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,9 @@ Protols is configured using a `protols.toml` file, which you can place in any di
8282
include_paths = ["foobar", "bazbaaz"] # Include paths to look for protofiles during parsing
8383
disable_parse_diagnostics = true # Disable diagnostics for parsing
8484

85-
[config.experimental] # Experimental configuration; this should be considered unsafe and not fully tested
86-
use_protoc_diagnostics = true # Use diagnostics from protoc
85+
[config.experimental] # experimental configuration; this should be considered unsafe and not fully tested
86+
use_protoc_diagnostics = true # use diagnostics from protoc
87+
protoc_path = "protoc" # Path to proto compiler (protoc)
8788

8889
[formatter] # Formatter specific configuration
8990
clang_format_path = "/usr/bin/clang-format" # clang-format binary to execute in formatting
@@ -96,13 +97,14 @@ clang_format_path = "/usr/bin/clang-format" # clang-format binary to execute in
9697
The `[config]` section contains stable settings that should generally remain unchanged.
9798

9899
- `include_paths`: Directories to search for `.proto` files. Absolute or relative to LSP workspace root.
99-
- `disable_parse_diagnostics`: Set to `true` to disable diagnostics during parsing.
100+
- `disable_parse_diagnostics`: Set to `true` to disable tree-sitter parse diagnostics during parsing.
100101

101102
#### Experimental Configuration
102103

103104
The `[config.experimental]` section contains settings that are in development or not fully tested.
104105

105106
- `use_protoc_diagnostics`: Enable diagnostics from the `protoc` compiler when set to `true`.
107+
- `protoc_path`: Uses protoc from this path for diagnostics
106108

107109
#### Formatter Configuration
108110

protols.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
[config]
22
include_paths = ["src/workspace/input"]
3+
4+
[config.experimental]
5+
use_protoc_diagnostics = true

sample/simple.proto

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ syntax = "proto3";
22

33
package com.book;
44

5+
import "google/protobuf/any.proto";
6+
57
// This is a book represeted by some comments that we like to address in the
68
// review
79
message Book {
@@ -24,6 +26,7 @@ message Book {
2426
}
2527

2628
enum BookState {
29+
UNSPECIFIED = 0;
2730
HARD_COVER = 1;
2831
SOFT_COVER = 2;
2932
}
@@ -52,10 +55,10 @@ service BookService {
5255

5356
message BookStore {
5457
reserved 1;
55-
Book book = 0;
56-
string name = 1;
57-
map<int64, string> books = 2;
58-
EnumSample sample = 3;
58+
Book book = 5;
59+
string name = 2;
60+
map<int64, string> books = 3;
61+
EnumSample sample = 4;
5962
}
6063

6164
// These are enum options representing some operation in the proto

src/config/mod.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ fn default_clang_format_path() -> String {
66
"clang-format".to_string()
77
}
88

9+
fn default_protoc_path() -> String {
10+
"protoc".to_string()
11+
}
12+
913
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
1014
#[serde(default)]
1115
pub struct ProtolsConfig {
@@ -28,10 +32,11 @@ pub struct Config {
2832
pub experimental: ExperimentalConfig,
2933
}
3034

31-
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
35+
#[derive(Serialize, Deserialize, Debug, Clone)]
3236
#[serde(default)]
3337
pub struct ExperimentalConfig {
3438
pub use_protoc_diagnostics: bool,
39+
pub protoc_path: String,
3540
}
3641

3742
impl Default for FormatterConfig {
@@ -41,3 +46,12 @@ impl Default for FormatterConfig {
4146
}
4247
}
4348
}
49+
50+
impl Default for ExperimentalConfig {
51+
fn default() -> Self {
52+
Self {
53+
protoc_path: default_protoc_path(),
54+
use_protoc_diagnostics: false,
55+
}
56+
}
57+
}

src/config/snapshots/protols__config__workspace__test__get_for_workspace-2.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ config:
88
disable_parse_diagnostics: false
99
experimental:
1010
use_protoc_diagnostics: false
11+
protoc_path: protoc
1112
formatter:
1213
clang_format_path: clang-format

src/config/snapshots/protols__config__workspace__test__get_for_workspace.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ config:
1010
disable_parse_diagnostics: true
1111
experimental:
1212
use_protoc_diagnostics: true
13+
protoc_path: protoc
1314
formatter:
1415
clang_format_path: /usr/bin/clang-format

src/config/workspace.rs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,18 @@ pub struct WorkspaceProtoConfigs {
2121

2222
impl WorkspaceProtoConfigs {
2323
pub fn new() -> Self {
24+
// Try to find protobuf library and get its include paths
25+
let protoc_include_prefix = Config::new()
26+
.atleast_version("3.0.0")
27+
.probe("protobuf")
28+
.map(|lib| lib.include_paths)
29+
.unwrap_or_default();
30+
2431
Self {
25-
workspaces: Default::default(),
26-
formatters: Default::default(),
27-
protoc_include_prefix: Config::new()
28-
.atleast_version("3.0.0")
29-
.probe("protobuf")
30-
.map(|l| l.include_paths)
31-
.unwrap_or_default(),
32-
configs: Default::default(),
32+
workspaces: HashSet::new(),
33+
formatters: HashMap::new(),
34+
configs: HashMap::new(),
35+
protoc_include_prefix,
3336
}
3437
}
3538

src/lsp.rs

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,26 @@ impl LanguageServer for ProtoLanguageServer {
392392
Box::pin(async move { Ok(response) })
393393
}
394394

395-
fn did_save(&mut self, _: DidSaveTextDocumentParams) -> Self::NotifyResult {
395+
fn did_save(&mut self, params: DidSaveTextDocumentParams) -> Self::NotifyResult {
396+
let uri = params.text_document.uri;
397+
let content = self.state.get_content(&uri);
398+
399+
let Some(ipath) = self.configs.get_include_paths(&uri) else {
400+
return ControlFlow::Continue(());
401+
};
402+
403+
let Some(pconf) = self.configs.get_config_for_uri(&uri) else {
404+
return ControlFlow::Continue(());
405+
};
406+
407+
if let Some(diagnostics) = self
408+
.state
409+
.upsert_file(&uri, content, &ipath, 8, &pconf.config)
410+
{
411+
if let Err(e) = self.client.publish_diagnostics(diagnostics) {
412+
error!(error=%e, "failed to publish diagnostics")
413+
}
414+
}
396415
ControlFlow::Continue(())
397416
}
398417

@@ -408,15 +427,14 @@ impl LanguageServer for ProtoLanguageServer {
408427
return ControlFlow::Continue(());
409428
};
410429

411-
let Some(diagnostics) = self.state.upsert_file(&uri, content.clone(), &ipath, 8) else {
412-
return ControlFlow::Continue(());
413-
};
414-
415430
let Some(pconf) = self.configs.get_config_for_uri(&uri) else {
416431
return ControlFlow::Continue(());
417432
};
418433

419-
if !pconf.config.disable_parse_diagnostics {
434+
if let Some(diagnostics) = self
435+
.state
436+
.upsert_file(&uri, content, &ipath, 8, &pconf.config)
437+
{
420438
if let Err(e) = self.client.publish_diagnostics(diagnostics) {
421439
error!(error=%e, "failed to publish diagnostics")
422440
}
@@ -432,20 +450,18 @@ impl LanguageServer for ProtoLanguageServer {
432450
return ControlFlow::Continue(());
433451
};
434452

435-
let Some(diagnostics) = self.state.upsert_file(&uri, content, &ipath, 2) else {
436-
return ControlFlow::Continue(());
437-
};
438-
439-
let Some(ws) = self.configs.get_config_for_uri(&uri) else {
453+
let Some(pconf) = self.configs.get_config_for_uri(&uri) else {
440454
return ControlFlow::Continue(());
441455
};
442456

443-
if !ws.config.disable_parse_diagnostics {
457+
// override config to disable protoc diagnostics during change
458+
let mut pconf = pconf.config.clone();
459+
pconf.experimental.use_protoc_diagnostics = false;
460+
if let Some(diagnostics) = self.state.upsert_file(&uri, content, &ipath, 8, &pconf) {
444461
if let Err(e) = self.client.publish_diagnostics(diagnostics) {
445462
error!(error=%e, "failed to publish diagnostics")
446463
}
447464
}
448-
449465
ControlFlow::Continue(())
450466
}
451467

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ mod formatter;
1515
mod lsp;
1616
mod nodekind;
1717
mod parser;
18+
mod protoc;
1819
mod server;
1920
mod state;
2021
mod utils;

src/protoc.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
use crate::utils::ts_to_lsp_position;
2+
use async_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Range};
3+
use std::process::Command;
4+
use tree_sitter::Point;
5+
6+
pub struct ProtocDiagnostics {}
7+
8+
impl ProtocDiagnostics {
9+
pub fn new() -> Self {
10+
Self {}
11+
}
12+
13+
pub fn collect_diagnostics(
14+
&self,
15+
protoc_path: &str,
16+
file_path: &str,
17+
include_paths: &[String],
18+
) -> Vec<Diagnostic> {
19+
let mut cmd = Command::new(protoc_path);
20+
21+
// Add include paths
22+
for path in include_paths {
23+
cmd.arg("-I").arg(path);
24+
}
25+
26+
// Generate descriptor but discard its output
27+
cmd.arg("-o")
28+
.arg(if cfg!(windows) { "NUL" } else { "/dev/null" });
29+
30+
// Add the file to check
31+
cmd.arg(file_path);
32+
33+
// Run protoc and capture output
34+
match cmd.output() {
35+
Ok(output) => {
36+
if !output.status.success() {
37+
let error = String::from_utf8_lossy(&output.stderr);
38+
self.parse_protoc_output(&error)
39+
} else {
40+
Vec::new()
41+
}
42+
}
43+
Err(e) => {
44+
tracing::error!(error=%e, "failed to run protoc");
45+
Vec::new()
46+
}
47+
}
48+
}
49+
50+
fn parse_protoc_output(&self, output: &str) -> Vec<Diagnostic> {
51+
let mut diagnostics = Vec::new();
52+
53+
for line in output.lines() {
54+
// Parse protoc error format: file:line:column: message
55+
if let Some((file_info, message)) = line.split_once(": ") {
56+
let parts: Vec<&str> = file_info.split(':').collect();
57+
if parts.len() >= 3 {
58+
if let (Ok(line), Ok(col)) = (parts[1].parse::<u32>(), parts[2].parse::<u32>())
59+
{
60+
let point = Point {
61+
row: (line - 1) as usize,
62+
column: (col - 1) as usize,
63+
};
64+
let diagnostic = Diagnostic {
65+
range: Range {
66+
start: ts_to_lsp_position(&point),
67+
end: ts_to_lsp_position(&Point {
68+
row: point.row,
69+
column: point.column + 1,
70+
}),
71+
},
72+
severity: Some(DiagnosticSeverity::ERROR),
73+
source: Some("protoc".to_string()),
74+
message: message.to_string(),
75+
..Default::default()
76+
};
77+
diagnostics.push(diagnostic);
78+
}
79+
}
80+
}
81+
}
82+
83+
diagnostics
84+
}
85+
}

src/state.rs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,19 @@ use tree_sitter::Node;
1212
use walkdir::WalkDir;
1313

1414
use crate::{
15+
config::Config,
1516
nodekind::NodeKind,
1617
parser::{ParsedTree, ProtoParser},
1718
};
1819

20+
use crate::protoc::ProtocDiagnostics;
21+
1922
pub struct ProtoLanguageState {
2023
documents: Arc<RwLock<HashMap<Url, String>>>,
2124
trees: Arc<RwLock<HashMap<Url, ParsedTree>>>,
2225
parser: Arc<Mutex<ProtoParser>>,
2326
parsed_workspaces: Arc<RwLock<HashSet<String>>>,
27+
protoc_diagnostics: Arc<Mutex<ProtocDiagnostics>>,
2428
}
2529

2630
impl ProtoLanguageState {
@@ -30,6 +34,7 @@ impl ProtoLanguageState {
3034
trees: Default::default(),
3135
parser: Arc::new(Mutex::new(ProtoParser::new())),
3236
parsed_workspaces: Arc::new(RwLock::new(HashSet::new())),
37+
protoc_diagnostics: Arc::new(Mutex::new(ProtocDiagnostics::new())),
3338
}
3439
}
3540

@@ -217,13 +222,34 @@ impl ProtoLanguageState {
217222
content: String,
218223
ipath: &[PathBuf],
219224
depth: usize,
225+
config: &Config,
220226
) -> Option<PublishDiagnosticsParams> {
221227
info!(%uri, %depth, "upserting file");
222228
let diag = self.upsert_content(uri, content.clone(), ipath, depth);
223229
self.get_tree(uri).map(|tree| {
224-
let diag = tree.collect_import_diagnostics(content.as_ref(), diag);
225-
let mut d = tree.collect_parse_diagnostics();
226-
d.extend(diag);
230+
let mut d = vec![];
231+
if !config.disable_parse_diagnostics {
232+
d.extend(tree.collect_parse_diagnostics());
233+
}
234+
d.extend(tree.collect_import_diagnostics(content.as_ref(), diag));
235+
236+
// Add protoc diagnostics if enabled
237+
if config.experimental.use_protoc_diagnostics {
238+
if let Ok(protoc_diagnostics) = self.protoc_diagnostics.lock() {
239+
if let Ok(file_path) = uri.to_file_path() {
240+
let protoc_diags = protoc_diagnostics.collect_diagnostics(
241+
&config.experimental.protoc_path,
242+
file_path.to_str().unwrap_or_default(),
243+
&ipath
244+
.iter()
245+
.map(|p| p.to_str().unwrap_or_default().to_string())
246+
.collect::<Vec<_>>(),
247+
);
248+
d.extend(protoc_diags);
249+
}
250+
}
251+
}
252+
227253
PublishDiagnosticsParams {
228254
uri: tree.uri.clone(),
229255
diagnostics: d,

0 commit comments

Comments
 (0)