Skip to content

Commit

Permalink
Semantic hover provider + cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
yuval-po committed Feb 10, 2025
1 parent 7c850b9 commit 1c43d4c
Show file tree
Hide file tree
Showing 10 changed files with 234 additions and 43 deletions.
5 changes: 5 additions & 0 deletions src/annotation/annotation.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ export class AnnotationProvider {
return this._attributes.map((attr) => attr.name);
}

public getSecurities(): Attribute[] {
const secAttrNames: string[] = [AttributeNames.Security, AttributeNames.AdvancedSecurity];
return this._attributes.filter((attr) => secAttrNames.includes(attr.name)) ?? [];
}

// Public method to get non-attribute comments (indexed by line number)
public getNonAttributeComments(): NonAttributeComment[] {
return this._nonAttributeComments;
Expand Down
File renamed without changes.
32 changes: 24 additions & 8 deletions src/configuration/config.manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
ConfigurationChangeEvent,
FileSystemWatcher,
Uri,
window,
workspace,
WorkspaceConfiguration
} from 'vscode';
Expand All @@ -15,6 +14,17 @@ import { logger } from '../logging/logger';
import { gleeceContext } from '../context/context';
import { ITypedEvent, TypedEvent } from 'weak-event';

export interface ConfigValueChangedEvent<TKey extends Paths<GleeceExtensionConfig>> {
previousValue?: PathValue<GleeceExtensionConfig, TKey>;
newValue?: PathValue<GleeceExtensionConfig, TKey>;
}

type ConfigValueChangedEventHandler<
TKey extends Paths<GleeceExtensionConfig>
> = (
e: ConfigValueChangedEvent<TKey>
) => any;

