Skip to content

Commit

Permalink
Add class import code actions.
Browse files Browse the repository at this point in the history
restrict function and class imports to .bs
  • Loading branch information
TwitchBronBron committed Mar 13, 2021
1 parent 471884d commit f1f9392
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 55 deletions.
5 changes: 4 additions & 1 deletion src/DiagnosticMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,10 @@ export let DiagnosticMessages = {
classCouldNotBeFound: (className: string, scopeName: string) => ({
message: `Class '${className}' could not be found when this file is included in scope '${scopeName}'`,
code: 1029,
severity: DiagnosticSeverity.Error
severity: DiagnosticSeverity.Error,
data: {
className: className
}
}),
expectedClassFieldIdentifier: () => ({
message: `Expected identifier in class body`,
Expand Down
19 changes: 19 additions & 0 deletions src/Program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1199,6 +1199,25 @@ export class Program {
return files;
}

/**
* Find a list of files in the program that have a function with the given name (case INsensitive)
*/
public findFilesForClass(className: string) {
const files = [] as BscFile[];
const lowerClassName = className.toLowerCase();
//find every file with this class defined
for (const file of Object.values(this.files)) {
if (isBrsFile(file)) {
//TODO handle namespace-relative classes
//if the file has a function with this name
if (file.parser.references.classStatementLookup.get(lowerClassName) !== undefined) {
files.push(file);
}
}
}
return files;
}

/**
* Get a map of the manifest information
*/
Expand Down
189 changes: 144 additions & 45 deletions src/bscPlugin/codeActions/CodeActionsProcessor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,51 +81,150 @@ describe('CodeActionsProcessor', () => {
util.createRange(1, 51, 1, 51)
);
});
});

it('does not produce duplicate code actions for bs imports', () => {
//define the function in two files
program.addOrReplaceFile('components/lib1.brs', `
sub doSomething()
end sub
`);
program.addOrReplaceFile('components/lib2.brs', `
sub doSomething()
end sub
`);

//use the function in this file
const componentCommonFile = program.addOrReplaceFile('components/ComponentCommon.bs', `
sub init()
doSomething()
end sub
`);

//import the file in two scopes
program.addOrReplaceFile('components/comp1.xml', trim`
<?xml version="1.0" encoding="utf-8" ?>
<component name="ChildScene">
<script uri="ComponentCommon.bs" />
</component>
`);
program.addOrReplaceFile('components/comp2.xml', trim`
<?xml version="1.0" encoding="utf-8" ?>
<component name="ChildScene">
<script uri="ComponentCommon.bs" />
</component>
`);

program.validate();

//we should only get each file import suggestion exactly once
const codeActions = program.getCodeActions(
componentCommonFile.pathAbsolute,
// doSome|thing()
util.createRange(2, 22, 2, 22)
);
expect(codeActions.map(x => x.title).sort()).to.eql([
`import "pkg:/components/lib1.brs"`,
`import "pkg:/components/lib2.brs"`
]);
it('does not produce duplicate code actions for bs imports', () => {
//define the function in two files
program.addOrReplaceFile('components/lib1.brs', `
sub doSomething()
end sub
`);
program.addOrReplaceFile('components/lib2.brs', `
sub doSomething()
end sub
`);

//use the function in this file
const componentCommonFile = program.addOrReplaceFile('components/ComponentCommon.bs', `
sub init()
doSomething()
end sub
`);

//import the file in two scopes
program.addOrReplaceFile('components/comp1.xml', trim`
<?xml version="1.0" encoding="utf-8" ?>
<component name="ChildScene">
<script uri="ComponentCommon.bs" />
</component>
`);
program.addOrReplaceFile('components/comp2.xml', trim`
<?xml version="1.0" encoding="utf-8" ?>
<component name="ChildScene">
<script uri="ComponentCommon.bs" />
</component>
`);

program.validate();

//we should only get each file import suggestion exactly once
const codeActions = program.getCodeActions(
componentCommonFile.pathAbsolute,
// doSome|thing()
util.createRange(2, 22, 2, 22)
);
expect(codeActions.map(x => x.title).sort()).to.eql([
`import "pkg:/components/lib1.brs"`,
`import "pkg:/components/lib2.brs"`
]);
});

it('does not suggest imports for brs files', () => {
//import the file in two scopes
program.addOrReplaceFile('components/comp1.xml', trim`
<?xml version="1.0" encoding="utf-8" ?>
<component name="ChildScene">
<script uri="comp1.brs" />
</component>
`);
//import the function here
const file = program.addOrReplaceFile('components/comp1.brs', `
sub init()
DoSomething()
end sub
`);

//define the function here
program.addOrReplaceFile('source/lib.brs', `
sub DoSomething()
end sub
`);

program.validate();

//there should be no code actions since this is a brs file
const codeActions = program.getCodeActions(
file.pathAbsolute,
// DoSometh|ing()
util.createRange(2, 28, 2, 28)
);
expect(codeActions).to.be.empty;
});

it('suggests class imports', () => {
//import the file in two scopes
program.addOrReplaceFile('components/comp1.xml', trim`
<?xml version="1.0" encoding="utf-8" ?>
<component name="ChildScene">
<script uri="comp1.bs" />
</component>
`);
const file = program.addOrReplaceFile('components/comp1.bs', `
sub init()
dude = new Person()
end sub
`);

program.addOrReplaceFile('source/Person.bs', `
class Person
end class
`);

program.validate();

expect(
program.getCodeActions(
file.pathAbsolute,
// new Per|son()
util.createRange(2, 34, 2, 34)
).map(x => x.title).sort()
).to.eql([
`import "pkg:/source/Person.bs"`
]);
});

it('suggests class imports', () => {
//import the file in two scopes
program.addOrReplaceFile('components/comp1.xml', trim`
<?xml version="1.0" encoding="utf-8" ?>
<component name="ChildScene">
<script uri="comp1.bs" />
</component>
`);
//import the function here
const file = program.addOrReplaceFile('components/comp1.bs', `
sub init()
kitty = new Animals.Cat()
end sub
`);
program.addOrReplaceFile('source/Animals.bs', `
namespace Animals
class Cat
end class
end namespace
`);

program.validate();

expect(
program.getCodeActions(
file.pathAbsolute,
// new Anim|als.Cat()
util.createRange(2, 36, 2, 36)
).map(x => x.title).sort()
).to.eql([
`import "pkg:/source/Animals.bs"`
]);
});
});

});
49 changes: 40 additions & 9 deletions src/bscPlugin/codeActions/CodeActionsProcessor.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { Diagnostic } from 'vscode-languageserver';
import { CodeActionKind } from 'vscode-languageserver';
import { codeActionUtil } from '../../CodeActionUtil';
import type { DiagnosticMessageType } from '../../DiagnosticMessages';
import { DiagnosticCodeMap } from '../../DiagnosticMessages';
import type { BrsFile } from '../../files/BrsFile';
import type { XmlFile } from '../../files/XmlFile';
import type { OnGetCodeActionsEvent } from '../../interfaces';
import type { BscFile, OnGetCodeActionsEvent } from '../../interfaces';
import { ParseMode } from '../../parser';
import { util } from '../../util';

