Skip to content

Commit

Permalink
Find and prefix function names in observeField calls
Browse files Browse the repository at this point in the history
  • Loading branch information
TwitchBronBron committed Sep 30, 2020
1 parent 4485482 commit 297148b
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 9 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ When module authors publish their modules, they should not include any type of p
`ropm` will scan every module for:
- function declaractions
- function calls
- string function name in every object's `observeField` calls
- component declarations
- component usage:
- component names in XML `extends` attribute
Expand Down Expand Up @@ -393,3 +394,15 @@ Here's an example (**NOTE:** comments are included here for explanation purposes
## rootDir versus packageRootDir
- `rootDir` - specifies where ropm_modules should be installed in your project.
- `packageRootDir` is exclusively for package authors to specify where their package module code resides (like in `dist`, `out`, `build`, `src`, etc...).

## Handling observeField
`ropm` will auto-detect most common `observeField` function calls. In order to prevent naming conflicts, please do not use the name `observeField` for custom object functions.

Here are the requirements for having `ropm` prefix your `observeField` string function names.
1. Use a single string literal for the function name. For example, `m.top.observeField(fieldName, "callbackFunction")`
2. The `observeField` call must be on a single line. For example, this call would remain unprefixed:
```BrightScript
m.top.observeField(getFieldName({
componentName: "something"
}, "callbackFunction")
```
23 changes: 23 additions & 0 deletions src/prefixer/File.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export class File {
this.findComponentDefinitions();
this.findComponentReferences();
this.findFileReferences();
this.findObserveFieldFunctionNames();
}

private findComponentReferences() {
Expand Down Expand Up @@ -206,6 +207,28 @@ export class File {
}
}

/**
* Find all occurances of *.observeField function calls that have a string literal as the second parameter
*/
private findObserveFieldFunctionNames() {
//capture function names as a string literal in `observeField` functions.
const regexp = /(\.observeField[ \t]*\(.*?,[ \t]*")([a-z0-9_]+)"\)[ \t]*(?:'.*)*$/gim;

let match: RegExpExecArray | null;
while (match = regexp.exec(this.fileContents)) {
//skip multi-line observeField calls (because they are way too hard to parse with regex :D )
if (util.hasMatchingParenCount(match[0]) === false) {
continue;
}

//just add this to function calls, since there's no difference in terms of how they get replaced
this.functionCalls.push({
name: match[2],
offset: match.index + match[1].length
});
}
}

private findComponentDefinitions() {
const nameAttribute = this.xmlAst?.rootElement?.attributes?.find(x => x.key?.toLowerCase() === 'name');
if (nameAttribute?.value && nameAttribute?.syntax?.value) {
Expand Down
66 changes: 66 additions & 0 deletions src/prefixer/ModuleManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,72 @@ describe('ModuleManager', () => {
`);
});

describe('prefixing observeField', () => {
async function testObserveField(testLine: string, expectedLine = testLine) {
await createDependencies([{
name: 'logger',
_files: {
'source/lib.brs': trim`
sub init()
${testLine}
end sub
sub logInfo()
end sub
`
}
}]);
await process();
fsEqual(`${hostDir}/source/roku_modules/logger/lib.brs`, trim`
sub init()
${expectedLine}
end sub
sub logger_logInfo()
end sub
`);
}

it('does not prefix because not an object call', async () => {
//no change because it's not on an object
await testObserveField(`observeField("field", "logInfo")`);
});

it('does not prefix because not a string literal', async () => {
//no change because the second parameter is not a string
await testObserveField(`m.top.observeField("field", callbackName)`);
});

it('does not prefix because references unknown function name', async () => {
//no change because the second parameter is not a string
await testObserveField(`m.top.observeField("field", "unknownFunctionName")`);
});

it('does not prefix multi-line first param', async () => {
//no change because the second parameter is not a string
await testObserveField(`m.top.observeField(getField({
name: "something"
}, "unknownFunctionName")`);
});

it('does not prefix multi-line with function call at end of first line', async () => {
await testObserveField(`m.top.observeField({name: getName("bob")
age: 12
}, "logInfo")
`);
});

it('prefixes object call with string literal', async () => {
await testObserveField(`m.top.observeField("field", "logInfo")`, `m.top.observeField("field", "logger_logInfo")`);
});

it('prefixes even with complex ', async () => {
await testObserveField(`m.top.observeField("field", "logInfo")`, `m.top.observeField("field", "logger_logInfo")`);
});

it('prefixes with trailing comment', async () => {
await testObserveField(`m.top.observeField("field", "logInfo") 'comment`, `m.top.observeField("field", "logger_logInfo") 'comment`);
});
});

it('does not prefix inner-function function calls', async () => {
await createDependencies([{
name: 'logger',
Expand Down
18 changes: 9 additions & 9 deletions src/prefixer/RopmModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,13 +219,13 @@ export class RopmModule {
);
}

private readonly nonPrefixedFunctions = [
'runuserinterface',
'main',
'runscreensaver',
'init',
'onkeyevent'
];
private readonly nonPrefixedFunctionMap = {
'runuserinterface': true,
'main': true,
'runscreensaver': true,
'init': true,
'onkeyevent': true
};

/**
* Create the prefix map for this module
Expand Down Expand Up @@ -274,7 +274,7 @@ export class RopmModule {
//create an edit for each this-module-owned function
for (const func of file.functionDefinitions) {
//skip edits for special functions
if (this.nonPrefixedFunctions.includes(func.name.toLowerCase())) {
if (this.nonPrefixedFunctionMap[func.name.toLowerCase()]) {
continue;
}
file.addEdit(func.offset, func.offset, prefix);
Expand Down Expand Up @@ -387,7 +387,7 @@ export class RopmModule {
for (const file of this.files) {
for (const func of file.functionDefinitions) {
//skip the special function names
if (this.nonPrefixedFunctions.includes(func.name.toLowerCase())) {
if (this.nonPrefixedFunctionMap[func.name.toLowerCase()]) {
continue;
}
result[func.name.toLowerCase()] = true;
Expand Down
15 changes: 15 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,21 @@ export class Util {
return semver.prerelease(version) ? version : semver.major(version).toString();
}

/**
* Determine if a string has the same number of open parens as it does close parens
*/
public hasMatchingParenCount(text: string) {
let count = 0;
for (const char of text) {
if (char === '(') {
count++;
} else if (char === ')') {
count--;
}
}
return count === 0;
}

}
export const util = new Util();

Expand Down

0 comments on commit 297148b

Please sign in to comment.