-
Notifications
You must be signed in to change notification settings - Fork 123
Expand file tree
/
Copy pathTextDocumentSyncHandler.cs
More file actions
217 lines (200 loc) · 11.1 KB
/
TextDocumentSyncHandler.cs
File metadata and controls
217 lines (200 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
using System.Collections.Immutable;
using System.Text;
using MediatR;
using Microsoft.ApplicationInspector.RulesEngine;
using Microsoft.DevSkim;
using Microsoft.DevSkim.LanguageProtoInterop;
using Microsoft.Extensions.Logging;
using OmniSharp.Extensions.LanguageServer.Protocol;
using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities;
using OmniSharp.Extensions.LanguageServer.Protocol.Document;
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
using OmniSharp.Extensions.LanguageServer.Protocol.Server;
using OmniSharp.Extensions.LanguageServer.Protocol.Server.Capabilities;
using Range = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range;
namespace DevSkim.LanguageServer
{
public class TextDocumentSyncHandler : TextDocumentSyncHandlerBase
{
private readonly ILogger<TextDocumentSyncHandler> _logger;
private readonly ILanguageServerFacade _facade;
private readonly TextDocumentSelector _documentSelector = TextDocumentSelector.ForLanguage(StaticScannerSettings.RuleProcessorOptions.Languages.GetNames());
private DevSkimRuleProcessor _processor => StaticScannerSettings.Processor;
public TextDocumentSyncHandler(ILogger<TextDocumentSyncHandler> logger, ILanguageServerFacade facade)
{
_facade = facade;
_logger = logger;
}
public TextDocumentSyncKind Change { get; } = TextDocumentSyncKind.Full;
/// <summary>
/// Public method to trigger document scanning on demand
/// </summary>
public async Task ScanDocumentAsync(string text, int? version, DocumentUri uri)
{
await GenerateDiagnosticsForTextDocumentAsync(text, version, uri);
}
private async Task<Unit> GenerateDiagnosticsForTextDocumentAsync(string text, int? version, DocumentUri uri)
{
if (string.IsNullOrEmpty(text))
{
_logger.LogDebug("\tNo content found");
return Unit.Value;
}
string filename = uri.Path;
if (StaticScannerSettings.IgnoreFiles.Any(x => x.IsMatch(filename)))
{
_logger.LogDebug($"\t{filename} was excluded due to matching IgnoreFiles setting");
return Unit.Value;
}
// Diagnostics are sent a document at a time
_logger.LogDebug($"\tProcessing document: {filename}");
List<Issue> issues = await Task.Run(() => _processor.Analyze(text, filename).ToList());
ImmutableArray<Diagnostic>.Builder diagnostics = ImmutableArray<Diagnostic>.Empty.ToBuilder();
ImmutableArray<CodeFixMapping>.Builder codeFixes = ImmutableArray<CodeFixMapping>.Empty.ToBuilder();
_logger.LogDebug($"\tAdding {issues.Count} issues to diagnostics");
foreach (Issue issue in issues)
{
if (!issue.IsSuppressionInfo)
{
Diagnostic diag = new Diagnostic()
{
Code = $"{ConfigHelpers.Section}: {issue.Rule.Id}",
Severity = DevSkimSeverityToDiagnositicSeverity(issue.Rule.Severity),
Message = $"{issue.Rule.Description ?? string.Empty}",
// DevSkim/Application Inspector line numbers are one-indexed, but column numbers are zero-indexed
// To get the diagnostic to appear on the correct line, we must subtract 1 from the line number
Range = new Range(issue.StartLocation.Line - 1, issue.StartLocation.Column, issue.EndLocation.Line - 1, issue.EndLocation.Column),
Source = "DevSkim Language Server"
};
diagnostics.Add(diag);
for (int i = 0; i < issue.Rule.Fixes?.Count; i++)
{
CodeFix fix = issue.Rule.Fixes[i];
var targetText = text.Substring(issue.Boundary.Index, issue.Boundary.Length);
if (fix.Replacement is not null && DevSkimRuleProcessor.IsFixable(targetText, fix))
{
string? potentialFix = DevSkimRuleProcessor.Fix(targetText, fix);
if (potentialFix is { })
{
codeFixes.Add(new CodeFixMapping(diag, potentialFix, uri.ToUri(), $"Replace with {potentialFix}", version, issue.Boundary.Index, issue.Boundary.Index + issue.Boundary.Length, false));
}
}
}
// Add suppression options
if (StaticScannerSettings.RuleProcessorOptions.EnableSuppressions)
{
// TODO: We should check if there is an existing, expired suppression to update, and if so the replacement range needs to include the old suppression
// TODO: Handle multiple suppressions on one line?
string proposedSuppression = DevSkimRuleProcessor.GenerateSuppressionByFileName(filename, issue.Rule.Id, StaticScannerSettings.SuppressionStyle == SuppressionStyle.Block, 0, StaticScannerSettings.ReviewerName, StaticScannerSettings.RuleProcessorOptions.Languages);
if (!string.IsNullOrEmpty(proposedSuppression))
{
codeFixes.Add(new CodeFixMapping(diag, $" {proposedSuppression}", uri.ToUri(), $"Suppress {issue.Rule.Id}", version, issue.Boundary.Index, issue.Boundary.Index + issue.Boundary.Length, true));
if (StaticScannerSettings.SuppressionDuration > -1)
{
DateTime expiration = DateTime.Now.AddDays(StaticScannerSettings.SuppressionDuration);
string proposedTimedSuppression = DevSkimRuleProcessor.GenerateSuppressionByFileName(filename, issue.Rule.Id, StaticScannerSettings.SuppressionStyle == SuppressionStyle.Block, StaticScannerSettings.SuppressionDuration, StaticScannerSettings.ReviewerName, StaticScannerSettings.RuleProcessorOptions.Languages);
if (!string.IsNullOrEmpty(proposedSuppression))
{
codeFixes.Add(new CodeFixMapping(diag, $" {proposedTimedSuppression}", uri.ToUri(), $"Suppress {issue.Rule.Id} until {expiration.ToString("yyyy-MM-dd")}", version, issue.Boundary.Index, issue.Boundary.Index + issue.Boundary.Length, true));
}
}
}
}
}
}
_logger.LogDebug("\tPublishing diagnostics...");
_facade.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams()
{
Diagnostics = new Container<Diagnostic>(diagnostics.ToArray()),
Uri = uri,
Version = version
});
_facade.TextDocument.SendNotification(DevSkimMessages.FileVersion, new MappingsVersion() { version = version, fileName = uri.ToUri() });
foreach (var mapping in codeFixes)
{
_facade.TextDocument.SendNotification(DevSkimMessages.CodeFixMapping, mapping);
}
return Unit.Value;
}
private static DiagnosticSeverity DevSkimSeverityToDiagnositicSeverity(Severity ruleSeverity)
{
return ruleSeverity switch
{
Severity.Unspecified => DiagnosticSeverity.Hint,
Severity.Critical => DiagnosticSeverity.Error,
Severity.Important => DiagnosticSeverity.Error,
Severity.Moderate => DiagnosticSeverity.Warning,
Severity.BestPractice => DiagnosticSeverity.Hint,
Severity.ManualReview => DiagnosticSeverity.Hint,
_ => DiagnosticSeverity.Information
};
}
public override async Task<Unit> Handle(DidChangeTextDocumentParams request, CancellationToken cancellationToken)
{
_logger.LogDebug("TextDocumentSyncHandler.cs: DidChangeTextDocumentParams");
if (StaticScannerSettings.ScanOnChange)
{
TextDocumentContentChangeEvent? content = request.ContentChanges.FirstOrDefault();
if (content is null)
{
_logger.LogDebug("\tNo content found");
return Unit.Value;
}
return await GenerateDiagnosticsForTextDocumentAsync(content.Text, request.TextDocument.Version, request.TextDocument.Uri);
}
return Unit.Value;
}
public override async Task<Unit> Handle(DidOpenTextDocumentParams request, CancellationToken cancellationToken)
{
_logger.LogDebug("TextDocumentSyncHandler.cs: DidOpenTextDocumentParams");
if (StaticScannerSettings.ScanOnOpen)
{
TextDocumentItem content = request.TextDocument;
return await GenerateDiagnosticsForTextDocumentAsync(content.Text, content.Version, request.TextDocument.Uri);
}
return Unit.Value;
}
public override Task<Unit> Handle(DidCloseTextDocumentParams request, CancellationToken cancellationToken)
{
_logger.LogDebug("TextDocumentSyncHandler.cs: DidCloseTextDocumentParams");
if (StaticScannerSettings.RemoveFindingsOnClose)
{
_facade.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams()
{
Diagnostics = new Container<Diagnostic>(),
Uri = request.TextDocument.Uri,
Version = null
});
}
return Unit.Task;
}
public override async Task<Unit> Handle(DidSaveTextDocumentParams request, CancellationToken cancellationToken)
{
_logger.LogDebug("TextDocumentSyncHandler.cs: DidSaveTextDocumentParams");
if (StaticScannerSettings.ScanOnSave)
{
if (request.Text is null)
{
_logger.LogDebug("\tNo content found");
return Unit.Value;
}
return await GenerateDiagnosticsForTextDocumentAsync(request.Text, null, request.TextDocument.Uri);
}
return Unit.Value;
}
public override TextDocumentAttributes GetTextDocumentAttributes(DocumentUri uri)
{
if (StaticScannerSettings.RuleProcessorOptions.Languages.FromFileNameOut(uri.GetFileSystemPath(), out LanguageInfo Info))
{
return new TextDocumentAttributes(uri, Info.Name);
}
return new TextDocumentAttributes(uri, "unknown");
}
protected override TextDocumentSyncRegistrationOptions CreateRegistrationOptions(TextSynchronizationCapability capability, ClientCapabilities clientCapabilities) => new TextDocumentSyncRegistrationOptions()
{
DocumentSelector = _documentSelector,
Change = Change,
Save = new SaveOptions() { IncludeText = true }
};
}
}