Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [0.15.1] - Unreleased

### Added:

- Diagnostics: Ensure at least one plugin is enabled

### Changed:

- Snippets: `devproxy-plugin-open-api-spec-generator` - OpenApiSpecGeneratorPlugin config section
- Snippets: All snippets that reference schemas updated to use `v0.24.0` schema
- Diagnostics: Refactored code to be more readable and maintainable
- Diagnostics: Improved config section check

### Fixed:

- Snippets: Fix invalid Json in `devproxy-config-file`
- Diagnostics: Fixed update schema check in proxy files

## [0.14.0] - 2024-11-27

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ The following sections describe the features that the extension contributes to V
- Check for missing `configSection` when defined in plugin instance
- Check that schema matches installed version of Dev Proxy
- Check that reporters are placed after plugins
- Check that at least one plugin is enabled

### Editor Actions

Expand Down
325 changes: 187 additions & 138 deletions src/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import * as vscode from 'vscode';
import parse from "json-to-ast";
import { pluginSnippets } from "./constants";
import { getASTNode, getRangeFromASTNode } from "./helpers";
import { DevProxyInstall } from './types';
import { DevProxyInstall, PluginConfig } from "./types";

export const updateConfigDiagnostics = (
export const updateConfigFileDiagnostics = (
context: vscode.ExtensionContext,
document: vscode.TextDocument,
collection: vscode.DiagnosticCollection,
Expand All @@ -14,145 +14,16 @@ export const updateConfigDiagnostics = (
return;
}
const diagnostics: vscode.Diagnostic[] = [];
const documentNode = parse(document.getText()) as parse.ObjectNode;
const documentNode = getObjectNodeFromDocument(document);
const pluginsNode = getPluginsNode(documentNode);

// check if schema version is compatible
checkSchemaCompatibility(documentNode, devProxyInstall, diagnostics);

// check validity of plugins
const pluginsNode = getASTNode(
documentNode.children,
'Identifier',
'plugins'
);
if (
pluginsNode &&
(pluginsNode.value as parse.ArrayNode).children.length !== 0
) {
const pluginNodes = (pluginsNode.value as parse.ArrayNode)
.children as parse.ObjectNode[];

// check for plugins
if (pluginNodes.length === 0) {
diagnostics.push(
new vscode.Diagnostic(
getRangeFromASTNode(pluginsNode),
'Add at least one plugin',
vscode.DiagnosticSeverity.Error
)
);
}

// check if we have any plugins that contain the name reporter in the plugins node
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
const pluginsAfterReporter = pluginNodes.slice(reporterIndex + 1);
// 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');
});
// 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
);
diagnostics.push(diagnostic);
}
}
}

// does the plugin have a config section?
pluginNodes.forEach((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;
const pluginSnippet = pluginSnippets[pluginName];
const requiresConfig = pluginSnippet.config
? pluginSnippet.config.required
: false;

if (requiresConfig) {
// check to see if the plugin has a config section
const configSectionNode = getASTNode(
pluginNode.children,
'Identifier',
'configSection'
);
if (!configSectionNode) {
// there is no config section defined on the plugin instance
diagnostics.push(
new vscode.Diagnostic(
getRangeFromASTNode(pluginNode),
`${pluginName} requires a config section.`,
isEnabled
? vscode.DiagnosticSeverity.Error
: vscode.DiagnosticSeverity.Warning
)
);
} else {
// check to see if the config section is in the document
const configSectionName = (
configSectionNode.value as parse.LiteralNode
).value as string;
const configSection = getASTNode(
documentNode.children,
'Identifier',
configSectionName
);

if (!configSection) {
diagnostics.push(
new vscode.Diagnostic(
getRangeFromASTNode(configSectionNode.value),
`${configSectionName} config section is missing. Use '${pluginSnippet.config?.name}' snippet to create one.`,
isEnabled
? vscode.DiagnosticSeverity.Error
: vscode.DiagnosticSeverity.Warning
)
);
}
}
}
});
}
checkPlugins(pluginsNode, diagnostics, documentNode);