export class ConfigManager {
private _extensionConfig?: WorkspaceConfiguration;

Expand All @@ -23,7 +33,7 @@ export class ConfigManager {

private _securitySchemaNames?: string[];

private _registeredConfigHandlers: Map<Paths<GleeceExtensionConfig>, ((value?: any) => any)[]> = new Map();
private _registeredConfigHandlers: Map<Paths<GleeceExtensionConfig>, ConfigValueChangedEventHandler<any>[]> = new Map();
private _extensionConfigChanged: TypedEvent<ConfigManager, ConfigurationChangeEvent> = new TypedEvent();

public get gleeceConfig(): GleeceConfig | undefined {
Expand Down Expand Up @@ -60,15 +70,15 @@ export class ConfigManager {

public registerConfigListener<TKey extends Paths<GleeceExtensionConfig>>(
configKey: TKey,
handler: (newValue?: PathValue<GleeceExtensionConfig, TKey>) => any
handler: ConfigValueChangedEventHandler<TKey>
): void {
const existingHandlers = this._registeredConfigHandlers.get(configKey) ?? [];
this._registeredConfigHandlers.set(configKey, existingHandlers.concat(handler));
}

public unregisterConfigListener<TKey extends Paths<GleeceExtensionConfig>>(
configKey: TKey,
handler: (newValue?: PathValue<GleeceExtensionConfig, TKey>) => any
handler: ConfigValueChangedEventHandler<TKey>
): void {
const existingHandlers = this._registeredConfigHandlers.get(configKey) ?? [];
const idx = existingHandlers.findIndex((existingHandler) => existingHandler === handler);
Expand All @@ -80,8 +90,12 @@ export class ConfigManager {

private async onExtensionConfigChanged(event: ConfigurationChangeEvent): Promise<void> {
// First, if the change affects our root, re-fetch the entire configuration
const oldConfig = this._extensionConfig;
if (event.affectsConfiguration('gleece')) {
this._extensionConfig = workspace.getConfiguration(ExtensionRootNamespace);
} else {
// No point in continuing if our namespace hasn't changed
return;
}

// Then, if it affects the gleece.config path, we need to re-load the file and re-initialize the watcher
Expand All @@ -98,10 +112,12 @@ export class ConfigManager {
// Finally, we notify the specific event listeners and provide the freshly obtained config value
const dispatchPromises: Promise<any>[] = [];
for (const key of this._registeredConfigHandlers.keys()) {
if (event.affectsConfiguration(`gleece.${key}`)) {
const fullConfigKey = `gleece.${key}`;
if (event.affectsConfiguration(fullConfigKey)) {
const previousValue = oldConfig ? oldConfig.get(key) : undefined;
const newValue = this.getExtensionConfigValue(key);
for (const handler of this._registeredConfigHandlers.get(key) ?? []) {
dispatchPromises.push(handler(newValue));
dispatchPromises.push(handler({ previousValue, newValue }));
}
}
}
Expand All @@ -116,14 +132,14 @@ export class ConfigManager {
const configPath = this.getExtensionConfigValue('config.path') ?? 'gleece.config.json';
const { error, data } = await this.loadFile(configPath);
if (error) {
window.showErrorMessage(`Failed to load configuration file: ${(error as any)?.message}`);
logger.errorPopup(`Failed to load configuration file: ${(error as any)?.message}`);
return;
}
try {
this._gleeceConfig = JSON.parse(data);
this._securitySchemaNames = undefined;
} catch (error) {
window.showErrorMessage(`Could not parse Gleece configuration file at '${configPath}' - ${(error as any)?.message}`);
logger.errorPopup(`Could not parse Gleece configuration file at '${configPath}' - ${(error as any)?.message}`);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/configuration/gleece.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export interface OpenAPIGeneratorConfig {
info: Info; // Required
baseUrl: string; // Required, must be a valid URL
securitySchemes: SecuritySchemeConfig[]; // Required, non-empty array
defaultSecurity: RouteSecurity[]; // Required, non-empty array
defaultSecurity: SecurityAnnotationComponent; // Required, non-empty array
specGeneratorConfig: SpecGeneratorConfig; // Required
}

Expand Down
46 changes: 32 additions & 14 deletions src/context/context.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import { Disposable, ExtensionContext, HoverProvider } from 'vscode';
import { Disposable, ExtensionContext, HoverProvider, languages } from 'vscode';
import { GleeceCodeActionProvider } from '../code-actions/code.action.provider';
import { SimpleCompletionProvider } from '../completion/gleece.simple.completion.provider';
import { GleeceDiagnosticsListener } from '../diagnostics/listener';
import { SemanticHoverProvider } from '../hover/semantic.hover.provider';
import { ResourceManager } from '../resource.manager';
import { ConfigManager } from '../configuration/config.manager';
import { ConfigManager, ConfigValueChangedEvent } from '../configuration/config.manager';
import { logger } from '../logging/logger';
import { SimpleHoverProvider } from '../hover/simple.hover.provider';
import { GoLangId } from '../common.constants';

class GleeceContext implements Disposable {
private _resourceManager!: ResourceManager;
private _configManager!: ConfigManager;

private _completionAndHoverProvider!: SimpleCompletionProvider;
private _codeActionsProvider!: GleeceCodeActionProvider;
private _semanticHoverProvider!: SemanticHoverProvider;
private _diagnosticsListener!: GleeceDiagnosticsListener;

private _enableSymbolicAwareness: boolean = true;
private _hoverProviderRegistration?: Disposable;
private _hoverProvider!: HoverProvider;

public get resourceManager(): ResourceManager {
return this._resourceManager;
Expand All @@ -30,10 +32,6 @@ class GleeceContext implements Disposable {
return this._codeActionsProvider;
}

public get hoverProvider(): HoverProvider {
return this._semanticHoverProvider;
}

public get diagnosticsListener(): GleeceDiagnosticsListener {
return this._diagnosticsListener;
}
Expand All @@ -49,16 +47,16 @@ class GleeceContext implements Disposable {
await this._configManager.init();

this._completionAndHoverProvider = new SimpleCompletionProvider();
this._semanticHoverProvider = new SemanticHoverProvider();
this._codeActionsProvider = new GleeceCodeActionProvider();
this._diagnosticsListener = new GleeceDiagnosticsListener();

// The logger is registered here so it's collected upon deactivation.
// The reasoning is that delegating disposal to the logger itself creates a cyclic dependency.
this._resourceManager.registerDisposable(logger);

this._configManager.registerConfigListener('analysis.enableSymbolicAwareness', this.setEnableSymbolicAwareness.bind(this));
this._enableSymbolicAwareness = this._configManager.getExtensionConfigValue('analysis.enableSymbolicAwareness') ?? false;
this._configManager.registerConfigListener('analysis.enableSymbolicAwareness', this.onChangeSemanticAnalysis.bind(this));
const useSemanticAnalysis = this._configManager.getExtensionConfigValue('analysis.enableSymbolicAwareness') ?? false;
this.onChangeSemanticAnalysis({ previousValue: undefined, newValue: useSemanticAnalysis });
}

public registerDisposable(disposable: Disposable): void {
Expand All @@ -74,11 +72,31 @@ class GleeceContext implements Disposable {
}

public dispose(): void {
this._configManager.unregisterConfigListener('analysis.enableSymbolicAwareness', this.setEnableSymbolicAwareness.bind(this));
this._configManager.unregisterConfigListener('analysis.enableSymbolicAwareness', this.onChangeSemanticAnalysis.bind(this));
}

private setEnableSymbolicAwareness(value?: boolean): void {
this._enableSymbolicAwareness = value ?? false;
private onChangeSemanticAnalysis(event: ConfigValueChangedEvent<'analysis.enableSymbolicAwareness'>): void {
if (event.previousValue === event.newValue) {
// No change. Noop.
// Config manager should not call this if value hasn't changed so this check is more to
// protect against weird calls coming from within the instance itself
return;
}

if (this._hoverProviderRegistration) {
// Manually dispose the provider here then unregister it.
// Similar to C# dispose pattern with finalizer
this._hoverProviderRegistration.dispose();
this._resourceManager.unRegisterDisposable(this._hoverProviderRegistration);
}

this._hoverProvider = event.newValue === true ? new SemanticHoverProvider() : new SimpleHoverProvider();
this._hoverProviderRegistration = languages.registerHoverProvider(
{ scheme: 'file', language: GoLangId },
this._hoverProvider
);

this.resourceManager.registerDisposable(this._hoverProviderRegistration);
}
}

Expand Down
7 changes: 2 additions & 5 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,12 @@ export async function activate(context: ExtensionContext) {
await gleeceContext.init(context);
logger.debug('Gleece Extension activating...');
context.subscriptions.push(
// Moving these to the context
languages.registerCompletionItemProvider(
GoLangId,
gleeceContext.completionAndHoverProvider,
'@'
),
languages.registerHoverProvider(
{ scheme: 'file', language: GoLangId },
gleeceContext.hoverProvider
),
languages.registerCodeActionsProvider(
{ scheme: 'file', language: GoLangId },
gleeceContext.codeActionsProvider,
Expand All @@ -39,7 +36,7 @@ export async function activate(context: ExtensionContext) {
gleeceContext.diagnosticsListener.fullDiagnostics(window.activeTextEditor.document)
.catch((err) => logger.error('Could not re-analyze file', err));
} else {
window.showWarningMessage('Cannot re-analyze - no file is open');
logger.warnPopup('Cannot re-analyze - no file is open');
}
})
);
Expand Down
108 changes: 93 additions & 15 deletions src/hover/semantic.hover.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ import {
HoverProvider,
MarkdownString
} from 'vscode';
import { AttributeNames, AttributeNamesCompletionObjects, RepeatableAttributes } from '../enums';
import { getAnnotationProvider, getProvidersForSymbols } from '../annotation/annotation.functional';
import { AttributeNames, AttributeNamesCompletionObjects, KnownJsonProperties, RepeatableAttributes } from '../enums';
import { getAnnotationProvider, getProviderForSymbol, getProvidersForSymbols } from '../annotation/annotation.functional';
import { semanticProvider } from '../semantics/semantics.provider';
import { GolangSymbolicAnalyzer } from '../symbolic-analysis/symbolic.analyzer';
import { GolangStruct } from '../symbolic-analysis/gonlang.struct';
import { GolangSymbolType } from '../symbolic-analysis/golang.common';
import { GolangSymbol, GolangSymbolType } from '../symbolic-analysis/golang.common';
import { GolangReceiver } from '../symbolic-analysis/golang.receiver';
import { createMarkdownTable } from './markdown.factory';
import { createMarkdownTable } from '../common/markdown.factory';
import { gleeceContext } from '../context/context';
import { AnnotationProvider, Attribute } from '../annotation/annotation.provider';

export class SemanticHoverProvider implements HoverProvider {

Expand Down Expand Up @@ -66,31 +68,31 @@ export class SemanticHoverProvider implements HoverProvider {
}

private async onCommentHover(document: TextDocument, position: Position): Promise<Hover | undefined> {
const provider = getAnnotationProvider(document, position);
const analyzer = await semanticProvider.getAnalyzerForDocument(document, true);
const annotatedSymbol = analyzer.findOneImmediatelyAfter(provider.range);
if (annotatedSymbol) {
//
}

return undefined;
const annotations = getAnnotationProvider(document, position);
const symbol = analyzer.findOneImmediatelyAfter(annotations.range);
return this.getHoverForSymbol(document, analyzer, symbol);
}

private async onUnknownHover(document: TextDocument, position: Position): Promise<Hover | undefined> {
const analyzer = await semanticProvider.getAnalyzerForDocument(document, true);
const symbol = analyzer.getSymbolAtPosition(position);
return this.getHoverForSymbol(document, analyzer, symbol);
}

private getHoverForSymbol(document: TextDocument, analyzer: GolangSymbolicAnalyzer, symbol?: GolangSymbol): Hover | undefined {
switch (symbol?.type) {
case GolangSymbolType.Struct:
if ((symbol as GolangStruct).isController) {
return this.onControllerHover(document, analyzer, symbol as GolangStruct);
}
return undefined;
case GolangSymbolType.Receiver:
break;
case GolangSymbolType.Receiver:
return this.onReceiverHover(document, analyzer, (symbol as GolangReceiver));
default:
break;
}

return undefined;
}

Expand All @@ -104,9 +106,7 @@ export class SemanticHoverProvider implements HoverProvider {
&& (symbol as GolangReceiver).ownerStructName === struct.symbol.name
) as GolangReceiver[];


const receiverHolders = getProvidersForSymbols(document, receivers);

return new Hover(
createMarkdownTable(
[
Expand All @@ -130,4 +130,82 @@ export class SemanticHoverProvider implements HoverProvider {
)
);
}

private onReceiverHover(
document: TextDocument,
analyzer: GolangSymbolicAnalyzer,
receiver: GolangReceiver
): Hover | undefined {
const annotations = getProviderForSymbol(document, receiver);
if (!annotations) {
// Func has no comments. Ignore
return undefined;
}

const parentController = analyzer.getStructByName(receiver.ownerStructName);
const markdown = new MarkdownString(`**${receiver.name}** (*${parentController?.symbol.name ?? 'N/A'}*)\n\n`);

const method = annotations.getAttribute(AttributeNames.Method)?.value ?? 'N/A';
const route = annotations.getAttribute(AttributeNames.Route)?.value ?? 'N/A';

markdown.appendMarkdown(`*${method}* \`${route}\`\n`);
this.appendReceiverSecurityMarkdown(document, analyzer, receiver, annotations, markdown);

return new Hover(markdown);
}

private appendReceiverSecurityMarkdown(
document: TextDocument,
analyzer: GolangSymbolicAnalyzer,
receiver: GolangReceiver,
annotationsForSymbol: AnnotationProvider,
markdown: MarkdownString
): void {
const methodSecurities = annotationsForSymbol.getSecurities();
if (methodSecurities?.length > 0) {
markdown.appendMarkdown('- Security - Explicit\n');
for (const secAttr of methodSecurities) {
markdown.appendMarkdown(` - *${secAttr.value ?? 'N/A'}* : ${this.getSecurityScopesString(secAttr)}\n`);
}

return;
}

const parentController = analyzer.getStructByName(receiver.ownerStructName);
if (!parentController || !parentController.isController) {
// Shouldn't happen but just in case.
return;
}

const controllerAnnotations = getProviderForSymbol(document, parentController);
if (controllerAnnotations) {
const controllerSecurities = controllerAnnotations.getSecurities();
if (controllerSecurities.length > 0) {
markdown.appendMarkdown('- Security - Inherited\n');
for (const secAttr of controllerSecurities) {
markdown.appendMarkdown(` - *${secAttr.value ?? 'N/A'}* : ${this.getSecurityScopesString(secAttr)}\n`);
}

return;
}
}

const defaultSecurity = gleeceContext.configManager.gleeceConfig?.openAPIGeneratorConfig.defaultSecurity;
if (defaultSecurity) {
markdown.appendMarkdown('- Security - Default\n');
markdown.appendMarkdown(` - *${defaultSecurity.name}* : ${defaultSecurity.scopes.map((s) => `*${s}*`).join(', ')}\n`);
return;
}

markdown.appendMarkdown('- Security - **None**');
return;
}

private getSecurityScopesString(attribute: Attribute): string {
const scopes = attribute.properties?.[KnownJsonProperties.SecurityScopes];
if (scopes && Array.isArray(scopes)) {
return scopes.map((s) => `*${s}*`).join(', ');
}
return 'N/A';
}
}
Loading

0 comments on commit 1c43d4c

Please sign in to comment.