Skip to content

Commit a2c4f56

Browse files
committed
Support for tracking attributes
1 parent 6fdc932 commit a2c4f56

File tree

3 files changed

+130
-6
lines changed

3 files changed

+130
-6
lines changed

server/src/capabilities/capabilities.ts

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,10 @@ export enum ScopeType {
185185
VARIABLE,
186186
/** A variable declaration in a signature */
187187
PARAMETER,
188-
/** Any reference type that isn't a declaration. */
189-
REFERENCE
188+
/** A reference to some type of declaration. */
189+
REFERENCE,
190+
/** A special reference type. */
191+
ATTRIBUTE
190192
}
191193

192194
export enum AssignmentType {
@@ -211,6 +213,7 @@ export class ScopeItemCapability {
211213
};
212214
parameters?: Map<string, ScopeItemCapability[]>;
213215
references?: Map<string, ScopeItemCapability[]>;
216+
attributes?: Map<string, ScopeItemCapability[]>;
214217

215218
// Special scope references for easier resolution of names.
216219
implicitDeclarations?: Map<string, ScopeItemCapability[]>;
@@ -288,8 +291,10 @@ export class ScopeItemCapability {
288291
* Recursively build from this node down.
289292
*/
290293
build(): void {
291-
if (this.type === ScopeType.REFERENCE) {
292-
// Link to declaration if it exists.
294+
if (this.type === ScopeType.ATTRIBUTE) {
295+
this.resolveAttribute();
296+
this.validateAttributes();
297+
} else if (this.type === ScopeType.REFERENCE) {
293298
this.resolveLinks();
294299
this.validateLink();
295300
} else {
@@ -308,6 +313,7 @@ export class ScopeItemCapability {
308313
this.properties?.letters?.forEach(items => items.forEach(item => item.build()));
309314
this.properties?.setters?.forEach(items => items.forEach(item => item.build()));
310315
this.references?.forEach(items => items.forEach(item => item.build()));
316+
this.attributes?.forEach(items => items.forEach(item => item.build()));
311317

312318
this.isDirty = false;
313319
}
@@ -460,6 +466,88 @@ export class ScopeItemCapability {
460466
}
461467
}
462468

469+
private resolveAttribute() {
470+
/**
471+
* Most attributes will be belong to the parent. Variable attributes
472+
* will belong to the same scope as the item they refer to.
473+
*
474+
* We set one way links here to facilitate renaming.
475+
* Setting an attribute as a back link will impact unused diagnostics.
476+
*/
477+
478+
if (!this.parent) {
479+
Services.logger.error(`Expected parent for attribute ${this.name}`);
480+
throw new Error("Attribute scope item has no parent.");
481+
}
482+
483+
// The immediate parent is probably the linked item.
484+
if (this.name === this.parent.name) {
485+
this.link = this.parent;
486+
return;
487+
}
488+
489+
// If not, we may be a variable attribute (shared parent).
490+
const declarations = this.parent.properties?.getters?.get(this.name);
491+
if (!declarations) {
492+
return;
493+
}
494+
495+
// Handle a single declaration found.
496+
if (declarations.length === 1) {
497+
this.link = declarations[0];
498+
this.parent.moveAttribute(this, declarations[0]);
499+
return;
500+
}
501+
502+
// Handle duplicate declarations by attaching to the closest above.
503+
const targetRow = this.range?.start.line ?? 0;
504+
let closestDeclaration: ScopeItemCapability | undefined;
505+
for (const declaration of declarations) {
506+
const declarationRow = declaration?.range?.start.line ?? 0;
507+
if (declarationRow === 0 || declarationRow >= targetRow) {
508+
return;
509+
}
510+
511+
if (!closestDeclaration) {
512+
closestDeclaration = declaration;
513+
return;
514+
}
515+
516+
const closestRow = closestDeclaration.range?.start.line ?? 0;
517+
if (targetRow > declarationRow && declarationRow > closestRow) {
518+
closestDeclaration = declaration;
519+
}
520+
}
521+
522+
if (closestDeclaration) {
523+
this.link = closestDeclaration;
524+
this.parent.moveAttribute(this, closestDeclaration);
525+
}
526+
}
527+
528+
moveAttribute(attr: ScopeItemCapability, destination: ScopeItemCapability) {
529+
const items = this.attributes?.get(attr.name);
530+
if (!items || items.length === 0) {
531+
return;
532+
}
533+
534+
const unmoved: ScopeItemCapability[] = [];
535+
items.forEach(item => {
536+
const isLocMatch = item.locationUri === attr.locationUri;
537+
const isRangeMatch = rangeEquals(item.element?.context.range, attr.range);
538+
if (isLocMatch && isRangeMatch) {
539+
destination.attributes ??= new Map();
540+
destination.addItem(destination.attributes, item);
541+
} else {
542+
unmoved.push(item);
543+
}
544+
});
545+
}
546+
547+
private validateAttributes() {
548+
// Attributes must be in specific locations to work.
549+
}
550+
463551
private resolveLinks() {
464552

465553
// Resolve where we have no member access names.
@@ -726,6 +814,13 @@ export class ScopeItemCapability {
726814
return this;
727815
}
728816

817+
// Register attributes
818+
if (item.type === ScopeType.ATTRIBUTE) {
819+
item.parent.attributes ??= new Map();
820+
item.parent.addItem(item.parent.attributes, item);
821+
return this;
822+
}
823+
729824
// Add implicitly accessible names to the project scope.
730825
if (item.isPublicScope && this.project && this !== this.project) {
731826
this.project.implicitDeclarations ??= new Map();
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Core
2+
import { TextDocument } from "vscode-languageserver-textdocument";
3+
4+
// Antlr
5+
import { AttributeStatementContext } from "../../antlr/out/vbaParser";
6+
7+
// Project
8+
import { BaseRuleSyntaxElement } from "./base";
9+
import { IdentifierCapability, ScopeItemCapability, ScopeType } from "../../capabilities/capabilities";
10+
11+
12+
export class AttributeElement extends BaseRuleSyntaxElement<AttributeStatementContext> {
13+
identifierCapability: IdentifierCapability;
14+
scopeItemCapability: ScopeItemCapability;
15+
16+
constructor(ctx: AttributeStatementContext, doc: TextDocument) {
17+
super(ctx, doc);
18+
19+
this.identifierCapability = new IdentifierCapability(this, () => ctx.ambiguousIdentifier());
20+
this.scopeItemCapability = new ScopeItemCapability(this, ScopeType.ATTRIBUTE);
21+
}
22+
}

server/src/project/parser/vbaListener.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
AmbiguousIdentifierContext,
1212
AnyOperatorContext,
1313
ArgumentListContext,
14+
AttributeStatementContext,
1415
CallStatementContext,
1516
ClassModuleContext,
1617
DictionaryAccessExpressionContext,
@@ -44,7 +45,7 @@ import {
4445
WithStatementContext
4546
} from '../../antlr/out/vbaParser';
4647
import {
47-
AttributeStatementContext,
48+
AttributeStatementContext as FmtAttributeStatementContext,
4849
BasicStatementContext,
4950
BlockContext,
5051
CaseDefaultStatementContext,
@@ -88,6 +89,7 @@ import {
8889
PropertyLetDeclarationElement,
8990
PropertySetDeclarationElement,
9091
} from '../elements/procedure';
92+
import { AttributeElement } from '../elements/attributes';
9193

9294

9395
enum ParserAssignmentState {
@@ -181,6 +183,11 @@ export class VbaListener extends vbaListener {
181183
this.document.registerElement(element);
182184
};
183185

186+
enterAttributeStatement = (ctx: AttributeStatementContext) => {
187+
const element = new AttributeElement(ctx, this.document.textDocument);
188+
this.document.registerElement(element);
189+
};
190+
184191
enterEnumDeclaration = (ctx: EnumDeclarationContext) => {
185192
const element = new EnumDeclarationElement(ctx, this.document.textDocument, this.isAfterMethodDeclaration);
186193
this.document.registerElement(element);
@@ -613,7 +620,7 @@ export class VbaFmtListener extends vbafmtListener {
613620
}
614621

615622
// Attributes are always zero indented.
616-
enterAttributeStatement = (ctx: AttributeStatementContext) => {
623+
enterAttributeStatement = (ctx: FmtAttributeStatementContext) => {
617624
const range = this.getCtxRange(ctx);
618625
const offset = ctx.endsWithLineEnding ? 0 : 1;
619626

0 commit comments

Comments
 (0)