diff --git a/.gitignore b/.gitignore
index 74920eab..5e986add 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,7 @@ DevSkim-DotNet/Microsoft.DevSkim.sln.DotSettings.user
DevSkim-VSCode-Plugin/client/dist/*
DevSkim-VSCode-Plugin/devskimBinaries/*
DevSkim-DotNet/Microsoft.DevSkim.VisualStudio/generatedLanguageServerBinaries/*
+DevSkim-DotNet/Microsoft.DevSkim.VisualStudio/Server/*
# Debug artifacts
DevSkim-DotNet/Microsoft.DevSkim.VisualStudio/devskim-server-*.txt
diff --git a/Changelog.md b/Changelog.md
index 4d112400..06c398ff 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [1.0.76] - 2026-02-06
+### Changed
+- Rewrote Visual Studio extension using VisualStudio.Extensibility SDK for VS2022/2026 compatibility
+- Replaced legacy MEF-based ILanguageClient with LanguageServerProvider from new VS Extensibility SDK
+- Implemented Windows Job Object for reliable language server process cleanup when VS exits
+- Added settings management using VS Extensibility SDK settings API with localized string resources
+- Enhanced code actions and fixes handling for better VS compatibility
+
## [1.0.75] - 2026-02-06
### Changed
- Removed unnecessary uninstall/reinstall of @vscode/vsce from postinstall script in VSCode plugin
diff --git a/DevSkim-DotNet/ManualGenerator/ManualUpdateSettingsGenerator.csproj b/DevSkim-DotNet/ManualGenerator/ManualUpdateSettingsGenerator.csproj
deleted file mode 100644
index e75bc7a7..00000000
--- a/DevSkim-DotNet/ManualGenerator/ManualUpdateSettingsGenerator.csproj
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
- Exe
- net7.0
- enable
- enable
-
-
-
-
-
-
-
diff --git a/DevSkim-DotNet/ManualGenerator/Program.cs b/DevSkim-DotNet/ManualGenerator/Program.cs
deleted file mode 100644
index fb510bdc..00000000
--- a/DevSkim-DotNet/ManualGenerator/Program.cs
+++ /dev/null
@@ -1,77 +0,0 @@
-namespace ManualGenerator
-{
- using Microsoft.DevSkim.LanguageProtoInterop;
- using System.Text;
-
- ///
- /// Run this to generate the update settinsg method for all implemented settings
- ///
- internal class Program
- {
- static void Main(string[] args)
- {
- Console.WriteLine(Execute());
- }
-
- public static string Execute()
- {
- StringBuilder source = new StringBuilder();
- source.Append($@"//
- namespace Microsoft.DevSkim.VisualStudio
- {{
- using System;
- using Microsoft.DevSkim.LanguageProtoInterop;
- using System.Collections.Generic;
-
- internal partial class VisualStudioSettingsManager
- {{
- partial void UpdateSettings(string propertyName)
- {{
- switch(propertyName)
- {{
-");
-
- string baseIndentation = " ";
- var props = typeof(IDevSkimOptions).GetProperties().Select(x => (x.Name, x.PropertyType));
- foreach (var prop in props)
- {
- var typeNameString = GetTypeNameString(prop.PropertyType);
- source.Append($@"{baseIndentation}case ""{prop.Name}"":
- {{
- var res = Get<{typeNameString}>(propertyName);
- if (res.Item1 == ValueResultEnum.Success)
- {{
- _currentSettings.{prop.Name} = res.Item2{ConditionallyUseNullCoalescing(typeNameString)}
- }}
- break;
- }}
-");
- }
-
- source.Append($@"{baseIndentation}default: break;
- }}
- }}
- }}
- }}");
- return source.ToString();
- }
-
- private static string ConditionallyUseNullCoalescing(string typeNameString)
- {
- return typeNameString.StartsWith("List") ? $" ?? new {typeNameString}();" : ";";
- }
-
- private static string GetTypeNameString(Type propPropertyType)
- {
- return propPropertyType.Name switch
- {
- "String" => "string",
- "List`1" => $"List<{GetTypeNameString(propPropertyType.GetGenericArguments()[0])}>",
- "Boolean" => "bool",
- "Int32" => "int",
- "CommentStylesEnum" => "CommentStylesEnum",
- _ => "string"
- };
- }
- }
-}
\ No newline at end of file
diff --git a/DevSkim-DotNet/Microsoft.DevSkim.LanguageProtoInterop/PortableScannerSettings.cs b/DevSkim-DotNet/Microsoft.DevSkim.LanguageProtoInterop/PortableScannerSettings.cs
index 32753a84..46e25a6e 100644
--- a/DevSkim-DotNet/Microsoft.DevSkim.LanguageProtoInterop/PortableScannerSettings.cs
+++ b/DevSkim-DotNet/Microsoft.DevSkim.LanguageProtoInterop/PortableScannerSettings.cs
@@ -21,14 +21,16 @@ public class PortableScannerSettings : IDevSkimOptions
public bool RemoveFindingsOnClose { get; set; } = true;
public string CustomLanguagesPath { get; set; } = string.Empty;
public string CustomCommentsPath { get; set; } = string.Empty;
- public string GuidanceBaseURL { get; set; }
- public bool EnableCriticalSeverityRules { get; set; }
- public bool EnableImportantSeverityRules { get; set; }
- public bool EnableModerateSeverityRules { get; set; }
- public bool EnableManualReviewSeverityRules { get; set; }
- public bool EnableBestPracticeSeverityRules { get; set; }
- public bool EnableHighConfidenceRules { get; set; }
- public bool EnableLowConfidenceRules { get; set; }
- public bool EnableMediumConfidenceRules { get; set; }
+ public string GuidanceBaseURL { get; set; } = "https://github.com/microsoft/devskim/tree/main/guidance";
+ // Default all severity rules to enabled
+ public bool EnableCriticalSeverityRules { get; set; } = true;
+ public bool EnableImportantSeverityRules { get; set; } = true;
+ public bool EnableModerateSeverityRules { get; set; } = true;
+ public bool EnableManualReviewSeverityRules { get; set; } = true;
+ public bool EnableBestPracticeSeverityRules { get; set; } = true;
+ // Default high and medium confidence to enabled, low disabled
+ public bool EnableHighConfidenceRules { get; set; } = true;
+ public bool EnableLowConfidenceRules { get; set; } = false;
+ public bool EnableMediumConfidenceRules { get; set; } = true;
}
}
\ No newline at end of file
diff --git a/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/CodeActionHandler.cs b/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/CodeActionHandler.cs
new file mode 100644
index 00000000..dd020737
--- /dev/null
+++ b/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/CodeActionHandler.cs
@@ -0,0 +1,168 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Collections.Concurrent;
+using MediatR;
+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;
+
+namespace DevSkim.LanguageServer
+{
+ ///
+ /// Handles textDocument/codeAction requests from the LSP client.
+ /// Provides quick fixes for DevSkim security findings.
+ ///
+ internal class CodeActionHandler : ICodeActionHandler
+ {
+ private readonly ILogger _logger;
+
+ // Store code fixes keyed by document URI and diagnostic key
+ private static readonly ConcurrentDictionary>> _codeFixCache = new();
+
+ // Store line lengths per document so we can compute exact end-of-line positions for suppressions
+ private static readonly ConcurrentDictionary _lineLengthCache = new();
+
+ public CodeActionHandler(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public CodeActionRegistrationOptions GetRegistrationOptions(CodeActionCapability capability, ClientCapabilities clientCapabilities)
+ {
+ return new CodeActionRegistrationOptions
+ {
+ DocumentSelector = TextDocumentSelector.ForPattern("**/*"),
+ CodeActionKinds = new Container(CodeActionKind.QuickFix),
+ ResolveProvider = false
+ };
+ }
+
+ public Task Handle(CodeActionParams request, CancellationToken cancellationToken)
+ {
+ var result = new List();
+ var documentUri = request.TextDocument.Uri.ToString();
+
+ _logger.LogDebug($"CodeActionHandler: Processing request for {documentUri}");
+ _logger.LogDebug($"CodeActionHandler: {request.Context.Diagnostics.Count()} diagnostics in context");
+
+ foreach (var diagnostic in request.Context.Diagnostics)
+ {
+ // Only process DevSkim diagnostics
+ if (diagnostic.Source != "DevSkim Language Server")
+ {
+ continue;
+ }
+
+ var diagnosticKey = CreateDiagnosticKey(documentUri, diagnostic);
+
+ if (_codeFixCache.TryGetValue(documentUri, out var documentFixes) &&
+ documentFixes.TryGetValue(diagnosticKey, out var fixes))
+ {
+ _logger.LogDebug($"CodeActionHandler: Found {fixes.Count} fixes");
+ var codeActions = fixes.Select(fix => new CodeAction
+ {
+ Title = fix.friendlyString,
+ Kind = CodeActionKind.QuickFix,
+ Diagnostics = new Container(diagnostic),
+ Edit = CreateWorkspaceEdit(request.TextDocument.Uri, diagnostic, fix)
+ });
+ result.AddRange(codeActions.Select(ca => new CommandOrCodeAction(ca)));
+ }
+ }
+
+ _logger.LogDebug($"CodeActionHandler: Returning {result.Count} code actions");
+ return Task.FromResult(new CommandOrCodeActionContainer(result));
+ }
+
+ private static WorkspaceEdit CreateWorkspaceEdit(DocumentUri uri, Diagnostic diagnostic, CodeFixMapping fix)
+ {
+ TextEdit textEdit;
+ if (fix.isSuppression)
+ {
+ // For suppressions, insert at the actual end of the line
+ int line = diagnostic.Range.End.Line;
+ int lineLength = GetLineLength(uri, line);
+ textEdit = new TextEdit
+ {
+ Range = new OmniSharp.Extensions.LanguageServer.Protocol.Models.Range(line, lineLength, line, lineLength),
+ NewText = fix.replacement
+ };
+ }
+ else
+ {
+ textEdit = new TextEdit
+ {
+ Range = diagnostic.Range,
+ NewText = fix.replacement
+ };
+ }
+
+ return new WorkspaceEdit
+ {
+ Changes = new Dictionary>
+ {
+ [uri] = new[] { textEdit }
+ }
+ };
+ }
+
+ ///
+ /// Store line lengths for a document, computed from the document text we already have during scanning.
+ ///
+ public static void SetLineLengths(DocumentUri uri, string text)
+ {
+ var lines = text.Split('\n');
+ var lengths = new int[lines.Length];
+ for (int i = 0; i < lines.Length; i++)
+ {
+ lengths[i] = lines[i].TrimEnd('\r').Length;
+ }
+ _lineLengthCache[uri.ToString()] = lengths;
+ }
+
+ private static int GetLineLength(DocumentUri uri, int line)
+ {
+ if (_lineLengthCache.TryGetValue(uri.ToString(), out var lengths) && line < lengths.Length)
+ {
+ return lengths[line];
+ }
+ return 0;
+ }
+
+ ///
+ /// Register code fixes for a diagnostic. Called by TextDocumentSyncHandler when processing documents.
+ ///
+ public static void RegisterCodeFix(DocumentUri uri, Diagnostic diagnostic, CodeFixMapping fix)
+ {
+ var documentUri = uri.ToString();
+ var diagnosticKey = CreateDiagnosticKey(documentUri, diagnostic);
+
+ var documentFixes = _codeFixCache.GetOrAdd(documentUri, _ => new ConcurrentDictionary>());
+ var fixes = documentFixes.GetOrAdd(diagnosticKey, _ => new List());
+
+ lock (fixes)
+ {
+ fixes.Add(fix);
+ }
+ }
+
+ ///
+ /// Clear all code fixes for a document. Called when document is closed or rescanned.
+ ///
+ public static void ClearCodeFixes(DocumentUri uri)
+ {
+ _codeFixCache.TryRemove(uri.ToString(), out _);
+ _lineLengthCache.TryRemove(uri.ToString(), out _);
+ }
+
+ private static string CreateDiagnosticKey(string documentUri, Diagnostic diagnostic)
+ {
+ return $"{diagnostic.Code}:{diagnostic.Range.Start.Line}:{diagnostic.Range.Start.Character}:{diagnostic.Range.End.Line}:{diagnostic.Range.End.Character}";
+ }
+ }
+}
diff --git a/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/ConfigHelpers.cs b/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/ConfigHelpers.cs
index 747b069f..e715a3fe 100644
--- a/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/ConfigHelpers.cs
+++ b/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/ConfigHelpers.cs
@@ -50,9 +50,9 @@ internal static void SetScannerSettings(IConfiguration configuration)
{
fileIgnoreRegexes.Add(new Glob(potentialRegex));
}
- catch (Exception e)
+ catch (Exception)
{
- // TODO: Log issue with provided regex
+ // Invalid glob pattern — skip
}
}
StaticScannerSettings.IgnoreFiles = fileIgnoreRegexes;
@@ -72,9 +72,9 @@ internal static void SetScannerSettings(IConfiguration configuration)
{
ruleSet.AddPath(path);
}
- catch (Exception e)
+ catch (Exception)
{
- // TODO: Log issue with provided path
+ // Invalid rule path — skip
}
}
ruleSet = ruleSet.WithoutIds(StaticScannerSettings.IgnoreRuleIds);
diff --git a/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/DidChangeConfigurationHandler.cs b/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/DidChangeConfigurationHandler.cs
deleted file mode 100644
index d82830f0..00000000
--- a/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/DidChangeConfigurationHandler.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using MediatR;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Configuration;
-using OmniSharp.Extensions.LanguageServer.Protocol.Models;
-using OmniSharp.Extensions.LanguageServer.Protocol.Server;
-using OmniSharp.Extensions.LanguageServer.Protocol.Workspace;
-
-namespace DevSkim.LanguageServer;
-
-internal class DidChangeConfigurationHandler : DidChangeConfigurationHandlerBase
-{
- private readonly ILogger _logger;
- private readonly ILanguageServerConfiguration _configuration;
-
- ///
- /// Handle configuration changes from vscode
- ///
- ///
- ///
- public DidChangeConfigurationHandler(ILogger logger, ILanguageServerConfiguration configuration)
- {
- _logger = logger;
- _configuration = configuration;
- }
-
- public override async Task Handle(DidChangeConfigurationParams request, CancellationToken cancellationToken)
- {
- _logger.LogDebug("DidChangeConfigurationHandler.cs: DidChangeConfigurationParams");
- ConfigHelpers.SetScannerSettings(
- (IConfiguration)await _configuration.GetConfiguration(new ConfigurationItem { Section = "MS-CST-E.vscode-devskim" })
- );
- return Unit.Value;
- }
-}
\ No newline at end of file
diff --git a/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/Microsoft.DevSkim.LanguageServer.csproj b/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/Microsoft.DevSkim.LanguageServer.csproj
index 37ecea17..da5505c3 100644
--- a/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/Microsoft.DevSkim.LanguageServer.csproj
+++ b/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/Microsoft.DevSkim.LanguageServer.csproj
@@ -12,7 +12,6 @@
-
diff --git a/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/Program.cs b/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/Program.cs
index 38330965..c6db08dd 100644
--- a/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/Program.cs
+++ b/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/Program.cs
@@ -1,27 +1,17 @@
-using CommandLine;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
+using OmniSharp.Extensions.LanguageServer.Protocol.Server.Capabilities;
using OmniSharp.Extensions.LanguageServer.Server;
using Serilog;
-using System.Diagnostics;
namespace DevSkim.LanguageServer;
internal class Program
{
- public class Options
- {
- }
-
static async Task Main(string[] args)
{
#if DEBUG
- //while (!Debugger.IsAttached)
- //{
- // await Task.Delay(100);
- //}
Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.WriteTo.File("devskim-server-log.txt", rollingInterval: RollingInterval.Day)
@@ -31,35 +21,51 @@ static async Task Main(string[] args)
// Creates a "silent" logger
Log.Logger = new LoggerConfiguration().CreateLogger();
#endif
- Options _options = new Options();
- Parser.Default.ParseArguments(args)
- .WithParsed(o =>
- {
- _options = o;
- });
Log.Logger.Debug("Configuring server...");
IObserver workDone = null!;
-
OmniSharp.Extensions.LanguageServer.Server.LanguageServer server = await OmniSharp.Extensions.LanguageServer.Server.LanguageServer.From(
options =>
options
.WithInput(Console.OpenStandardInput())
.WithOutput(Console.OpenStandardOutput())
+ .WithServerInfo(new ServerInfo { Name = "DevSkim Language Server" })
.ConfigureLogging(
x => x
.AddSerilog(Log.Logger)
.AddLanguageProtocolLogging()
)
.WithHandler()
- .WithHandler()
+ .WithHandler()
+ // Handle settings push from clients (devskim/setSettings custom method)
+ // This works for both VS Code and VS - avoids workspace/configuration issues
.WithHandler()
.WithServices(x => x.AddLogging(b => b.SetMinimumLevel(LogLevel.Debug)))
- .WithConfigurationSection(ConfigHelpers.Section)
.OnInitialize(
async (server, request, token) =>
{
Log.Logger.Debug("Server is starting...");
+
+ // Check if the client sent settings via initializationOptions
+ // (VS extension passes PortableScannerSettings here)
+ Microsoft.DevSkim.LanguageProtoInterop.PortableScannerSettings? clientSettings = null;
+ try
+ {
+ if (request.InitializationOptions is Newtonsoft.Json.Linq.JToken initOptions)
+ {
+ clientSettings = initOptions.ToObject();
+ Log.Logger.Debug("Received settings from initializationOptions");
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Logger.Warning(ex, "Failed to parse initializationOptions as settings");
+ }
+
+ // Apply client settings if provided, otherwise use defaults
+ StaticScannerSettings.UpdateWith(clientSettings ?? new Microsoft.DevSkim.LanguageProtoInterop.PortableScannerSettings());
+ Log.Logger.Debug("Settings applied");
+
OmniSharp.Extensions.LanguageServer.Protocol.Server.WorkDone.IWorkDoneObserver manager = server.WorkDoneManager.For(
request, new WorkDoneProgressBegin
{
diff --git a/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/StaticScannerSettings.cs b/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/StaticScannerSettings.cs
index 804be1db..c008d808 100644
--- a/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/StaticScannerSettings.cs
+++ b/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/StaticScannerSettings.cs
@@ -25,15 +25,29 @@ internal static class StaticScannerSettings
internal static bool ScanOnChange { get; set; } = true;
internal static bool RemoveFindingsOnClose { get; set; } = true;
internal static DevSkimRuleSet RuleSet { get; set; } = new DevSkimRuleSet();
- internal static DevSkimRuleProcessorOptions RuleProcessorOptions { get; set; } = new DevSkimRuleProcessorOptions();
- internal static DevSkimRuleProcessor Processor { get; set; } = new DevSkimRuleProcessor(DevSkimRuleSet.GetDefaultRuleSet(), new DevSkimRuleProcessorOptions());
+ internal static DevSkimRuleProcessorOptions RuleProcessorOptions { get; set; } = CreateDefaultOptions();
+ internal static DevSkimRuleProcessor Processor { get; set; } = new DevSkimRuleProcessor(DevSkimRuleSet.GetDefaultRuleSet(), CreateDefaultOptions());
+
+ private static DevSkimRuleProcessorOptions CreateDefaultOptions()
+ {
+ return new DevSkimRuleProcessorOptions
+ {
+ // Include all severities by default
+ SeverityFilter = Severity.Critical | Severity.Important | Severity.Moderate | Severity.BestPractice | Severity.ManualReview,
+ // Include all confidence levels by default
+ ConfidenceFilter = Confidence.High | Confidence.Medium | Confidence.Low,
+ EnableSuppressions = true
+ };
+ }
public static void UpdateWith(PortableScannerSettings request)
{
SuppressionStyle = ToSuppressionStyle(request.SuppressionCommentStyle);
- CustomRulePaths = request.CustomRulesPathsString.Split(',');
- IgnoreRuleIds = request.IgnoreRulesListString.Split(',');
- IgnoreFiles = request.IgnoreFilesString.Split(',').Select(x => new Glob(x)).ToArray();
+ CustomRulePaths = request.CustomRulesPathsString.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ IgnoreRuleIds = request.IgnoreRulesListString.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ IgnoreFiles = request.IgnoreFilesString
+ .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
+ .Select(x => new Glob(x)).ToArray();
ReviewerName = request.ManualReviewerName;
SuppressionDuration = request.SuppressionDurationInDays;
IgnoreDefaultRuleSet = request.IgnoreDefaultRules;
diff --git a/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/TextDocumentSyncHandler.cs b/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/TextDocumentSyncHandler.cs
index 7ea29b6c..50484fd3 100644
--- a/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/TextDocumentSyncHandler.cs
+++ b/DevSkim-DotNet/Microsoft.DevSkim.LanguageServer/TextDocumentSyncHandler.cs
@@ -20,7 +20,9 @@ internal class TextDocumentSyncHandler : TextDocumentSyncHandlerBase
{
private readonly ILogger _logger;
private readonly ILanguageServerFacade _facade;
- private readonly TextDocumentSelector _documentSelector = TextDocumentSelector.ForLanguage(StaticScannerSettings.RuleProcessorOptions.Languages.GetNames());
+ // Use a broad document selector to handle all common code file types
+ // The actual language detection happens in GetTextDocumentAttributes
+ private readonly TextDocumentSelector _documentSelector = TextDocumentSelector.ForPattern("**/*");
private DevSkimRuleProcessor _processor => StaticScannerSettings.Processor;
public TextDocumentSyncHandler(ILogger logger, ILanguageServerFacade facade)
@@ -47,6 +49,8 @@ private async Task GenerateDiagnosticsForTextDocumentAsync(string text, in
}
// Diagnostics are sent a document at a time
_logger.LogDebug($"\tProcessing document: {filename}");
+ _logger.LogDebug($"\tRuleSet has {StaticScannerSettings.RuleSet.Count()} rules");
+ _logger.LogDebug($"\tScanOnOpen: {StaticScannerSettings.ScanOnOpen}, ScanOnChange: {StaticScannerSettings.ScanOnChange}");
List issues = await Task.Run(() => _processor.Analyze(text, filename).ToList());
ImmutableArray.Builder diagnostics = ImmutableArray.Empty.ToBuilder();
ImmutableArray.Builder codeFixes = ImmutableArray.Empty.ToBuilder();
@@ -103,6 +107,12 @@ private async Task GenerateDiagnosticsForTextDocumentAsync(string text, in
}
}
+ // Clear previous code fixes for this document and register new ones
+ CodeActionHandler.ClearCodeFixes(uri);
+
+ // Store line lengths so CodeActionHandler can compute exact end-of-line positions for suppressions
+ CodeActionHandler.SetLineLengths(uri, text);
+
_logger.LogDebug("\tPublishing diagnostics...");
_facade.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams()
{
@@ -110,10 +120,13 @@ private async Task GenerateDiagnosticsForTextDocumentAsync(string text, in
Uri = uri,
Version = version
});
- _facade.TextDocument.SendNotification(DevSkimMessages.FileVersion, new MappingsVersion() { version = version, fileName = uri.ToUri() });
+
+ // Register code fixes with CodeActionHandler for standard LSP textDocument/codeAction
+ // This works for both VS and VS Code — no custom notifications needed
+ _logger.LogDebug($"\tRegistering {codeFixes.Count} code fixes...");
foreach (var mapping in codeFixes)
{
- _facade.TextDocument.SendNotification(DevSkimMessages.CodeFixMapping, mapping);
+ CodeActionHandler.RegisterCodeFix(uri, mapping.diagnostic, mapping);
}
return Unit.Value;
diff --git a/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio.SourceGenerator/Microsoft.DevSkim.VisualStudio.SourceGenerator.csproj b/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio.SourceGenerator/Microsoft.DevSkim.VisualStudio.SourceGenerator.csproj
deleted file mode 100644
index 7eeb54fd..00000000
--- a/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio.SourceGenerator/Microsoft.DevSkim.VisualStudio.SourceGenerator.csproj
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
- netstandard2.0
- enable
- enable
- 11
- SourceGenerator
-
-
-
-
-
-
-
-
-
-
-
diff --git a/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio.SourceGenerator/UpdateSettingsGenerator.cs b/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio.SourceGenerator/UpdateSettingsGenerator.cs
deleted file mode 100644
index 58e657c9..00000000
--- a/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio.SourceGenerator/UpdateSettingsGenerator.cs
+++ /dev/null
@@ -1,75 +0,0 @@
-using System.Text;
-using Microsoft.CodeAnalysis;
-using Microsoft.DevSkim.LanguageProtoInterop;
-
-namespace Microsoft.DevSkim.VisualStudio
-{
- [Generator]
- public class UpdateSettingsGenerator : ISourceGenerator
- {
- public void Initialize(GeneratorInitializationContext context)
- {
- }
-
- public void Execute(GeneratorExecutionContext context)
- {
- StringBuilder source = new StringBuilder();
- source.Append($@"//
- namespace Microsoft.DevSkim.VisualStudio
- {{
- using System;
- using Microsoft.DevSkim.LanguageProtoInterop;
- using System.Collections.Generic;
-
- internal partial class VisualStudioSettingsManager
- {{
- partial void UpdateSettings(string propertyName)
- {{
- switch(propertyName)
- {{
-");
-
- string baseIndentation = " ";
- var props = typeof(IDevSkimOptions).GetProperties().Select(x => (x.Name, x.PropertyType));
- foreach (var prop in props)
- {
- var typeNameString = GetTypeNameString(prop.PropertyType);
- source.Append($@"{baseIndentation}case ""{prop.Name}"":
- {{
- var res = Get<{typeNameString}>(propertyName);
- if (res.Item1 == ValueResultEnum.Success)
- {{
- _currentSettings.{prop.Name} = res.Item2{ConditionallyUseNullCoalescing(typeNameString)}
- }}
- break;
- }}
-");
- }
-
- source.Append($@"{baseIndentation}default: break;
- }}
- }}
- }}
- }}");
- context.AddSource("VisualStudioSettingsManager.g.cs", source.ToString());
- }
-
- private string ConditionallyUseNullCoalescing(string typeNameString)
- {
- return typeNameString.StartsWith("List") ? $" ?? new {typeNameString}();" : ";";
- }
-
- private string GetTypeNameString(Type propPropertyType)
- {
- return propPropertyType.Name switch
- {
- "String" => "string",
- "List`1" => $"List<{GetTypeNameString(propPropertyType.GetGenericArguments()[0])}>",
- "Boolean" => "bool",
- "Int32" => "int",
- "CommentStylesEnum" => "CommentStylesEnum",
- _ => "string"
- };
- }
- }
-}
\ No newline at end of file
diff --git a/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio/.vsextension/string-resources.json b/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio/.vsextension/string-resources.json
new file mode 100644
index 00000000..8ac5db47
--- /dev/null
+++ b/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio/.vsextension/string-resources.json
@@ -0,0 +1,90 @@
+{
+ "DevSkim.LanguageServerProvider.DisplayName": "DevSkim Security Analyzer",
+
+ "devskim.DisplayName": "DevSkim",
+ "devskim.Description": "DevSkim security analyzer settings.",
+
+ "devskim.rules.DisplayName": "Rules",
+ "devskim.rules.Description": "Settings for controlling which DevSkim rules are enabled and custom rule paths.",
+
+ "devskim.suppressions.DisplayName": "Suppressions",
+ "devskim.suppressions.Description": "Settings for controlling how DevSkim suppressions are created and managed.",
+
+ "devskim.triggers.DisplayName": "Triggers",
+ "devskim.triggers.Description": "Settings for controlling when DevSkim scans are triggered.",
+
+ "devskim.ignores.DisplayName": "Ignores",
+ "devskim.ignores.Description": "Settings for ignoring specific rules or files.",
+
+ "devskim.rules.enableCriticalSeverityRules.DisplayName": "Enable Critical Severity Rules",
+ "devskim.rules.enableCriticalSeverityRules.Description": "Enable rules with critical severity.",
+
+ "devskim.rules.enableImportantSeverityRules.DisplayName": "Enable Important Severity Rules",
+ "devskim.rules.enableImportantSeverityRules.Description": "Enable rules with important severity.",
+
+ "devskim.rules.enableModerateSeverityRules.DisplayName": "Enable Moderate Severity Rules",
+ "devskim.rules.enableModerateSeverityRules.Description": "Enable rules with moderate severity.",
+
+ "devskim.rules.enableManualReviewSeverityRules.DisplayName": "Enable Manual Review Severity Rules",
+ "devskim.rules.enableManualReviewSeverityRules.Description": "Enable rules that require manual review.",
+
+ "devskim.rules.enableBestPracticeSeverityRules.DisplayName": "Enable Best Practice Severity Rules",
+ "devskim.rules.enableBestPracticeSeverityRules.Description": "Enable rules for best practice recommendations.",
+
+ "devskim.rules.enableHighConfidenceRules.DisplayName": "Enable High Confidence Rules",
+ "devskim.rules.enableHighConfidenceRules.Description": "Enable rules with high confidence findings.",
+
+ "devskim.rules.enableMediumConfidenceRules.DisplayName": "Enable Medium Confidence Rules",
+ "devskim.rules.enableMediumConfidenceRules.Description": "Enable rules with medium confidence findings.",
+
+ "devskim.rules.enableLowConfidenceRules.DisplayName": "Enable Low Confidence Rules",
+ "devskim.rules.enableLowConfidenceRules.Description": "Enable rules with low confidence findings.",
+
+ "devskim.rules.ignoreDefaultRules.DisplayName": "Ignore Default Rules",
+ "devskim.rules.ignoreDefaultRules.Description": "When enabled, the built-in default rules are not loaded. Only custom rules will be used.",
+
+ "devskim.rules.customRulesPaths.DisplayName": "Custom Rules Paths",
+ "devskim.rules.customRulesPaths.Description": "Semicolon-separated list of paths to directories containing custom DevSkim rules.",
+
+ "devskim.rules.customLanguagesPath.DisplayName": "Custom Languages Path",
+ "devskim.rules.customLanguagesPath.Description": "Path to a custom language definitions file.",
+
+ "devskim.rules.customCommentsPath.DisplayName": "Custom Comments Path",
+ "devskim.rules.customCommentsPath.Description": "Path to a custom comment definitions file.",
+
+ "devskim.rules.guidanceBaseURL.DisplayName": "Guidance Base URL",
+ "devskim.rules.guidanceBaseURL.Description": "Base URL for rule guidance documentation.",
+
+ "devskim.suppressions.suppressionCommentStyle.DisplayName": "Suppression Comment Style",
+ "devskim.suppressions.suppressionCommentStyle.Description": "Comment style used for suppression comments. Valid values: Line, Block.",
+
+ "devskim.suppressions.manualReviewerName.DisplayName": "Manual Reviewer Name",
+ "devskim.suppressions.manualReviewerName.Description": "Name used to populate the reviewer field in suppression comments.",
+
+ "devskim.suppressions.suppressionDurationInDays.DisplayName": "Suppression Duration (Days)",
+ "devskim.suppressions.suppressionDurationInDays.Description": "Default number of days before a suppression expires.",
+
+ "devskim.triggers.scanOnOpen.DisplayName": "Scan on Open",
+ "devskim.triggers.scanOnOpen.Description": "Run DevSkim analysis when a file is opened.",
+
+ "devskim.triggers.scanOnSave.DisplayName": "Scan on Save",
+ "devskim.triggers.scanOnSave.Description": "Run DevSkim analysis when a file is saved.",
+
+ "devskim.triggers.scanOnChange.DisplayName": "Scan on Change",
+ "devskim.triggers.scanOnChange.Description": "Run DevSkim analysis when a file is modified.",
+
+ "devskim.triggers.removeFindingsOnClose.DisplayName": "Remove Findings on Close",
+ "devskim.triggers.removeFindingsOnClose.Description": "Remove DevSkim findings when a file is closed.",
+
+ "devskim.ignores.ignoreRulesList.DisplayName": "Ignore Rules List",
+ "devskim.ignores.ignoreRulesList.Description": "Comma-separated list of DevSkim rule IDs to ignore.",
+
+ "devskim.ignores.ignoreFiles.DisplayName": "Ignore Files",
+ "devskim.ignores.ignoreFiles.Description": "Comma-separated list of file glob patterns to exclude from scanning.",
+
+ "devskim.diagnostics.DisplayName": "Diagnostics",
+ "devskim.diagnostics.Description": "Settings for DevSkim extension diagnostics and logging.",
+
+ "devskim.diagnostics.enableFileLogging.DisplayName": "Enable File Logging",
+ "devskim.diagnostics.enableFileLogging.Description": "Write extension logs to a file in the temp directory (devskim-vs-extension.log). Useful for troubleshooting."
+}
diff --git a/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio/DevSkimExtension.cs b/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio/DevSkimExtension.cs
new file mode 100644
index 00000000..e3fa50e4
--- /dev/null
+++ b/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio/DevSkimExtension.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Microsoft.DevSkim.VisualStudio;
+
+using global::Microsoft.Extensions.DependencyInjection;
+using global::Microsoft.VisualStudio.Extensibility;
+
+///
+/// Extension entry point for the DevSkim Visual Studio extension.
+///
+[VisualStudioContribution]
+internal class DevSkimExtension : Extension
+{
+ ///
+ public override ExtensionConfiguration ExtensionConfiguration => new()
+ {
+ Metadata = new(
+ id: "Microsoft.DevSkim.VisualStudio.f3a2c5e8-7d9b-4a1c-8e6f-2b3d4c5e6f7a",
+ version: new Version(ThisAssembly.AssemblyFileVersion),
+ publisherName: "Microsoft DevLabs",
+ displayName: "Microsoft DevSkim",
+ description: "DevSkim is a highly configurable security linter with a default ruleset focused on common security related issues.")
+ {
+ MoreInfo = "https://github.com/Microsoft/DevSkim",
+ Tags = ["linter", "linters", "coding", "security", "static analysis"],
+ },
+ };
+
+ ///
+ protected override void InitializeServices(IServiceCollection serviceCollection)
+ {
+ base.InitializeServices(serviceCollection);
+ }
+}
diff --git a/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio/DevSkimFixMessageTarget.cs b/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio/DevSkimFixMessageTarget.cs
deleted file mode 100644
index d7b42022..00000000
--- a/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio/DevSkimFixMessageTarget.cs
+++ /dev/null
@@ -1,83 +0,0 @@
-namespace Microsot.DevSkim.LanguageClient
-{
- using Microsoft.DevSkim.LanguageProtoInterop;
- using Microsoft.DevSkim.VisualStudio;
- using Newtonsoft.Json.Linq;
- using StreamJsonRpc;
- using System;
- using System.Collections.Concurrent;
- using System.Collections.Generic;
- using System.Threading.Tasks;
-
- public class DevSkimFixMessageTarget
- {
- public DevSkimFixMessageTarget()
- {
- }
-
- ///
- /// Remove all Code fixes for the specified filename that are not of the specified version
- ///
- /// JToken representation of
- ///
- [JsonRpcMethod(DevSkimMessages.FileVersion)]
- public async Task RemoveOldMappingsByVersionAsync(JToken token)
- {
- await Task.Run(() =>
- {
- MappingsVersion version = token.ToObject();
- if (version is { })
- {
- if (StaticData.FileToCodeFixMap.ContainsKey(version.fileName))
- {
- foreach (var key in StaticData.FileToCodeFixMap[version.fileName].Keys)
- {
- if (key != version.version)
- {
- StaticData.FileToCodeFixMap[version.fileName].TryRemove(key, out _);
- }
- }
- }
- }
- });
- }
-
-
- ///
- /// Update the client cache of available fixes for published diagnostics
- ///
- /// JToken representation of
- ///
- [JsonRpcMethod(DevSkimMessages.CodeFixMapping)]
- public async Task CodeFixMappingEventAsync(JToken jToken)
- {
- await Task.Run(() =>
- {
- CodeFixMapping mapping = jToken.ToObject();
- if (mapping is { })
- {
- StaticData.FileToCodeFixMap.AddOrUpdate(mapping.fileName,
- // Add New Nested Dictionary
- (Uri _) => new (new Dictionary>
- { { mapping.version ?? -1, new (new Dictionary()
- { {mapping, true } }) } }),
- // Update Nested Dictionary
- (key, oldValue) =>
- {
- oldValue.AddOrUpdate(mapping.version ?? -1,
- // Add new Set of mappings
- (int _) =>
- {
- var addedMapping = new ConcurrentDictionary();
- addedMapping.TryAdd(mapping, true);
- return addedMapping;
- },
- // Update Set of CodeFixMappings
- (versionKey, oldSet) => { oldSet.TryAdd(mapping, true); return oldSet; });
- return oldValue;
- });
- }
- });
- }
- }
-}
\ No newline at end of file
diff --git a/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio/DevSkimLanguageClient.cs b/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio/DevSkimLanguageClient.cs
deleted file mode 100644
index c89173dd..00000000
--- a/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio/DevSkimLanguageClient.cs
+++ /dev/null
@@ -1,136 +0,0 @@
-using Microsoft.VisualStudio.LanguageServer.Client;
-using Microsoft.VisualStudio.Shell;
-using Microsoft.VisualStudio.Utilities;
-using StreamJsonRpc;
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Reflection;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.VisualStudio.Threading;
-using Task = System.Threading.Tasks.Task;
-using System.ComponentModel.Composition;
-using Microsoft.Build.Framework.XamlTypes;
-using Microsoft.DevSkim.VisualStudio.ProcessTracker;
-using Microsoft.DevSkim.VisualStudio;
-
-namespace Microsot.DevSkim.LanguageClient
-{
- [ContentType("code")]
- [Export(typeof(ILanguageClient))]
- public class DevSkimLanguageClient : ILanguageClient, ILanguageClientCustomMessage2
- {
- [ImportingConstructor]
- public DevSkimLanguageClient(IProcessTracker processTracker)
- {
- ThreadHelper.ThrowIfNotOnUIThread();
- _manager = new VisualStudioSettingsManager(ServiceProvider.GlobalProvider, this);
- _processTracker = processTracker;
- }
-
- ///
- /// A reference to the Rpc connection between the client and server
- ///
- internal JsonRpc Rpc
- {
- get;
- set;
- }
- /// Pushes changed settings to the server
- public SettingsChangedNotifier SettingsNotifier { get; private set; }
- ///
- public string Name => "DevSkim Visual Studio Extension";
- // This handles incoming messages to the language client about fixes
- public object CustomMessageTarget => new DevSkimFixMessageTarget();
- /// Detects changes in the client settings
- private readonly VisualStudioSettingsManager _manager;
- /// Keeps track of the started language server process and ensures that it is properly closed when the extension closes.
- private readonly IProcessTracker _processTracker;
- ///
- public async Task ActivateAsync(CancellationToken token)
- {
- await Task.Yield();
- ProcessStartInfo info = new ProcessStartInfo();
- info.FileName = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Server", @"Microsoft.DevSkim.LanguageServer.exe");
- info.RedirectStandardInput = true;
- info.RedirectStandardOutput = true;
- info.UseShellExecute = false;
- info.CreateNoWindow = true;
-
- Process process = new Process();
- process.StartInfo = info;
-
- if (process.Start())
- {
- _processTracker.AddProcess(process);
- return new Connection(process.StandardOutput.BaseStream, process.StandardInput.BaseStream);
- }
- return null;
- }
- ///
- public event AsyncEventHandler StartAsync;
- ///
- public event AsyncEventHandler StopAsync;
- ///
- public async Task OnLoadedAsync()
- {
- if (StartAsync != null)
- {
- await StartAsync.InvokeAsync(this, EventArgs.Empty);
- }
- }
- ///
- public async Task StopServerAsync()
- {
- if (StopAsync != null)
- {
- await StopAsync.InvokeAsync(this, EventArgs.Empty);
- }
- }
-
- ///
- public Task AttachForCustomMessageAsync(JsonRpc rpc)
- {
- Rpc = rpc;
- SettingsNotifier = new SettingsChangedNotifier(Rpc);
- return Task.CompletedTask;
- }
-
- ///
- public async Task OnServerInitializedAsync()
- {
- await _manager.UpdateAllSettingsAsync();
- }
-
- ///
- public Task OnServerInitializeFailedAsync(ILanguageClientInitializationInfo initializationState)
- {
- string message = "DevSkim Language Client failed to activate.";
- string exception = initializationState.InitializationException?.ToString() ?? string.Empty;
- message = $"{message}\n {exception}";
-
- InitializationFailureContext failureContext = new InitializationFailureContext()
- {
- FailureMessage = message,
- };
-
- return Task.FromResult(failureContext);
- }
-
- // Not used but required by Interface
- ///
- public IEnumerable ConfigurationSections => null;
- ///
- public object InitializationOptions => null;
- ///
- public IEnumerable FilesToWatch => null;
- ///
- public bool ShowNotificationOnInitializeFailed => true;
-
- // This handles modifying outgoing messages to the language server
- ///
- object ILanguageClientCustomMessage2.MiddleLayer => null;
- }
-}
diff --git a/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio/DevSkimLanguageServerProvider.cs b/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio/DevSkimLanguageServerProvider.cs
new file mode 100644
index 00000000..f267306f
--- /dev/null
+++ b/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio/DevSkimLanguageServerProvider.cs
@@ -0,0 +1,512 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Microsoft.DevSkim.VisualStudio;
+
+using System.Diagnostics;
+using System.IO.Pipelines;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using global::Microsoft.DevSkim.LanguageProtoInterop;
+using global::Microsoft.VisualStudio.Extensibility;
+using global::Microsoft.VisualStudio.Extensibility.Editor;
+using global::Microsoft.VisualStudio.Extensibility.LanguageServer;
+using global::Microsoft.VisualStudio.Extensibility.Settings;
+using global::Microsoft.VisualStudio.RpcContracts.LanguageServerProvider;
+using Microsoft.Win32.SafeHandles;
+using Nerdbank.Streams;
+using Newtonsoft.Json.Linq;
+using Serilog;
+using Serilog.Core;
+
+///
+/// DevSkim Language Server Provider.
+/// Activates the DevSkim language server for security analysis when supported files are opened.
+///
+#pragma warning disable VSEXTPREVIEW_LSP // Type is for evaluation purposes only and is subject to change or removal in future updates.
+#pragma warning disable VSEXTPREVIEW_SETTINGS
+[VisualStudioContribution]
+internal class DevSkimLanguageServerProvider : LanguageServerProvider
+{
+ private static readonly string LogFilePath = Path.Combine(Path.GetTempPath(), "devskim-vs-extension.log");
+
+ private Process? _serverProcess;
+ private SafeHandle? _jobHandle;
+ private readonly System.Collections.Concurrent.ConcurrentBag _settingsSubscriptions = [];
+ private bool _initialPushDone;
+ private CancellationTokenSource? _restartDebounce;
+ private readonly CancellationTokenSource _disposeCts = new();
+
+ ///
+ public override LanguageServerProviderConfiguration LanguageServerProviderConfiguration => new(
+ "%DevSkim.LanguageServerProvider.DisplayName%",
+ [
+ DocumentFilter.FromDocumentType("text"),
+ ]);
+
+ ///
+ public override async Task CreateServerConnectionAsync(CancellationToken cancellationToken)
+ {
+ // Configure logging on each server start (file logging may have changed)
+ ConfigureLogging(await ReadFileLoggingSettingAsync(cancellationToken));
+
+ Log.Debug("CreateServerConnectionAsync called");
+
+ // Kill any leftover process from a previous activation
+ StopServerProcess();
+
+ var serverPath = GetLanguageServerPath();
+ Log.Debug("Server path: {ServerPath}", serverPath);
+
+ if (string.IsNullOrEmpty(serverPath) || !File.Exists(serverPath))
+ {
+ Log.Error("Language server not found at: {ServerPath}", serverPath);
+ return null;
+ }
+
+ // Read current settings and pass them as LSP initializationOptions
+ // so the server applies them during the initialize handshake.
+ var currentSettings = await ReadAllSettingsAsync(cancellationToken);
+ LanguageServerOptions = new LanguageServerOptions
+ {
+ InitializationOptions = JToken.FromObject(currentSettings),
+ };
+ Log.Debug("InitializationOptions set with current settings");
+
+ // Create a Windows Job Object so the server process is killed when the host process exits
+ EnsureJobObject();
+
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = serverPath,
+ RedirectStandardInput = true,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WorkingDirectory = Path.GetDirectoryName(serverPath),
+ };
+
+ var process = new Process { StartInfo = startInfo };
+
+ process.ErrorDataReceived += (sender, e) =>
+ {
+ if (!string.IsNullOrEmpty(e.Data))
+ {
+ Log.Debug("Server stderr: {Data}", e.Data);
+ }
+ };
+
+ if (process.Start())
+ {
+ _serverProcess = process;
+ AssignProcessToJobObject(process);
+ process.BeginErrorReadLine();
+ Log.Information("Language server started (PID: {ProcessId})", process.Id);
+
+ return new DuplexPipe(
+ PipeReader.Create(process.StandardOutput.BaseStream),
+ PipeWriter.Create(process.StandardInput.BaseStream));
+ }
+
+ Log.Error("Failed to start language server process");
+ return null;
+ }
+
+ ///
+ public override async Task OnServerInitializationResultAsync(
+ ServerInitializationResult serverInitializationResult,
+ LanguageServerInitializationFailureInfo? initializationFailureInfo,
+ CancellationToken cancellationToken)
+ {
+ if (serverInitializationResult == ServerInitializationResult.Failed)
+ {
+ Log.Error("Language server initialization failed: {Message}", initializationFailureInfo?.StatusMessage);
+ Enabled = false;
+ }
+ else
+ {
+ Log.Information("Language server initialized successfully");
+ SubscribeToSettingsChanges();
+ // Delay enabling change detection so SubscribeAsync initial callbacks are ignored
+ _ = Task.Run(async () =>
+ {
+ await Task.Delay(5000);
+ _initialPushDone = true;
+ Log.Debug("Settings change detection enabled");
+ });
+ }
+
+ await base.OnServerInitializationResultAsync(serverInitializationResult, initializationFailureInfo, cancellationToken);
+ }
+
+ ///
+ protected override void Dispose(bool isDisposing)
+ {
+ if (isDisposing)
+ {
+ // Cancel pending subscription operations first
+ _disposeCts.Cancel();
+ _disposeCts.Dispose();
+
+ foreach (var sub in _settingsSubscriptions)
+ {
+ sub.Dispose();
+ }
+ while (_settingsSubscriptions.TryTake(out _)) { }
+ _restartDebounce?.Cancel();
+ _restartDebounce?.Dispose();
+ StopServerProcess();
+ _jobHandle?.Dispose();
+ _jobHandle = null;
+ }
+
+ base.Dispose(isDisposing);
+ }
+
+ #region Logging
+
+ private static void ConfigureLogging(bool enableFileLogging)
+ {
+ var config = new LoggerConfiguration()
+ .Enrich.FromLogContext()
+ .WriteTo.Debug(outputTemplate: "[DevSkim] {Message:lj}{NewLine}{Exception}");
+
+#if DEBUG
+ // Always log to file in debug builds
+ config = config
+ .WriteTo.File(LogFilePath, rollingInterval: RollingInterval.Day)
+ .MinimumLevel.Verbose();
+#else
+ if (enableFileLogging)
+ {
+ config = config.WriteTo.File(LogFilePath, rollingInterval: RollingInterval.Day);
+ }
+
+ config = config.MinimumLevel.Debug();
+#endif
+
+ Log.Logger = config.CreateLogger();
+ }
+
+ private async Task ReadFileLoggingSettingAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ return (await Extensibility.Settings()
+ .ReadEffectiveValueAsync(DevSkimSettingDefinitions.EnableFileLogging, cancellationToken))
+ .ValueOrDefault(false);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ #endregion
+
+ #region Settings
+
+ ///
+ /// Subscribes to all settings changes. On change, restarts the server so it picks up
+ /// new settings via initializationOptions during the LSP handshake.
+ ///
+ private void SubscribeToSettingsChanges()
+ {
+ SubscribeSetting(DevSkimSettingDefinitions.EnableCriticalSeverityRules);
+ SubscribeSetting(DevSkimSettingDefinitions.EnableImportantSeverityRules);
+ SubscribeSetting(DevSkimSettingDefinitions.EnableModerateSeverityRules);
+ SubscribeSetting(DevSkimSettingDefinitions.EnableManualReviewSeverityRules);
+ SubscribeSetting(DevSkimSettingDefinitions.EnableBestPracticeSeverityRules);
+ SubscribeSetting(DevSkimSettingDefinitions.EnableHighConfidenceRules);
+ SubscribeSetting(DevSkimSettingDefinitions.EnableMediumConfidenceRules);
+ SubscribeSetting(DevSkimSettingDefinitions.EnableLowConfidenceRules);
+ SubscribeSetting(DevSkimSettingDefinitions.IgnoreDefaultRules);
+ SubscribeSetting(DevSkimSettingDefinitions.ScanOnOpen);
+ SubscribeSetting(DevSkimSettingDefinitions.ScanOnSave);
+ SubscribeSetting(DevSkimSettingDefinitions.ScanOnChange);
+ SubscribeSetting(DevSkimSettingDefinitions.RemoveFindingsOnClose);
+ SubscribeStringSetting(DevSkimSettingDefinitions.CustomRulesPaths);
+ SubscribeStringSetting(DevSkimSettingDefinitions.CustomLanguagesPath);
+ SubscribeStringSetting(DevSkimSettingDefinitions.CustomCommentsPath);
+ SubscribeStringSetting(DevSkimSettingDefinitions.GuidanceBaseURL);
+ SubscribeStringSetting(DevSkimSettingDefinitions.SuppressionCommentStyle);
+ SubscribeStringSetting(DevSkimSettingDefinitions.ManualReviewerName);
+ SubscribeStringSetting(DevSkimSettingDefinitions.IgnoreRulesList);
+ SubscribeStringSetting(DevSkimSettingDefinitions.IgnoreFiles);
+ SubscribeIntSetting(DevSkimSettingDefinitions.SuppressionDurationInDays);
+ }
+
+ private void SubscribeSetting(Setting.Boolean setting)
+ {
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ var sub = await Extensibility.Settings().SubscribeAsync(
+ setting,
+ _disposeCts.Token,
+ changeHandler: _ => OnSettingChanged());
+ _settingsSubscriptions.Add(sub);
+ }
+ catch (OperationCanceledException)
+ {
+ // Provider is being disposed, ignore
+ }
+ });
+ }
+
+ private void SubscribeStringSetting(Setting.String setting)
+ {
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ var sub = await Extensibility.Settings().SubscribeAsync(
+ setting,
+ _disposeCts.Token,
+ changeHandler: _ => OnSettingChanged());
+ _settingsSubscriptions.Add(sub);
+ }
+ catch (OperationCanceledException)
+ {
+ // Provider is being disposed, ignore
+ }
+ });
+ }
+
+ private void SubscribeIntSetting(Setting.Integer setting)
+ {
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ var sub = await Extensibility.Settings().SubscribeAsync(
+ setting,
+ _disposeCts.Token,
+ changeHandler: _ => OnSettingChanged());
+ _settingsSubscriptions.Add(sub);
+ }
+ catch (OperationCanceledException)
+ {
+ // Provider is being disposed, ignore
+ }
+ });
+ }
+
+ private void OnSettingChanged()
+ {
+ if (!_initialPushDone)
+ {
+ return;
+ }
+
+ // Debounce: cancel any pending restart and start a new 2-second timer.
+ // This way rapid changes (e.g. toggling multiple settings) cause only one restart.
+ _restartDebounce?.Cancel();
+ _restartDebounce = new CancellationTokenSource();
+ var token = _restartDebounce.Token;
+
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ await Task.Delay(2000, token);
+ Log.Information("Settings changed, restarting server");
+ Enabled = false;
+ await Task.Delay(500, CancellationToken.None);
+ Enabled = true;
+ }
+ catch (OperationCanceledException)
+ {
+ // Debounced — a newer change superseded this one
+ }
+ });
+ }
+
+ ///
+ /// Reads all DevSkim settings from VS and maps them to a .
+ ///
+ private async Task ReadAllSettingsAsync(CancellationToken cancellationToken)
+ {
+ var api = Extensibility.Settings();
+
+ var suppressionStyle = (await api.ReadEffectiveValueAsync(DevSkimSettingDefinitions.SuppressionCommentStyle, cancellationToken))
+ .ValueOrDefault("Line");
+ var parsedStyle = Enum.TryParse(suppressionStyle, ignoreCase: true, out var style)
+ ? style
+ : CommentStylesEnum.Line;
+
+ return new PortableScannerSettings
+ {
+ EnableCriticalSeverityRules = (await api.ReadEffectiveValueAsync(DevSkimSettingDefinitions.EnableCriticalSeverityRules, cancellationToken)).ValueOrDefault(true),
+ EnableImportantSeverityRules = (await api.ReadEffectiveValueAsync(DevSkimSettingDefinitions.EnableImportantSeverityRules, cancellationToken)).ValueOrDefault(true),
+ EnableModerateSeverityRules = (await api.ReadEffectiveValueAsync(DevSkimSettingDefinitions.EnableModerateSeverityRules, cancellationToken)).ValueOrDefault(true),
+ EnableManualReviewSeverityRules = (await api.ReadEffectiveValueAsync(DevSkimSettingDefinitions.EnableManualReviewSeverityRules, cancellationToken)).ValueOrDefault(true),
+ EnableBestPracticeSeverityRules = (await api.ReadEffectiveValueAsync(DevSkimSettingDefinitions.EnableBestPracticeSeverityRules, cancellationToken)).ValueOrDefault(true),
+ EnableHighConfidenceRules = (await api.ReadEffectiveValueAsync(DevSkimSettingDefinitions.EnableHighConfidenceRules, cancellationToken)).ValueOrDefault(true),
+ EnableMediumConfidenceRules = (await api.ReadEffectiveValueAsync(DevSkimSettingDefinitions.EnableMediumConfidenceRules, cancellationToken)).ValueOrDefault(true),
+ EnableLowConfidenceRules = (await api.ReadEffectiveValueAsync(DevSkimSettingDefinitions.EnableLowConfidenceRules, cancellationToken)).ValueOrDefault(false),
+ IgnoreDefaultRules = (await api.ReadEffectiveValueAsync(DevSkimSettingDefinitions.IgnoreDefaultRules, cancellationToken)).ValueOrDefault(false),
+ ScanOnOpen = (await api.ReadEffectiveValueAsync(DevSkimSettingDefinitions.ScanOnOpen, cancellationToken)).ValueOrDefault(true),
+ ScanOnSave = (await api.ReadEffectiveValueAsync(DevSkimSettingDefinitions.ScanOnSave, cancellationToken)).ValueOrDefault(true),
+ ScanOnChange = (await api.ReadEffectiveValueAsync(DevSkimSettingDefinitions.ScanOnChange, cancellationToken)).ValueOrDefault(true),
+ RemoveFindingsOnClose = (await api.ReadEffectiveValueAsync(DevSkimSettingDefinitions.RemoveFindingsOnClose, cancellationToken)).ValueOrDefault(true),
+ CustomRulesPathsString = (await api.ReadEffectiveValueAsync(DevSkimSettingDefinitions.CustomRulesPaths, cancellationToken)).ValueOrDefault(""),
+ CustomLanguagesPath = (await api.ReadEffectiveValueAsync(DevSkimSettingDefinitions.CustomLanguagesPath, cancellationToken)).ValueOrDefault(""),
+ CustomCommentsPath = (await api.ReadEffectiveValueAsync(DevSkimSettingDefinitions.CustomCommentsPath, cancellationToken)).ValueOrDefault(""),
+ GuidanceBaseURL = (await api.ReadEffectiveValueAsync(DevSkimSettingDefinitions.GuidanceBaseURL, cancellationToken)).ValueOrDefault("https://github.com/microsoft/devskim/tree/main/guidance"),
+ SuppressionCommentStyle = parsedStyle,
+ ManualReviewerName = (await api.ReadEffectiveValueAsync(DevSkimSettingDefinitions.ManualReviewerName, cancellationToken)).ValueOrDefault(""),
+ SuppressionDurationInDays = (await api.ReadEffectiveValueAsync(DevSkimSettingDefinitions.SuppressionDurationInDays, cancellationToken)).ValueOrDefault(30),
+ IgnoreRulesListString = (await api.ReadEffectiveValueAsync(DevSkimSettingDefinitions.IgnoreRulesList, cancellationToken)).ValueOrDefault(""),
+ IgnoreFilesString = (await api.ReadEffectiveValueAsync(DevSkimSettingDefinitions.IgnoreFiles, cancellationToken)).ValueOrDefault(""),
+ };
+ }
+
+ #endregion
+
+ #region Process Management
+
+ private void StopServerProcess()
+ {
+ try
+ {
+ if (_serverProcess is { HasExited: false })
+ {
+ _serverProcess.Kill();
+ _serverProcess.Dispose();
+ }
+ }
+ catch
+ {
+ // Best effort cleanup
+ }
+ finally
+ {
+ _serverProcess = null;
+ }
+ }
+
+ private void EnsureJobObject()
+ {
+ if (_jobHandle != null)
+ {
+ return;
+ }
+
+ try
+ {
+ string jobName = "DevSkimLanguageServer" + Environment.ProcessId;
+ _jobHandle = NativeMethods.CreateJobObject(IntPtr.Zero, jobName);
+
+ var extendedInfo = new NativeMethods.JOBOBJECT_EXTENDED_LIMIT_INFORMATION
+ {
+ BasicLimitInformation = new NativeMethods.JOBOBJECT_BASIC_LIMIT_INFORMATION
+ {
+ LimitFlags = 0x2000 /* JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE */
+ }
+ };
+
+ int length = Marshal.SizeOf(extendedInfo);
+ IntPtr pExtendedInfo = Marshal.AllocHGlobal(length);
+ try
+ {
+ Marshal.StructureToPtr(extendedInfo, pExtendedInfo, false);
+ NativeMethods.SetInformationJobObject(_jobHandle, 9 /* JobObjectExtendedLimitInformation */, pExtendedInfo, (uint)length);
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(pExtendedInfo);
+ }
+ }
+ catch
+ {
+ // Non-fatal: process cleanup will fall back to Dispose/Kill
+ }
+ }
+
+ private void AssignProcessToJobObject(Process process)
+ {
+ try
+ {
+ if (_jobHandle != null && !_jobHandle.IsInvalid)
+ {
+ NativeMethods.AssignProcessToJobObject(_jobHandle, process.Handle);
+ }
+ }
+ catch
+ {
+ // Non-fatal
+ }
+ }
+
+ private static string GetLanguageServerPath()
+ {
+ var extensionDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+ if (string.IsNullOrEmpty(extensionDirectory))
+ {
+ return string.Empty;
+ }
+
+ return Path.Combine(extensionDirectory, "Server", "Microsoft.DevSkim.LanguageServer.exe");
+ }
+
+ ///
+ /// Native Windows API declarations for Job Object functionality.
+ /// Job Objects are required to ensure the language server child process is automatically
+ /// terminated when Visual Studio exits (even if killed unexpectedly). There is no managed
+ /// .NET equivalent for this Windows kernel feature, so P/Invoke is necessary.
+ ///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "SYSLIB1054:Use 'LibraryImportAttribute' instead of 'DllImportAttribute'", Justification = "DllImport is simpler for these few calls and works correctly")]
+ private static class NativeMethods
+ {
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
+ internal static extern SafeWaitHandle CreateJobObject(IntPtr lpJobAttributes, string? lpName);
+
+ [DllImport("kernel32.dll")]
+ internal static extern bool SetInformationJobObject(SafeHandle hJob, int jobObjectInfoClass, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength);
+
+ [DllImport("kernel32.dll")]
+ internal static extern bool AssignProcessToJobObject(SafeHandle hJob, IntPtr hProcess);
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct JOBOBJECT_BASIC_LIMIT_INFORMATION
+ {
+ public long PerProcessUserTimeLimit;
+ public long PerJobUserTimeLimit;
+ public uint LimitFlags;
+ public UIntPtr MinimumWorkingSetSize;
+ public UIntPtr MaximumWorkingSetSize;
+ public uint ActiveProcessLimit;
+ public UIntPtr Affinity;
+ public uint PriorityClass;
+ public uint SchedulingClass;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct IO_COUNTERS
+ {
+ public ulong ReadOperationCount;
+ public ulong WriteOperationCount;
+ public ulong OtherOperationCount;
+ public ulong ReadTransferCount;
+ public ulong WriteTransferCount;
+ public ulong OtherTransferCount;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
+ {
+ public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
+ public IO_COUNTERS IoInfo;
+ public UIntPtr ProcessMemoryLimit;
+ public UIntPtr JobMemoryLimit;
+ public UIntPtr PeakProcessMemoryUsed;
+ public UIntPtr PeakJobMemoryUsed;
+ }
+ }
+
+ #endregion
+}
+#pragma warning restore VSEXTPREVIEW_LSP
diff --git a/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio/DevSkimSettingDefinitions.cs b/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio/DevSkimSettingDefinitions.cs
new file mode 100644
index 00000000..934042c5
--- /dev/null
+++ b/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio/DevSkimSettingDefinitions.cs
@@ -0,0 +1,223 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Microsoft.DevSkim.VisualStudio;
+
+using Microsoft.VisualStudio.Extensibility;
+using Microsoft.VisualStudio.Extensibility.Settings;
+
+#pragma warning disable VSEXTPREVIEW_SETTINGS
+
+///
+/// Defines the settings for the DevSkim Visual Studio extension using the VS Extensibility SDK settings API.
+/// Settings mirror properties.
+///
+internal static class DevSkimSettingDefinitions
+{
+ #region Categories
+
+ [VisualStudioContribution]
+ internal static SettingCategory DevSkimCategory { get; } = new("devskim", "%devskim.DisplayName%")
+ {
+ Description = "%devskim.Description%",
+ GenerateObserverClass = true,
+ };
+
+ [VisualStudioContribution]
+ internal static SettingCategory RulesCategory { get; } = new("devskimRules", "%devskim.rules.DisplayName%", DevSkimCategory)
+ {
+ Description = "%devskim.rules.Description%",
+ };
+
+ [VisualStudioContribution]
+ internal static SettingCategory SuppressionsCategory { get; } = new("devskimSuppressions", "%devskim.suppressions.DisplayName%", DevSkimCategory)
+ {
+ Description = "%devskim.suppressions.Description%",
+ };
+
+ [VisualStudioContribution]
+ internal static SettingCategory TriggersCategory { get; } = new("devskimTriggers", "%devskim.triggers.DisplayName%", DevSkimCategory)
+ {
+ Description = "%devskim.triggers.Description%",
+ };
+
+ [VisualStudioContribution]
+ internal static SettingCategory IgnoresCategory { get; } = new("devskimIgnores", "%devskim.ignores.DisplayName%", DevSkimCategory)
+ {
+ Description = "%devskim.ignores.Description%",
+ };
+
+ #endregion
+
+ #region Rules Settings - Severity
+
+ [VisualStudioContribution]
+ internal static Setting.Boolean EnableCriticalSeverityRules { get; } = new("devskimEnableCriticalSeverityRules", "%devskim.rules.enableCriticalSeverityRules.DisplayName%", RulesCategory, defaultValue: true)
+ {
+ Description = "%devskim.rules.enableCriticalSeverityRules.Description%",
+ };
+
+ [VisualStudioContribution]
+ internal static Setting.Boolean EnableImportantSeverityRules { get; } = new("devskimEnableImportantSeverityRules", "%devskim.rules.enableImportantSeverityRules.DisplayName%", RulesCategory, defaultValue: true)
+ {
+ Description = "%devskim.rules.enableImportantSeverityRules.Description%",
+ };
+
+ [VisualStudioContribution]
+ internal static Setting.Boolean EnableModerateSeverityRules { get; } = new("devskimEnableModerateSeverityRules", "%devskim.rules.enableModerateSeverityRules.DisplayName%", RulesCategory, defaultValue: true)
+ {
+ Description = "%devskim.rules.enableModerateSeverityRules.Description%",
+ };
+
+ [VisualStudioContribution]
+ internal static Setting.Boolean EnableManualReviewSeverityRules { get; } = new("devskimEnableManualReviewSeverityRules", "%devskim.rules.enableManualReviewSeverityRules.DisplayName%", RulesCategory, defaultValue: true)
+ {
+ Description = "%devskim.rules.enableManualReviewSeverityRules.Description%",
+ };
+
+ [VisualStudioContribution]
+ internal static Setting.Boolean EnableBestPracticeSeverityRules { get; } = new("devskimEnableBestPracticeSeverityRules", "%devskim.rules.enableBestPracticeSeverityRules.DisplayName%", RulesCategory, defaultValue: true)
+ {
+ Description = "%devskim.rules.enableBestPracticeSeverityRules.Description%",
+ };
+
+ #endregion
+
+ #region Rules Settings - Confidence
+
+ [VisualStudioContribution]
+ internal static Setting.Boolean EnableHighConfidenceRules { get; } = new("devskimEnableHighConfidenceRules", "%devskim.rules.enableHighConfidenceRules.DisplayName%", RulesCategory, defaultValue: true)
+ {
+ Description = "%devskim.rules.enableHighConfidenceRules.Description%",
+ };
+
+ [VisualStudioContribution]
+ internal static Setting.Boolean EnableMediumConfidenceRules { get; } = new("devskimEnableMediumConfidenceRules", "%devskim.rules.enableMediumConfidenceRules.DisplayName%", RulesCategory, defaultValue: true)
+ {
+ Description = "%devskim.rules.enableMediumConfidenceRules.Description%",
+ };
+
+ [VisualStudioContribution]
+ internal static Setting.Boolean EnableLowConfidenceRules { get; } = new("devskimEnableLowConfidenceRules", "%devskim.rules.enableLowConfidenceRules.DisplayName%", RulesCategory, defaultValue: false)
+ {
+ Description = "%devskim.rules.enableLowConfidenceRules.Description%",
+ };
+
+ #endregion
+
+ #region Rules Settings - Paths and Configuration
+
+ [VisualStudioContribution]
+ internal static Setting.Boolean IgnoreDefaultRules { get; } = new("devskimIgnoreDefaultRules", "%devskim.rules.ignoreDefaultRules.DisplayName%", RulesCategory, defaultValue: false)
+ {
+ Description = "%devskim.rules.ignoreDefaultRules.Description%",
+ };
+
+ [VisualStudioContribution]
+ internal static Setting.String CustomRulesPaths { get; } = new("devskimCustomRulesPaths", "%devskim.rules.customRulesPaths.DisplayName%", RulesCategory, defaultValue: "")
+ {
+ Description = "%devskim.rules.customRulesPaths.Description%",
+ };
+
+ [VisualStudioContribution]
+ internal static Setting.String CustomLanguagesPath { get; } = new("devskimCustomLanguagesPath", "%devskim.rules.customLanguagesPath.DisplayName%", RulesCategory, defaultValue: "")
+ {
+ Description = "%devskim.rules.customLanguagesPath.Description%",
+ };
+
+ [VisualStudioContribution]
+ internal static Setting.String CustomCommentsPath { get; } = new("devskimCustomCommentsPath", "%devskim.rules.customCommentsPath.DisplayName%", RulesCategory, defaultValue: "")
+ {
+ Description = "%devskim.rules.customCommentsPath.Description%",
+ };
+
+ [VisualStudioContribution]
+ internal static Setting.String GuidanceBaseURL { get; } = new("devskimGuidanceBaseURL", "%devskim.rules.guidanceBaseURL.DisplayName%", RulesCategory, defaultValue: "https://github.com/microsoft/devskim/tree/main/guidance")
+ {
+ Description = "%devskim.rules.guidanceBaseURL.Description%",
+ };
+
+ #endregion
+
+ #region Suppressions Settings
+
+ [VisualStudioContribution]
+ internal static Setting.String SuppressionCommentStyle { get; } = new("devskimSuppressionCommentStyle", "%devskim.suppressions.suppressionCommentStyle.DisplayName%", SuppressionsCategory, defaultValue: "Line")
+ {
+ Description = "%devskim.suppressions.suppressionCommentStyle.Description%",
+ };
+
+ [VisualStudioContribution]
+ internal static Setting.String ManualReviewerName { get; } = new("devskimManualReviewerName", "%devskim.suppressions.manualReviewerName.DisplayName%", SuppressionsCategory, defaultValue: "")
+ {
+ Description = "%devskim.suppressions.manualReviewerName.Description%",
+ };
+
+ [VisualStudioContribution]
+ internal static Setting.Integer SuppressionDurationInDays { get; } = new("devskimSuppressionDurationInDays", "%devskim.suppressions.suppressionDurationInDays.DisplayName%", SuppressionsCategory, defaultValue: 30)
+ {
+ Description = "%devskim.suppressions.suppressionDurationInDays.Description%",
+ };
+
+ #endregion
+
+ #region Triggers Settings
+
+ [VisualStudioContribution]
+ internal static Setting.Boolean ScanOnOpen { get; } = new("devskimScanOnOpen", "%devskim.triggers.scanOnOpen.DisplayName%", TriggersCategory, defaultValue: true)
+ {
+ Description = "%devskim.triggers.scanOnOpen.Description%",
+ };
+
+ [VisualStudioContribution]
+ internal static Setting.Boolean ScanOnSave { get; } = new("devskimScanOnSave", "%devskim.triggers.scanOnSave.DisplayName%", TriggersCategory, defaultValue: true)
+ {
+ Description = "%devskim.triggers.scanOnSave.Description%",
+ };
+
+ [VisualStudioContribution]
+ internal static Setting.Boolean ScanOnChange { get; } = new("devskimScanOnChange", "%devskim.triggers.scanOnChange.DisplayName%", TriggersCategory, defaultValue: true)
+ {
+ Description = "%devskim.triggers.scanOnChange.Description%",
+ };
+
+ [VisualStudioContribution]
+ internal static Setting.Boolean RemoveFindingsOnClose { get; } = new("devskimRemoveFindingsOnClose", "%devskim.triggers.removeFindingsOnClose.DisplayName%", TriggersCategory, defaultValue: true)
+ {
+ Description = "%devskim.triggers.removeFindingsOnClose.Description%",
+ };
+
+ #endregion
+
+ #region Ignores Settings
+
+ [VisualStudioContribution]
+ internal static Setting.String IgnoreRulesList { get; } = new("devskimIgnoreRulesList", "%devskim.ignores.ignoreRulesList.DisplayName%", IgnoresCategory, defaultValue: "")
+ {
+ Description = "%devskim.ignores.ignoreRulesList.Description%",
+ };
+
+ [VisualStudioContribution]
+ internal static Setting.String IgnoreFiles { get; } = new("devskimIgnoreFiles", "%devskim.ignores.ignoreFiles.DisplayName%", IgnoresCategory, defaultValue: "")
+ {
+ Description = "%devskim.ignores.ignoreFiles.Description%",
+ };
+
+ #endregion
+
+ #region Diagnostics Settings
+
+ [VisualStudioContribution]
+ internal static SettingCategory DiagnosticsCategory { get; } = new("devskimDiagnostics", "%devskim.diagnostics.DisplayName%", DevSkimCategory)
+ {
+ Description = "%devskim.diagnostics.Description%",
+ };
+
+ [VisualStudioContribution]
+ internal static Setting.Boolean EnableFileLogging { get; } = new("devskimEnableFileLogging", "%devskim.diagnostics.enableFileLogging.DisplayName%", DiagnosticsCategory, defaultValue: false)
+ {
+ Description = "%devskim.diagnostics.enableFileLogging.Description%",
+ };
+
+ #endregion
+}
diff --git a/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio/DevSkimSuggestedAction.cs b/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio/DevSkimSuggestedAction.cs
deleted file mode 100644
index 80d004bb..00000000
--- a/DevSkim-DotNet/Microsoft.DevSkim.VisualStudio/DevSkimSuggestedAction.cs
+++ /dev/null
@@ -1,116 +0,0 @@
-// Copyright (C) Microsoft. All rights reserved. Licensed under the MIT License.
-
-using Microsoft.VisualStudio.Imaging.Interop;
-using Microsoft.VisualStudio.Language.Intellisense;
-using Microsoft.VisualStudio.Text;
-using System.Threading;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Documents;
-using System.Windows.Media;
-namespace Microsoft.DevSkim.VisualStudio
-{
- using Microsoft.DevSkim.LanguageProtoInterop;
- using System;
- using System.Collections.Generic;
- using System.Threading.Tasks;
-
- internal class DevSkimSuggestedAction: ISuggestedAction
- {
- public DevSkimSuggestedAction(SnapshotSpan span, CodeFixMapping mapping)
- {
- _span = span;
- _snapshot = span.Snapshot;
- _mapping = mapping;
- DisplayText = mapping.friendlyString;
- }
-
- public string DisplayText { get; }
-
- public bool HasActionSets
- {
- get
- {
- return false;
- }
- }
-
- public bool HasPreview
- {
- get
- {
- return false;
- }
- }
-
- public string IconAutomationText
- {
- get
- {
- return null;
- }
- }
-
- ImageMoniker ISuggestedAction.IconMoniker
- {
- get
- {
- return default(ImageMoniker);
- }
- }
-
- public string InputGestureText
- {
- get
- {
- return null;
- }
- }
-
- public void Dispose()
- {
- }
-
- public Task> GetActionSetsAsync(CancellationToken cancellationToken)
- {
- return Task.FromResult>(Array.Empty());
- }
-
- public Task