export class CodeActionsProcessor {
Expand All @@ -17,7 +19,9 @@ export class CodeActionsProcessor {
public process() {
for (const diagnostic of this.event.diagnostics) {
if (diagnostic.code === DiagnosticCodeMap.callToUnknownFunction) {
this.suggestImports(diagnostic as any);
this.suggestFunctionImports(diagnostic as any);
} else if (diagnostic.code === DiagnosticCodeMap.classCouldNotBeFound) {
this.suggestClassImports(diagnostic as any);
} else if (diagnostic.code === DiagnosticCodeMap.xmlComponentMissingExtendsAttribute) {
this.addMissingExtends(diagnostic as any);
}
Expand All @@ -26,21 +30,22 @@ export class CodeActionsProcessor {

private suggestedImports = new Set<string>();

private suggestImports(diagnostic: DiagnosticMessageType<'callToUnknownFunction'>) {
const lowerFunctionName = diagnostic.data.functionName.toLowerCase();

//skip generating duplicate suggestions if we've already done this one
if (this.suggestedImports.has(lowerFunctionName)) {
/**
* Generic import suggestion function. Shouldn't be called directly from the main loop, but instead called by more specific diagnostic handlers
*/
private suggestImports(diagnostic: Diagnostic, key: string, files: BscFile[]) {
//skip if we already have this suggestion
if (this.suggestedImports.has(key)) {
return;
}

this.suggestedImports.add(lowerFunctionName);
this.suggestedImports.add(key);
const importStatements = (this.event.file as BrsFile).parser.references.importStatements;
//find the position of the first import statement, or the top of the file if there is none
const insertPosition = importStatements[importStatements.length - 1]?.importToken.range?.start ?? util.createPosition(0, 0);

//find all files that reference this function
for (const file of this.event.file.program.findFilesForFunction(lowerFunctionName)) {
for (const file of files) {
const pkgPath = util.getRokuPkgPath(file.pkgPath);
this.event.codeActions.push(
codeActionUtil.createCodeAction({
Expand All @@ -59,6 +64,32 @@ export class CodeActionsProcessor {
}
}

private suggestFunctionImports(diagnostic: DiagnosticMessageType<'callToUnknownFunction'>) {
//skip if not a BrighterScript file
if ((diagnostic.file as BrsFile).parseMode !== ParseMode.BrighterScript) {
return;
}
const lowerFunctionName = diagnostic.data.functionName.toLowerCase();
this.suggestImports(
diagnostic,
lowerFunctionName,
this.event.file.program.findFilesForFunction(lowerFunctionName)
);
}

private suggestClassImports(diagnostic: DiagnosticMessageType<'classCouldNotBeFound'>) {
//skip if not a BrighterScript file
if ((diagnostic.file as BrsFile).parseMode !== ParseMode.BrighterScript) {
return;
}
const lowerClassName = diagnostic.data.className.toLowerCase();
this.suggestImports(
diagnostic,
lowerClassName,
this.event.file.program.findFilesForClass(lowerClassName)
);
}

private addMissingExtends(diagnostic: DiagnosticMessageType<'xmlComponentMissingExtendsAttribute'>) {
const srcPath = this.event.file.pathAbsolute;
const { component } = (this.event.file as XmlFile).parser.ast;
Expand Down
12 changes: 12 additions & 0 deletions src/parser/Parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2637,6 +2637,18 @@ export interface ParseOptions {
export class References {
public assignmentStatements = [] as AssignmentStatement[];
public classStatements = [] as ClassStatement[];

public get classStatementLookup() {
if (!this._classStatementLookup) {
this._classStatementLookup = new Map();
for (const stmt of this.classStatements) {
this._classStatementLookup.set(stmt.getName(ParseMode.BrighterScript).toLowerCase(), stmt);
}
}
return this._classStatementLookup;
}
private _classStatementLookup: Map<string, ClassStatement>;

public functionExpressions = [] as FunctionExpression[];
public functionStatements = [] as FunctionStatement[];
/**
Expand Down

0 comments on commit f1f9392

Please sign in to comment.