collection.set(document.uri, diagnostics);
};

export const updateDiagnostics = (
export const updateFileDiagnostics = (
context: vscode.ExtensionContext,
document: vscode.TextDocument,
collection: vscode.DiagnosticCollection,
Expand All @@ -163,15 +34,14 @@ export const updateDiagnostics = (
}

const diagnostics: vscode.Diagnostic[] = [];
const documentNode = parse(document.getText()) as parse.ObjectNode;
const documentNode = getObjectNodeFromDocument(document);

// check if schema version is compatible
checkSchemaCompatibility(documentNode, devProxyInstall, diagnostics);

collection.set(document.uri, diagnostics);
};

export 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;
Expand All @@ -187,3 +57,182 @@ export const checkSchemaCompatibility = (documentNode: parse.ObjectNode, devProx
}
}
};

const checkPlugins = (pluginsNode: parse.PropertyNode | undefined, diagnostics: vscode.Diagnostic[], documentNode: parse.ObjectNode) => {
if (pluginsNode &&
(pluginsNode.value as parse.ArrayNode)) {
const pluginNodes = (pluginsNode.value as parse.ArrayNode)
.children as parse.ObjectNode[];

checkAtLeastOneEnabledPlugin(pluginNodes, diagnostics, pluginsNode);
warnOnReporterPosition(pluginNodes, diagnostics);
validatePluginConfigurations(pluginNodes, diagnostics, documentNode);
}
};

const validatePluginConfigurations = (pluginNodes: parse.ObjectNode[], diagnostics: vscode.Diagnostic[], documentNode: parse.ObjectNode) => {
pluginNodes.forEach((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;
const pluginSnippet = pluginSnippets[pluginName];

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');
});

if (reporterIndex !== -1) {
// check if we have any more plugins after the reporter plugin
const pluginsAfterReporter = pluginNodes.slice(reporterIndex + 1);
// 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');
});
// 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
);
diagnostics.push(diagnostic);
}
}
}
};

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.Error
)
);
} 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;
});
if (enabledPlugins.length === 0) {
diagnostics.push(
new vscode.Diagnostic(
getRangeFromASTNode(pluginsNode),
'At least one plugin must be enabled',
vscode.DiagnosticSeverity.Error
)
);
}
}
};

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'
);

// if the plugin does not require a config section, we should not have one
if (!pluginSnippet.config && configSectionNode) {
diagnostics.push(
new vscode.Diagnostic(
getRangeFromASTNode(configSectionNode),
`${pluginName} does not require a config section.`,
isEnabled
? vscode.DiagnosticSeverity.Error
: vscode.DiagnosticSeverity.Warning
)
);
return;
}

// if there is no config section defined on the plugin, we should have one if the plugin requires it
if (!configSectionNode) {
if (pluginSnippet.config?.required) {
diagnostics.push(
new vscode.Diagnostic(
getRangeFromASTNode(pluginNode),
`${pluginName} requires a config section.`,
isEnabled
? vscode.DiagnosticSeverity.Error
: vscode.DiagnosticSeverity.Warning
)
);
}
} 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 configSection = getASTNode(
documentNode.children,
'Identifier',
configSectionName
);

if (!configSection) {
diagnostics.push(
new vscode.Diagnostic(
getRangeFromASTNode(configSectionNode.value),
`${configSectionName} config section is missing. Use '${pluginSnippet.config?.name}' snippet to create one.`,
isEnabled
? vscode.DiagnosticSeverity.Error
: vscode.DiagnosticSeverity.Warning
)
);
}
}
};

const getPluginsNode = (documentNode: parse.ObjectNode) => {
return getASTNode(
documentNode.children,
'Identifier',
'plugins'
);
};

const getObjectNodeFromDocument = (document: vscode.TextDocument): parse.ObjectNode => {
return parse(document.getText()) as parse.ObjectNode;
};

Loading
Loading