diff --git a/CHANGELOG.md b/CHANGELOG.md index ff00b2a..1da38f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,17 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 > **Note**: odd version numbers, for example, `0.13.0`, are not included in this changelog. They are used to test the new features and fixes before the final release. -## [0.25.2] - Unreleased +## [0.25.3] - Unreleased ### Added: - MCP Server: Dev Proxy +- Diagnostics: Show error if pluginPath in plugin instance is not correctly set to DevProxy.Plugins.dll when using Dev Proxy v0.29.0 or later +- Code action: Update single or all plugin paths to DevProxy.Plugins.dll ### Changed: - Snippets: Updated all snippets to use `v0.29.0` schema -- Snippets: Updated all snuppers to use new DLL name, `DevProxy.Plugins.dll` +- Snippets: Updated all snippets to use new DLL name, `DevProxy.Plugins.dll` - Notification: Upgrade notification invokes package manager to upgrade Dev Proxy +- Improved diagnostics range detection to ensure that they only appear againt the relevant code and don't overlap with ending quotes and commas ## [0.24.0] - 2025-06-04 @@ -29,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Snippets: Added `devproxy-plugin-openai-telemetry-config` - OpenAITelemetryPlugin config section - Snippets: Added `devproxy-plugin-prices-file` - OpenAITelemetryPlugin telemetry prices file - Snippets: Added `devproxy-plugin-price` - OpenAITelemetryPlugin telemetry model price +- Code ### Changed: diff --git a/README.md b/README.md index 7a78185..a25d551 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ The following sections describe the features that the extension contributes to V ### Code Actions - Update schema to match installed version of Dev Proxy +- Update single or all plugin paths to DevProxy.Plugins.dll ### Code Lenses @@ -44,6 +45,7 @@ The following sections describe the features that the extension contributes to V - Check for configSections that are not used in plugins - Check for reporter plugin when a summary plugin is used - Check that ApiCenterOnboardingPlugin is placed after OpenApiSpecGeneratorPlugin +- Check that pluginPath in plugin instance is correctly set to DevProxy.Plugins.dll when using Dev Proxy v0.29.0 or later ### Editor Actions diff --git a/package-lock.json b/package-lock.json index fea3092..4864100 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "dev-proxy-toolkit", - "version": "0.25.2", + "version": "0.25.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dev-proxy-toolkit", - "version": "0.25.1", + "version": "0.25.3", "dependencies": { - "json-to-ast": "^2.1.0" + "json-to-ast": "^2.1.0", + "semver": "^7.7.2" }, "devDependencies": { "@types/json-to-ast": "^2.1.4", @@ -5341,10 +5342,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" diff --git a/package.json b/package.json index 768f5d0..0697687 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "dev-proxy-toolkit", "displayName": "Dev Proxy Toolkit", "description": "Makes it easy to create and update Dev Proxy configuration files.", - "version": "0.25.2", + "version": "0.25.3", "publisher": "garrytrinder", "engines": { "vscode": "^1.101.0" @@ -207,6 +207,7 @@ "webpack-cli": "^5.1.4" }, "dependencies": { - "json-to-ast": "^2.1.0" + "json-to-ast": "^2.1.0", + "semver": "^7.7.2" } } diff --git a/src/codeactions.ts b/src/codeactions.ts index 66486fe..866ba47 100644 --- a/src/codeactions.ts +++ b/src/codeactions.ts @@ -1,31 +1,104 @@ import * as vscode from 'vscode'; -import { DevProxyInstall } from './types'; +import {DevProxyInstall} from './types'; export const registerCodeActions = (context: vscode.ExtensionContext) => { - const devProxyInstall = context.globalState.get('devProxyInstall'); - if (!devProxyInstall) { - return; - } - const devProxyVersion = devProxyInstall.isBeta ? devProxyInstall.version.split('-')[0] : devProxyInstall.version; - context.subscriptions.push( - vscode.languages.registerCodeActionsProvider('json', { - provideCodeActions: (document, range, context, token) => { - const diagnostic = context.diagnostics.find(diagnostic => { - return diagnostic.code === 'invalidSchema'; - }); - if (diagnostic) { - const fix = new vscode.CodeAction('Update schema', vscode.CodeActionKind.QuickFix); - fix.edit = new vscode.WorkspaceEdit(); - fix.edit.replace( - document.uri, - new vscode.Range( - diagnostic.range.start, - diagnostic.range.end - ), - `$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v${devProxyVersion}/rc.schema.json",` - ); - return [fix]; - } - } - })); -}; \ No newline at end of file + const devProxyInstall = + context.globalState.get('devProxyInstall'); + if (!devProxyInstall) { + return; + } + const devProxyVersion = devProxyInstall.isBeta + ? devProxyInstall.version.split('-')[0] + : devProxyInstall.version; + + // Code action for invalid schema + context.subscriptions.push( + vscode.languages.registerCodeActionsProvider('json', { + provideCodeActions: (document, range, context, token) => { + const diagnostic = context.diagnostics.find(diagnostic => { + return diagnostic.code === 'invalidSchema'; + }); + if (diagnostic) { + const fix = new vscode.CodeAction( + 'Update schema', + vscode.CodeActionKind.QuickFix, + ); + fix.edit = new vscode.WorkspaceEdit(); + fix.edit.replace( + document.uri, + new vscode.Range(diagnostic.range.start, diagnostic.range.end), + `https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v${devProxyVersion}/rc.schema.json`, + ); + return [fix]; + } + }, + }), + ); + + // Code action for deprecated plugin paths (individual and bulk updates) + context.subscriptions.push( + vscode.languages.registerCodeActionsProvider('json', { + provideCodeActions: (document, range, context, token) => { + const correctPluginPath = '~appFolder/plugins/DevProxy.Plugins.dll'; + + // Check if the current range intersects with a deprecated plugin path diagnostic + const currentDiagnostic = context.diagnostics.find(diagnostic => { + return diagnostic.code === 'deprecatedPluginPath' && + diagnostic.range.intersection(range); + }); + + // Only provide deprecated plugin path actions if user is on a deprecated plugin path diagnostic + if (!currentDiagnostic) { + return []; + } + + const fixes: vscode.CodeAction[] = []; + + // Individual fix for the current diagnostic + const individualFix = new vscode.CodeAction( + 'Update plugin path', + vscode.CodeActionKind.QuickFix, + ); + individualFix.edit = new vscode.WorkspaceEdit(); + individualFix.edit.replace( + document.uri, + new vscode.Range( + currentDiagnostic.range.start, + currentDiagnostic.range.end, + ), + correctPluginPath, + ); + fixes.push(individualFix); + + // Bulk fix for all deprecated plugin paths in the document (only show when on a deprecated plugin path) + const allDeprecatedPluginPathDiagnostics = vscode.languages + .getDiagnostics(document.uri) + .filter(diagnostic => { + return diagnostic.code === 'deprecatedPluginPath'; + }); + + if (allDeprecatedPluginPathDiagnostics.length > 1) { + const bulkFix = new vscode.CodeAction( + `Update all plugin paths`, + vscode.CodeActionKind.QuickFix, + ); + bulkFix.edit = new vscode.WorkspaceEdit(); + + // Update all deprecated plugin paths + allDeprecatedPluginPathDiagnostics.forEach(diagnostic => { + bulkFix.edit!.replace( + document.uri, + new vscode.Range(diagnostic.range.start, diagnostic.range.end), + correctPluginPath, + ); + }); + + bulkFix.isPreferred = true; + fixes.push(bulkFix); + } + + return fixes; + }, + }), + ); +}; diff --git a/src/diagnostics.ts b/src/diagnostics.ts index afe5cd7..7f077b0 100644 --- a/src/diagnostics.ts +++ b/src/diagnostics.ts @@ -1,15 +1,17 @@ import * as vscode from 'vscode'; -import parse from "json-to-ast"; -import { pluginSnippets } from "./constants"; -import { getASTNode, getRangeFromASTNode } from "./helpers"; -import { DevProxyInstall, PluginConfig } from "./types"; +import parse from 'json-to-ast'; +import {pluginSnippets} from './constants'; +import {getASTNode, getRangeFromASTNode} from './helpers'; +import {DevProxyInstall, PluginConfig} from './types'; +import * as semver from 'semver'; export const updateConfigFileDiagnostics = ( context: vscode.ExtensionContext, document: vscode.TextDocument, collection: vscode.DiagnosticCollection, ): void => { - const devProxyInstall = context.globalState.get('devProxyInstall'); + const devProxyInstall = + context.globalState.get('devProxyInstall'); if (!devProxyInstall) { return; } @@ -18,7 +20,7 @@ export const updateConfigFileDiagnostics = ( const pluginsNode = getPluginsNode(documentNode); checkSchemaCompatibility(documentNode, devProxyInstall, diagnostics); - checkPlugins(pluginsNode, diagnostics, documentNode); + checkPlugins(pluginsNode, diagnostics, documentNode, devProxyInstall); checkConfigSection(documentNode, diagnostics); collection.set(document.uri, diagnostics); @@ -29,7 +31,8 @@ export const updateFileDiagnostics = ( document: vscode.TextDocument, collection: vscode.DiagnosticCollection, ): void => { - const devProxyInstall = context.globalState.get('devProxyInstall'); + const devProxyInstall = + context.globalState.get('devProxyInstall'); if (!devProxyInstall) { return; } @@ -42,10 +45,17 @@ export const updateFileDiagnostics = ( collection.set(document.uri, diagnostics); }; -const checkConfigSection = (documentNode: parse.ObjectNode, diagnostics: vscode.Diagnostic[]) => { - const objects = documentNode.children.filter((node) => node.type === 'Property' && (node as parse.PropertyNode).value.type === 'Object'); +const checkConfigSection = ( + documentNode: parse.ObjectNode, + diagnostics: vscode.Diagnostic[], +) => { + const objects = documentNode.children.filter( + node => + node.type === 'Property' && + (node as parse.PropertyNode).value.type === 'Object', + ); - objects.forEach((object) => { + objects.forEach(object => { const objectNode = object as parse.PropertyNode; const objectName = objectNode.key.value as string; const pluginNodes = getPluginsNode(documentNode); @@ -55,14 +65,18 @@ const checkConfigSection = (documentNode: parse.ObjectNode, diagnostics: vscode. } if (pluginNodes && pluginNodes.value.type === 'Array') { - const plugins = (pluginNodes.value as parse.ArrayNode).children as parse.ObjectNode[]; - const matchFound = plugins.some((plugin) => { + const plugins = (pluginNodes.value as parse.ArrayNode) + .children as parse.ObjectNode[]; + const matchFound = plugins.some(plugin => { const configSectionNode = getASTNode( plugin.children, 'Identifier', - 'configSection' + 'configSection', + ); + return ( + configSectionNode && + (configSectionNode.value as parse.LiteralNode).value === objectName ); - return configSectionNode && (configSectionNode.value as parse.LiteralNode).value === objectName; }); if (matchFound) { @@ -73,23 +87,30 @@ const checkConfigSection = (documentNode: parse.ObjectNode, diagnostics: vscode. const diagnostic = new vscode.Diagnostic( getRangeFromASTNode(objectNode), `Config section '${objectName}' does not correspond to any plugin. Remove it or add a plugin with a matching configSection.`, - vscode.DiagnosticSeverity.Warning + vscode.DiagnosticSeverity.Warning, ); diagnostic.code = 'invalidConfigSection'; diagnostics.push(diagnostic); }); }; -const checkSchemaCompatibility = (documentNode: parse.ObjectNode, devProxyInstall: DevProxyInstall, diagnostics: vscode.Diagnostic[]) => { +const checkSchemaCompatibility = ( + documentNode: parse.ObjectNode, + devProxyInstall: DevProxyInstall, + diagnostics: vscode.Diagnostic[], +) => { const schemaNode = getASTNode(documentNode.children, 'Identifier', '$schema'); if (schemaNode) { - const schemaValue = (schemaNode.value as parse.LiteralNode).value as string; - const devProxyVersion = devProxyInstall.isBeta ? devProxyInstall.version.split('-')[0] : devProxyInstall.version; + const schemaValueNode = schemaNode.value as parse.LiteralNode; + const schemaValue = schemaValueNode.value as string; + const devProxyVersion = devProxyInstall.isBeta + ? devProxyInstall.version.split('-')[0] + : devProxyInstall.version; if (!schemaValue.includes(`${devProxyVersion}`)) { const diagnostic = new vscode.Diagnostic( - getRangeFromASTNode(schemaNode), + getRangeFromASTNode(schemaValueNode), `Schema version is not compatible with the installed version of Dev Proxy. Expected v${devProxyVersion}`, - vscode.DiagnosticSeverity.Warning + vscode.DiagnosticSeverity.Warning, ); diagnostic.code = 'invalidSchema'; diagnostics.push(diagnostic); @@ -97,9 +118,13 @@ const checkSchemaCompatibility = (documentNode: parse.ObjectNode, devProxyInstal } }; -const checkPlugins = (pluginsNode: parse.PropertyNode | undefined, diagnostics: vscode.Diagnostic[], documentNode: parse.ObjectNode) => { - if (pluginsNode && - (pluginsNode.value as parse.ArrayNode)) { +const checkPlugins = ( + pluginsNode: parse.PropertyNode | undefined, + diagnostics: vscode.Diagnostic[], + documentNode: parse.ObjectNode, + devProxyInstall?: DevProxyInstall, +) => { + if (pluginsNode && (pluginsNode.value as parse.ArrayNode)) { const pluginNodes = (pluginsNode.value as parse.ArrayNode) .children as parse.ObjectNode[]; @@ -107,43 +132,68 @@ const checkPlugins = (pluginsNode: parse.PropertyNode | undefined, diagnostics: warnOnReporterPosition(pluginNodes, diagnostics); validatePluginConfigurations(pluginNodes, diagnostics, documentNode); checkForSummaryPluginWithoutReporter(pluginNodes, diagnostics); - checkAPICOnboardingPluginAfterOpenApiSpecGeneratorPlugin(pluginNodes, diagnostics); + checkAPICOnboardingPluginAfterOpenApiSpecGeneratorPlugin( + pluginNodes, + diagnostics, + ); + checkDeprecatedPluginPath( + pluginNodes, + diagnostics, + documentNode, + devProxyInstall?.version, + ); } }; -const validatePluginConfigurations = (pluginNodes: parse.ObjectNode[], diagnostics: vscode.Diagnostic[], documentNode: parse.ObjectNode) => { +const validatePluginConfigurations = ( + pluginNodes: parse.ObjectNode[], + diagnostics: vscode.Diagnostic[], + documentNode: parse.ObjectNode, +) => { pluginNodes.forEach((pluginNode: parse.ObjectNode) => { const pluginNameNode = getASTNode( pluginNode.children, 'Identifier', - 'name' + 'name', ); const pluginName = (pluginNameNode?.value as parse.LiteralNode) .value as string; const enabledNode = getASTNode( pluginNode.children, 'Identifier', - 'enabled' + 'enabled', ); const isEnabled = (enabledNode?.value as parse.LiteralNode) .value as boolean; const pluginSnippet = pluginSnippets[pluginName]; - checkPluginConfiguration(pluginNode, diagnostics, pluginName, isEnabled, documentNode, pluginSnippet); + checkPluginConfiguration( + pluginNode, + diagnostics, + pluginName, + isEnabled, + documentNode, + pluginSnippet, + ); }); }; -const warnOnReporterPosition = (pluginNodes: parse.ObjectNode[], diagnostics: vscode.Diagnostic[]) => { - const reporterIndex = pluginNodes.findIndex((pluginNode: parse.ObjectNode) => { - const pluginNameNode = getASTNode( - pluginNode.children, - 'Identifier', - 'name' - ); - const pluginName = (pluginNameNode?.value as parse.LiteralNode) - .value as string; - return pluginName.toLowerCase().includes('reporter'); - }); +const warnOnReporterPosition = ( + pluginNodes: parse.ObjectNode[], + diagnostics: vscode.Diagnostic[], +) => { + const reporterIndex = pluginNodes.findIndex( + (pluginNode: parse.ObjectNode) => { + const pluginNameNode = getASTNode( + pluginNode.children, + 'Identifier', + 'name', + ); + const pluginName = (pluginNameNode?.value as parse.LiteralNode) + .value as string; + return pluginName.toLowerCase().includes('reporter'); + }, + ); if (reporterIndex !== -1) { // check if we have any more plugins after the reporter plugin @@ -151,22 +201,24 @@ const warnOnReporterPosition = (pluginNodes: parse.ObjectNode[], diagnostics: vs // if we do, add a warning to the reporter plugin stating that it should be the last plugin if (pluginsAfterReporter.length > 0) { // check if there are any plugins after the reporter plugin that are not reporters - const pluginAfterReporter = pluginsAfterReporter.find((pluginNode: parse.ObjectNode) => { - const pluginNameNode = getASTNode( - pluginNode.children, - 'Identifier', - 'name' - ); - const pluginName = (pluginNameNode?.value as parse.LiteralNode) - .value as string; - return !pluginName.toLowerCase().includes('reporter'); - }); + const pluginAfterReporter = pluginsAfterReporter.find( + (pluginNode: parse.ObjectNode) => { + const pluginNameNode = getASTNode( + pluginNode.children, + 'Identifier', + 'name', + ); + const pluginName = (pluginNameNode?.value as parse.LiteralNode) + .value as string; + return !pluginName.toLowerCase().includes('reporter'); + }, + ); // if there are, add a warning to the reporter plugin if (pluginAfterReporter) { const diagnostic = new vscode.Diagnostic( getRangeFromASTNode(pluginNodes[reporterIndex]), 'Reporters should be placed after other plugins.', - vscode.DiagnosticSeverity.Warning + vscode.DiagnosticSeverity.Warning, ); diagnostics.push(diagnostic); } @@ -174,43 +226,56 @@ const warnOnReporterPosition = (pluginNodes: parse.ObjectNode[], diagnostics: vs } }; -const checkAtLeastOneEnabledPlugin = (pluginNodes: parse.ObjectNode[], diagnostics: vscode.Diagnostic[], pluginsNode: parse.PropertyNode) => { +const checkAtLeastOneEnabledPlugin = ( + pluginNodes: parse.ObjectNode[], + diagnostics: vscode.Diagnostic[], + pluginsNode: parse.PropertyNode, +) => { // check if there are any plugins if (pluginNodes.length === 0) { diagnostics.push( new vscode.Diagnostic( getRangeFromASTNode(pluginsNode), 'Add at least one plugin', - vscode.DiagnosticSeverity.Warning - ) + vscode.DiagnosticSeverity.Warning, + ), ); } else { // check if there are any enabled plugins - const enabledPlugins = pluginNodes.filter((pluginNode: parse.ObjectNode) => { - const enabledNode = getASTNode( - pluginNode.children, - 'Identifier', - 'enabled' - ); - return (enabledNode?.value as parse.LiteralNode).value as boolean; - }); + const enabledPlugins = pluginNodes.filter( + (pluginNode: parse.ObjectNode) => { + const enabledNode = getASTNode( + pluginNode.children, + 'Identifier', + 'enabled', + ); + return (enabledNode?.value as parse.LiteralNode).value as boolean; + }, + ); if (enabledPlugins.length === 0) { diagnostics.push( new vscode.Diagnostic( getRangeFromASTNode(pluginsNode), 'At least one plugin must be enabled', - vscode.DiagnosticSeverity.Warning - ) + vscode.DiagnosticSeverity.Warning, + ), ); } } }; -const checkPluginConfiguration = (pluginNode: parse.ObjectNode, diagnostics: vscode.Diagnostic[], pluginName: string, isEnabled: boolean, documentNode: parse.ObjectNode, pluginSnippet: { instance: string; config?: PluginConfig; }) => { +const checkPluginConfiguration = ( + pluginNode: parse.ObjectNode, + diagnostics: vscode.Diagnostic[], + pluginName: string, + isEnabled: boolean, + documentNode: parse.ObjectNode, + pluginSnippet: {instance: string; config?: PluginConfig}, +) => { const configSectionNode = getASTNode( pluginNode.children, 'Identifier', - 'configSection' + 'configSection', ); // if the plugin does not require a config section, we should not have one @@ -221,8 +286,8 @@ const checkPluginConfiguration = (pluginNode: parse.ObjectNode, diagnostics: vsc `${pluginName} does not require a config section.`, isEnabled ? vscode.DiagnosticSeverity.Error - : vscode.DiagnosticSeverity.Warning - ) + : vscode.DiagnosticSeverity.Warning, + ), ); return; } @@ -236,34 +301,33 @@ const checkPluginConfiguration = (pluginNode: parse.ObjectNode, diagnostics: vsc `${pluginName} requires a config section.`, isEnabled ? vscode.DiagnosticSeverity.Error - : vscode.DiagnosticSeverity.Warning - ) + : vscode.DiagnosticSeverity.Warning, + ), ); } else if (pluginSnippet.config?.required === false) { const pluginNameNode = getASTNode( pluginNode.children, 'Identifier', - 'name' + 'name', ); if (pluginNameNode) { diagnostics.push( new vscode.Diagnostic( getRangeFromASTNode(pluginNameNode.value), `${pluginName} can be configured with a configSection. Use '${pluginSnippet.config?.name}' snippet to create one.`, - vscode.DiagnosticSeverity.Information - ) + vscode.DiagnosticSeverity.Information, + ), ); } } } else { // if there is a config section defined on the plugin, we should have the config section defined in the document - const configSectionName = ( - configSectionNode.value as parse.LiteralNode - ).value as string; + const configSectionName = (configSectionNode.value as parse.LiteralNode) + .value as string; const configSection = getASTNode( documentNode.children, 'Identifier', - configSectionName + configSectionName, ); if (!configSection) { @@ -273,40 +337,41 @@ const checkPluginConfiguration = (pluginNode: parse.ObjectNode, diagnostics: vsc `${configSectionName} config section is missing. Use '${pluginSnippet.config?.name}' snippet to create one.`, isEnabled ? vscode.DiagnosticSeverity.Error - : vscode.DiagnosticSeverity.Warning - ) + : vscode.DiagnosticSeverity.Warning, + ), ); } } }; const getPluginsNode = (documentNode: parse.ObjectNode) => { - return getASTNode( - documentNode.children, - 'Identifier', - 'plugins' - ); + return getASTNode(documentNode.children, 'Identifier', 'plugins'); }; -const getObjectNodeFromDocument = (document: vscode.TextDocument): parse.ObjectNode => { +const getObjectNodeFromDocument = ( + document: vscode.TextDocument, +): parse.ObjectNode => { return parse(document.getText()) as parse.ObjectNode; }; -function checkForSummaryPluginWithoutReporter(pluginNodes: parse.ObjectNode[], diagnostics: vscode.Diagnostic[]) { +function checkForSummaryPluginWithoutReporter( + pluginNodes: parse.ObjectNode[], + diagnostics: vscode.Diagnostic[], +) { const summaryPluginNames = ['ExecutionSummaryPlugin', 'UrlDiscoveryPlugin']; const summaryPlugin = pluginNodes.find((pluginNode: parse.ObjectNode) => { const pluginNameNode = getASTNode( pluginNode.children, 'Identifier', - 'name' + 'name', ); const pluginName = (pluginNameNode?.value as parse.LiteralNode) .value as string; const enabledNode = getASTNode( pluginNode.children, 'Identifier', - 'enabled' + 'enabled', ); const isEnabled = (enabledNode?.value as parse.LiteralNode) .value as boolean; @@ -318,14 +383,14 @@ function checkForSummaryPluginWithoutReporter(pluginNodes: parse.ObjectNode[], d const pluginNameNode = getASTNode( pluginNode.children, 'Identifier', - 'name' + 'name', ); const pluginName = (pluginNameNode?.value as parse.LiteralNode) .value as string; const enabledNode = getASTNode( pluginNode.children, 'Identifier', - 'enabled' + 'enabled', ); const isEnabled = (enabledNode?.value as parse.LiteralNode) .value as boolean; @@ -337,52 +402,96 @@ function checkForSummaryPluginWithoutReporter(pluginNodes: parse.ObjectNode[], d new vscode.Diagnostic( getRangeFromASTNode(summaryPlugin), `Summary plugins should be used with a reporter plugin.`, - vscode.DiagnosticSeverity.Warning - ) + vscode.DiagnosticSeverity.Warning, + ), ); } } } -function checkAPICOnboardingPluginAfterOpenApiSpecGeneratorPlugin(pluginNodes: parse.ObjectNode[], diagnostics: vscode.Diagnostic[]) { - const openApiSpecGeneratorPluginIndex = pluginNodes.findIndex((pluginNode: parse.ObjectNode) => { - const pluginNameNode = getASTNode( - pluginNode.children, - 'Identifier', - 'name' - ); - const pluginName = (pluginNameNode?.value as parse.LiteralNode) - .value as string; - const enabledNode = getASTNode( - pluginNode.children, - 'Identifier', - 'enabled' - ); - const isEnabled = (enabledNode?.value as parse.LiteralNode) - .value as boolean; - return pluginName === 'OpenApiSpecGeneratorPlugin' && isEnabled; - } - ); - if (openApiSpecGeneratorPluginIndex !== -1) { - const apiCenterOnboardingPluginIndex = pluginNodes.findIndex((pluginNode: parse.ObjectNode) => { +function checkAPICOnboardingPluginAfterOpenApiSpecGeneratorPlugin( + pluginNodes: parse.ObjectNode[], + diagnostics: vscode.Diagnostic[], +) { + const openApiSpecGeneratorPluginIndex = pluginNodes.findIndex( + (pluginNode: parse.ObjectNode) => { const pluginNameNode = getASTNode( pluginNode.children, 'Identifier', - 'name' + 'name', ); const pluginName = (pluginNameNode?.value as parse.LiteralNode) .value as string; - return pluginName === 'ApiCenterOnboardingPlugin'; - } + const enabledNode = getASTNode( + pluginNode.children, + 'Identifier', + 'enabled', + ); + const isEnabled = (enabledNode?.value as parse.LiteralNode) + .value as boolean; + return pluginName === 'OpenApiSpecGeneratorPlugin' && isEnabled; + }, + ); + if (openApiSpecGeneratorPluginIndex !== -1) { + const apiCenterOnboardingPluginIndex = pluginNodes.findIndex( + (pluginNode: parse.ObjectNode) => { + const pluginNameNode = getASTNode( + pluginNode.children, + 'Identifier', + 'name', + ); + const pluginName = (pluginNameNode?.value as parse.LiteralNode) + .value as string; + return pluginName === 'ApiCenterOnboardingPlugin'; + }, ); - if (apiCenterOnboardingPluginIndex !== -1 && apiCenterOnboardingPluginIndex < openApiSpecGeneratorPluginIndex) { + if ( + apiCenterOnboardingPluginIndex !== -1 && + apiCenterOnboardingPluginIndex < openApiSpecGeneratorPluginIndex + ) { diagnostics.push( new vscode.Diagnostic( getRangeFromASTNode(pluginNodes[openApiSpecGeneratorPluginIndex]), 'OpenApiSpecGeneratorPlugin should be placed before ApiCenterOnboardingPlugin.', - vscode.DiagnosticSeverity.Warning - ) + vscode.DiagnosticSeverity.Warning, + ), ); } } -} \ No newline at end of file +} + +function checkDeprecatedPluginPath( + pluginNodes: parse.ObjectNode[], + diagnostics: vscode.Diagnostic[], + documentNode: parse.ObjectNode, + version: string | undefined, +) { + if (!version || semver.lt(version, '0.29.0')) { + return; + } + + // Check each plugin for deprecated pluginPath + pluginNodes.forEach((pluginNode: parse.ObjectNode) => { + const pluginPathNode = getASTNode( + pluginNode.children, + 'Identifier', + 'pluginPath', + ); + + if (pluginPathNode) { + const pluginPath = (pluginPathNode.value as parse.LiteralNode) + .value as string; + + // Check for old plugin path format + if (pluginPath === '~appFolder/plugins/dev-proxy-plugins.dll') { + const diagnostic = new vscode.Diagnostic( + getRangeFromASTNode(pluginPathNode.value), + `The pluginPath '${pluginPath}' was deprecated in v0.29. Use '~appFolder/plugins/DevProxy.Plugins.dll' instead.`, + vscode.DiagnosticSeverity.Error, + ); + diagnostic.code = 'deprecatedPluginPath'; + diagnostics.push(diagnostic); + } + } + }); +} diff --git a/src/helpers.ts b/src/helpers.ts index e1a8c53..f89388c 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -21,8 +21,23 @@ export const getRangeFromASTNode = ( ) => { const startLine = node?.loc?.start.line || 0; const endLine = node?.loc?.end.line || 0; - const startColumn = node?.loc?.start.column || 0; - const endColumn = node?.loc?.end.column || 0; + let startColumn = node?.loc?.start.column || 0; + let endColumn = node?.loc?.end.column || 0; + + // For string literals, exclude the surrounding quotes from the range + // The json-to-ast library uses 1-based column numbers, but we need 0-based for VS Code + // For string literals: column points to the quote, we want to point to the content + if (node.type === 'Literal' && typeof (node as parse.LiteralNode).value === 'string') { + // Convert to 0-based and adjust for quotes: + // Start: column is 1-based pointing to quote, so column-1+1 = column (no change) + // End: column is 1-based pointing after quote, so column-1-1 = column-2 + startColumn = startColumn; // column already points to quote, no change needed for 0-based + skip quote + endColumn = endColumn - 2; // convert to 0-based and skip closing quote + } else { + // For non-string literals, just convert from 1-based to 0-based + startColumn = startColumn - 1; + endColumn = endColumn - 1; + } // we remove 1 from the line numbers because vscode uses 0 based line numbers return new vscode.Range( diff --git a/src/test/examples/config-plugins-codelens.json b/src/test/examples/config-plugins-codelens.json index 794d1c0..3b513c0 100644 --- a/src/test/examples/config-plugins-codelens.json +++ b/src/test/examples/config-plugins-codelens.json @@ -1,11 +1,11 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/dev-proxy/main/schemas/v0.24.0/rc.schema.json", + "$schema": "https://raw.githubusercontent.com/microsoft/dev-proxy/main/schemas/v0.29.0/rc.schema.json", "plugins": [ { "name": "CachingGuidancePlugin", "enabled": true, "pluginPath": "~appFolder/plugins/dev-proxy-plugins.dll", - "configSection": "cachingGuidance" + "configSection": "cachingGuidance" }, { "name": "DevToolsPlugin", diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index 4715e0f..9916879 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -512,5 +512,31 @@ suite('schema', () => { }); assert.deepStrictEqual(actual, expected); }); +}); + +suite('diagnostic ranges', () => { + + teardown(async () => { + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + }); -}); \ No newline at end of file + test('should exclude quotes from string literal ranges', async () => { + // Test the core functionality by parsing JSON and checking ranges + const parse = require('json-to-ast'); + const { getRangeFromASTNode } = require('../helpers'); + + const jsonText = '{"key": "value"}'; + const ast = parse(jsonText); + const keyNode = ast.children[0]; + + // Test our modified range + const range = getRangeFromASTNode(keyNode.value); + const modifiedText = jsonText.substring( + range.start.character, + range.end.character + ); + + // Verify that the range excludes quotes and extracts just the string content + assert.strictEqual(modifiedText, 'value', 'Should extract just the string content without quotes'); + }); +});