Skip to content

Commit 15b6790

Browse files
authored
Refactor diagnostics (#170)
Closes #169 Closes #167
1 parent 4072d24 commit 15b6790

18 files changed

+253
-172
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
1010
## [0.15.1] - Unreleased
1111

12+
### Added:
13+
14+
- Diagnostics: Ensure at least one plugin is enabled
15+
1216
### Changed:
1317

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

1723
### Fixed:
1824

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

2128
## [0.14.0] - 2024-11-27
2229

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ The following sections describe the features that the extension contributes to V
3535
- Check for missing `configSection` when defined in plugin instance
3636
- Check that schema matches installed version of Dev Proxy
3737
- Check that reporters are placed after plugins
38+
- Check that at least one plugin is enabled
3839

3940
### Editor Actions
4041

src/diagnostics.ts

Lines changed: 187 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import * as vscode from 'vscode';
22
import parse from "json-to-ast";
33
import { pluginSnippets } from "./constants";
44
import { getASTNode, getRangeFromASTNode } from "./helpers";
5-
import { DevProxyInstall } from './types';
5+
import { DevProxyInstall, PluginConfig } from "./types";
66

7-
export const updateConfigDiagnostics = (
7+
export const updateConfigFileDiagnostics = (
88
context: vscode.ExtensionContext,
99
document: vscode.TextDocument,
1010
collection: vscode.DiagnosticCollection,
@@ -14,145 +14,16 @@ export const updateConfigDiagnostics = (
1414
return;
1515
}
1616
const diagnostics: vscode.Diagnostic[] = [];
17-
const documentNode = parse(document.getText()) as parse.ObjectNode;
17+
const documentNode = getObjectNodeFromDocument(document);
18+
const pluginsNode = getPluginsNode(documentNode);
1819

19-
// check if schema version is compatible
2020
checkSchemaCompatibility(documentNode, devProxyInstall, diagnostics);
21-
22-
// check validity of plugins
23-
const pluginsNode = getASTNode(
24-
documentNode.children,
25-
'Identifier',
26-
'plugins'
27-
);
28-
if (
29-
pluginsNode &&
30-
(pluginsNode.value as parse.ArrayNode).children.length !== 0
31-
) {
32-
const pluginNodes = (pluginsNode.value as parse.ArrayNode)
33-
.children as parse.ObjectNode[];
34-
35-
// check for plugins
36-
if (pluginNodes.length === 0) {
37-
diagnostics.push(
38-
new vscode.Diagnostic(
39-
getRangeFromASTNode(pluginsNode),
40-
'Add at least one plugin',
41-
vscode.DiagnosticSeverity.Error
42-
)
43-
);
44-
}
45-
46-
// check if we have any plugins that contain the name reporter in the plugins node
47-
const reporterIndex = pluginNodes.findIndex((pluginNode: parse.ObjectNode) => {
48-
const pluginNameNode = getASTNode(
49-
pluginNode.children,
50-
'Identifier',
51-
'name'
52-
);
53-
const pluginName = (pluginNameNode?.value as parse.LiteralNode)
54-
.value as string;
55-
return pluginName.toLowerCase().includes('reporter');
56-
});
57-
58-
if (reporterIndex !== -1) {
59-
// check if we have any more plugins after the reporter plugin
60-
const pluginsAfterReporter = pluginNodes.slice(reporterIndex + 1);
61-
// if we do, add a warning to the reporter plugin stating that it should be the last plugin
62-
if (pluginsAfterReporter.length > 0) {
63-
// check if there are any plugins after the reporter plugin that are not reporters
64-
const pluginAfterReporter = pluginsAfterReporter.find((pluginNode: parse.ObjectNode) => {
65-
const pluginNameNode = getASTNode(
66-
pluginNode.children,
67-
'Identifier',
68-
'name'
69-
);
70-
const pluginName = (pluginNameNode?.value as parse.LiteralNode)
71-
.value as string;
72-
return !pluginName.toLowerCase().includes('reporter');
73-
});
74-
// if there are, add a warning to the reporter plugin
75-
if (pluginAfterReporter) {
76-
const diagnostic = new vscode.Diagnostic(
77-
getRangeFromASTNode(pluginNodes[reporterIndex]),
78-
'Reporters should be placed after other plugins.',
79-
vscode.DiagnosticSeverity.Warning
80-
);
81-
diagnostics.push(diagnostic);
82-
}
83-
}
84-
}
85-
86-
// does the plugin have a config section?
87-
pluginNodes.forEach((pluginNode: parse.ObjectNode) => {
88-
const pluginNameNode = getASTNode(
89-
pluginNode.children,
90-
'Identifier',
91-
'name'
92-
);
93-
const pluginName = (pluginNameNode?.value as parse.LiteralNode)
94-
.value as string;
95-
const enabledNode = getASTNode(
96-
pluginNode.children,
97-
'Identifier',
98-
'enabled'
99-
);
100-
const isEnabled = (enabledNode?.value as parse.LiteralNode)
101-
.value as boolean;
102-
const pluginSnippet = pluginSnippets[pluginName];
103-
const requiresConfig = pluginSnippet.config
104-
? pluginSnippet.config.required
105-
: false;
106-
107-
if (requiresConfig) {
108-
// check to see if the plugin has a config section
109-
const configSectionNode = getASTNode(
110-
pluginNode.children,
111-
'Identifier',
112-
'configSection'
113-
);
114-
if (!configSectionNode) {
115-
// there is no config section defined on the plugin instance
116-
diagnostics.push(
117-
new vscode.Diagnostic(
118-
getRangeFromASTNode(pluginNode),
119-
`${pluginName} requires a config section.`,
120-
isEnabled
121-
? vscode.DiagnosticSeverity.Error
122-
: vscode.DiagnosticSeverity.Warning
123-
)
124-
);
125-
} else {
126-
// check to see if the config section is in the document
127-
const configSectionName = (
128-
configSectionNode.value as parse.LiteralNode
129-
).value as string;
130-
const configSection = getASTNode(
131-
documentNode.children,
132-
'Identifier',
133-
configSectionName
134-
);
135-
136-
if (!configSection) {
137-
diagnostics.push(
138-
new vscode.Diagnostic(
139-
getRangeFromASTNode(configSectionNode.value),
140-
`${configSectionName} config section is missing. Use '${pluginSnippet.config?.name}' snippet to create one.`,
141-
isEnabled
142-
? vscode.DiagnosticSeverity.Error
143-
: vscode.DiagnosticSeverity.Warning
144-
)
145-
);
146-
}
147-
}
148-
}
149-
});
150-
}
21+
checkPlugins(pluginsNode, diagnostics, documentNode);
15122

15223
collection.set(document.uri, diagnostics);
15324
};
15425

155-
export const updateDiagnostics = (
26+
export const updateFileDiagnostics = (
15627
context: vscode.ExtensionContext,
15728
document: vscode.TextDocument,
15829
collection: vscode.DiagnosticCollection,
@@ -163,15 +34,14 @@ export const updateDiagnostics = (
16334
}
16435

16536
const diagnostics: vscode.Diagnostic[] = [];
166-
const documentNode = parse(document.getText()) as parse.ObjectNode;
37+
const documentNode = getObjectNodeFromDocument(document);
16738

168-
// check if schema version is compatible
16939
checkSchemaCompatibility(documentNode, devProxyInstall, diagnostics);
17040

17141
collection.set(document.uri, diagnostics);
17242
};
17343

174-
export const checkSchemaCompatibility = (documentNode: parse.ObjectNode, devProxyInstall: DevProxyInstall, diagnostics: vscode.Diagnostic[]) => {
44+
const checkSchemaCompatibility = (documentNode: parse.ObjectNode, devProxyInstall: DevProxyInstall, diagnostics: vscode.Diagnostic[]) => {
17545
const schemaNode = getASTNode(documentNode.children, 'Identifier', '$schema');
17646
if (schemaNode) {
17747
const schemaValue = (schemaNode.value as parse.LiteralNode).value as string;
@@ -187,3 +57,182 @@ export const checkSchemaCompatibility = (documentNode: parse.ObjectNode, devProx
18757
}
18858
}
18959
};
60+
61+
const checkPlugins = (pluginsNode: parse.PropertyNode | undefined, diagnostics: vscode.Diagnostic[], documentNode: parse.ObjectNode) => {
62+
if (pluginsNode &&
63+
(pluginsNode.value as parse.ArrayNode)) {
64+
const pluginNodes = (pluginsNode.value as parse.ArrayNode)
65+
.children as parse.ObjectNode[];
66+
67+
checkAtLeastOneEnabledPlugin(pluginNodes, diagnostics, pluginsNode);
68+
warnOnReporterPosition(pluginNodes, diagnostics);
69+
validatePluginConfigurations(pluginNodes, diagnostics, documentNode);
70+
}
71+
};
72+
73+
const validatePluginConfigurations = (pluginNodes: parse.ObjectNode[], diagnostics: vscode.Diagnostic[], documentNode: parse.ObjectNode) => {
74+
pluginNodes.forEach((pluginNode: parse.ObjectNode) => {
75+
const pluginNameNode = getASTNode(
76+
pluginNode.children,
77+
'Identifier',
78+
'name'
79+
);
80+
const pluginName = (pluginNameNode?.value as parse.LiteralNode)
81+
.value as string;
82+
const enabledNode = getASTNode(
83+
pluginNode.children,
84+
'Identifier',
85+
'enabled'
86+
);
87+
const isEnabled = (enabledNode?.value as parse.LiteralNode)
88+
.value as boolean;
89+
const pluginSnippet = pluginSnippets[pluginName];
90+
91+
checkPluginConfiguration(pluginNode, diagnostics, pluginName, isEnabled, documentNode, pluginSnippet);
92+
});
93+
};
94+
95+
const warnOnReporterPosition = (pluginNodes: parse.ObjectNode[], diagnostics: vscode.Diagnostic[]) => {
96+
const reporterIndex = pluginNodes.findIndex((pluginNode: parse.ObjectNode) => {
97+
const pluginNameNode = getASTNode(
98+
pluginNode.children,
99+
'Identifier',
100+
'name'
101+
);
102+
const pluginName = (pluginNameNode?.value as parse.LiteralNode)
103+
.value as string;
104+
return pluginName.toLowerCase().includes('reporter');
105+
});
106+
107+
if (reporterIndex !== -1) {
108+
// check if we have any more plugins after the reporter plugin
109+
const pluginsAfterReporter = pluginNodes.slice(reporterIndex + 1);
110+
// if we do, add a warning to the reporter plugin stating that it should be the last plugin
111+
if (pluginsAfterReporter.length > 0) {
112+
// check if there are any plugins after the reporter plugin that are not reporters
113+
const pluginAfterReporter = pluginsAfterReporter.find((pluginNode: parse.ObjectNode) => {
114+
const pluginNameNode = getASTNode(
115+
pluginNode.children,
116+
'Identifier',
117+
'name'
118+
);
119+
const pluginName = (pluginNameNode?.value as parse.LiteralNode)
120+
.value as string;
121+
return !pluginName.toLowerCase().includes('reporter');
122+
});
123+
// if there are, add a warning to the reporter plugin
124+
if (pluginAfterReporter) {
125+
const diagnostic = new vscode.Diagnostic(
126+
getRangeFromASTNode(pluginNodes[reporterIndex]),
127+
'Reporters should be placed after other plugins.',
128+
vscode.DiagnosticSeverity.Warning
129+
);
130+
diagnostics.push(diagnostic);
131+
}
132+
}
133+
}
134+
};
135+
136+
const checkAtLeastOneEnabledPlugin = (pluginNodes: parse.ObjectNode[], diagnostics: vscode.Diagnostic[], pluginsNode: parse.PropertyNode) => {
137+
// check if there are any plugins
138+
if (pluginNodes.length === 0) {
139+
diagnostics.push(
140+
new vscode.Diagnostic(
141+
getRangeFromASTNode(pluginsNode),
142+
'Add at least one plugin',
143+
vscode.DiagnosticSeverity.Error
144+
)
145+
);
146+
} else {
147+
// check if there are any enabled plugins
148+
const enabledPlugins = pluginNodes.filter((pluginNode: parse.ObjectNode) => {
149+
const enabledNode = getASTNode(
150+
pluginNode.children,
151+
'Identifier',
152+
'enabled'
153+
);
154+
return (enabledNode?.value as parse.LiteralNode).value as boolean;
155+
});
156+
if (enabledPlugins.length === 0) {
157+
diagnostics.push(
158+
new vscode.Diagnostic(
159+
getRangeFromASTNode(pluginsNode),
160+
'At least one plugin must be enabled',
161+
vscode.DiagnosticSeverity.Error
162+
)
163+
);
164+
}
165+
}
166+
};
167+
168+
const checkPluginConfiguration = (pluginNode: parse.ObjectNode, diagnostics: vscode.Diagnostic[], pluginName: string, isEnabled: boolean, documentNode: parse.ObjectNode, pluginSnippet: { instance: string; config?: PluginConfig; }) => {
169+
const configSectionNode = getASTNode(
170+
pluginNode.children,
171+
'Identifier',
172+
'configSection'
173+
);
174+
175+
// if the plugin does not require a config section, we should not have one
176+
if (!pluginSnippet.config && configSectionNode) {
177+
diagnostics.push(
178+
new vscode.Diagnostic(
179+
getRangeFromASTNode(configSectionNode),
180+
`${pluginName} does not require a config section.`,
181+
isEnabled
182+
? vscode.DiagnosticSeverity.Error
183+
: vscode.DiagnosticSeverity.Warning
184+
)
185+
);
186+
return;
187+
}
188+
189+
// if there is no config section defined on the plugin, we should have one if the plugin requires it
190+
if (!configSectionNode) {
191+
if (pluginSnippet.config?.required) {
192+
diagnostics.push(
193+
new vscode.Diagnostic(
194+
getRangeFromASTNode(pluginNode),
195+
`${pluginName} requires a config section.`,
196+
isEnabled
197+
? vscode.DiagnosticSeverity.Error
198+
: vscode.DiagnosticSeverity.Warning
199+
)
200+
);
201+
}
202+
} else {
203+
// if there is a config section defined on the plugin, we should have the config section defined in the document
204+
const configSectionName = (
205+
configSectionNode.value as parse.LiteralNode
206+
).value as string;
207+
const configSection = getASTNode(
208+
documentNode.children,
209+
'Identifier',
210+
configSectionName
211+
);
212+
213+
if (!configSection) {
214+
diagnostics.push(
215+
new vscode.Diagnostic(
216+
getRangeFromASTNode(configSectionNode.value),
217+
`${configSectionName} config section is missing. Use '${pluginSnippet.config?.name}' snippet to create one.`,
218+
isEnabled
219+
? vscode.DiagnosticSeverity.Error
220+
: vscode.DiagnosticSeverity.Warning
221+
)
222+
);
223+
}
224+
}
225+
};
226+
227+
const getPluginsNode = (documentNode: parse.ObjectNode) => {
228+
return getASTNode(
229+
documentNode.children,
230+
'Identifier',
231+
'plugins'
232+
);
233+
};
234+
235+
const getObjectNodeFromDocument = (document: vscode.TextDocument): parse.ObjectNode => {
236+
return parse(document.getText()) as parse.ObjectNode;
237+
};
238+

0 commit comments

Comments
 (0)