Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions bsconfig.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,21 @@
"11.0.0",
"12.0.0"
]
},
"strict": {
"description": "Enables a set of stricter type-checking rules and language features. When set, this acts as a default value for other `strict`-related options. Defaults to false.",
"type": "boolean",
"default": false
},
"strictCallFunc": {
"description": "Enables stricter type-checking for callfunc() invocations. When true, callfunc() invocations will not be allowed on generic Node types. Defaults to false.",
"type": "boolean",
"default": false
},
"strictNodeMembers": {
"description": "Enables stricter type-checking for Node members. When true, unknown members on Node types will be treated as errors instead of dynamic values. Defaults to false.",
"type": "boolean",
"default": false
}
}
}
51 changes: 51 additions & 0 deletions docs/bsconfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ While a minimal `bsconfig.json` file is sufficient for getting started, `bsc` su
- [`relativeSourceMaps: true`](#relativesourcemaps-true)
- [`sourceRoot`](#sourceroot)
- [`stagingDir`](#stagingdir)
- [`strict`](#strict)
- [`strictCallFunc`](#strictCallFunc)
- [`strictNodeMembers`](#strictNodeMembers)
- [`username`](#username)
- [`watch`](#watch)

Expand Down Expand Up @@ -587,6 +590,54 @@ The exact behavior depends on whether [`relativeSourceMaps`](#relativesourcemaps

In both cases, `SOURCE_FILE_PATH` and `SOURCE_LOCATION` source literals embedded in the transpiled output will reflect the `sourceRoot`-substituted path at runtime.

## `strict`

Type: `boolean`

Enables a set of stricter type-checking rules and language features. When set, this acts as a default value for other `strict`-related options. Defaults to false.

For example, in order to turn all all `strict` options, but disable `strictCallFunc`, you could specify:

```json
{
"strict": true,
"strictCallFunc": false
}
```

### `strictCallFunc`

Type: `boolean`

Enables stricter type-checking for callfunc() invocations. When true, callfunc() invocations will not be allowed on generic Node types. Defaults to false.

For example, if this were true:

```brighterscript
sub example(node as roSGNode)
node@.someFunction() ' error because roSGNode itself does not have any available call func's
end sub
```

### `strictNodeMembers`

Type: `boolean`

Enables stricter type-checking for Node members. When true, unknown members on Node types will be treated as errors instead of dynamic values. Defaults to false.

For example, if this were true:

```brighterscript
sub example()
myPoster = createObject("roSGNode", "Poster")
' update the node with a new field
myPoster.update({
data: 1234
, true})
print myPoster.data ' error because "Poster" does not have the "data" field
end sub
```

## `outDir`
Type: `string`

Expand Down
15 changes: 8 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions src/BsConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,24 @@ export interface BsConfig {
* @default true
*/
validate?: boolean;

/**
* When true, enables all strict mode options (strictCallFunc, strictNodeMembers, and any future options added that are considered "strict"). This is a convenient way to enable strict mode without having to set each option individually.
* @default false
*/
strict?: boolean;

/**
* Enables stricter type-checking for callfunc() invocations. When true, callfunc() invocations will not be allowed on generic Node types.
* @default false
*/
strictCallFunc?: boolean;

/**
* Enables stricter type-checking for Node members. When true, unknown members on Node types will be treated as errors instead of dynamic values.
* @default false
*/
strictNodeMembers?: boolean;
}

/**
Expand Down
14 changes: 7 additions & 7 deletions src/DiagnosticFilterer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,11 +326,11 @@ describe('DiagnosticFilterer', () => {
]);
});

describe('isFileFiltered', () => {
describe('isFileCompletelyFiltered', () => {
it('should find files that are completely filtered by src', () => {
filterer.options = options;
expect(filterer.isFileFiltered({ srcPath: `${rootDir}/lib/some/file.brs`, destPath: `source/some/file.brs` })).true;
expect(filterer.isFileFiltered({ srcPath: `${rootDir}/notlib/other/file.brs`, destPath: `source/other/file.brs` })).false;
expect(filterer.isFileCompletelyFiltered({ srcPath: `${rootDir}/lib/some/file.brs`, destPath: `source/some/file.brs` })).true;
expect(filterer.isFileCompletelyFiltered({ srcPath: `${rootDir}/notlib/other/file.brs`, destPath: `source/other/file.brs` })).false;
});

it('should find files that are completely filtered by dest', () => {
Expand All @@ -340,8 +340,8 @@ describe('DiagnosticFilterer', () => {
{ files: [{ dest: 'source/remove/**/*.*' }] }
]
};
expect(filterer.isFileFiltered({ srcPath: `${rootDir}/source/some/file.brs`, destPath: `source/remove/some/file.brs` })).true;
expect(filterer.isFileFiltered({ srcPath: `${rootDir}/source/other/file.brs`, destPath: `source/keep/other/file.brs` })).false;
expect(filterer.isFileCompletelyFiltered({ srcPath: `${rootDir}/source/some/file.brs`, destPath: `source/remove/some/file.brs` })).true;
expect(filterer.isFileCompletelyFiltered({ srcPath: `${rootDir}/source/other/file.brs`, destPath: `source/keep/other/file.brs` })).false;
});

it('should find files that are completely filtered by src, with negative filtering', () => {
Expand All @@ -354,8 +354,8 @@ describe('DiagnosticFilterer', () => {
{ files: '!lib/special/**/*.brs' }
]
};
expect(filterer.isFileFiltered({ srcPath: `${rootDir}/lib/some/file.brs`, destPath: `source/some/file.brs` })).true;
expect(filterer.isFileFiltered({ srcPath: `${rootDir}/lib/special/file.brs`, destPath: `source/special/file.brs` })).false;
expect(filterer.isFileCompletelyFiltered({ srcPath: `${rootDir}/lib/some/file.brs`, destPath: `source/some/file.brs` })).true;
expect(filterer.isFileCompletelyFiltered({ srcPath: `${rootDir}/lib/special/file.brs`, destPath: `source/special/file.brs` })).false;
});
});

Expand Down
2 changes: 1 addition & 1 deletion src/DiagnosticFilterer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export class DiagnosticFilterer {
* Should this file be completely ignored?
* If the file diagnostics are ignored, we do not need to validate this file
*/
public isFileFiltered(file: { srcPath: string; destPath: string }) {
public isFileCompletelyFiltered(file: { srcPath: string; destPath: string }) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed name to make function more clear

if (!this.options || !this.filters) {
return false;
}
Expand Down
10 changes: 5 additions & 5 deletions src/DiagnosticManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,17 +253,17 @@ describe('DiagnosticManager', () => {
});
});

describe('shouldFilterFile', () => {
describe('canSkipScopeValidationForFile', () => {
it('returns true if the DiagnosticFilterer says to filter the file', () => {
const file = program.setFile('source/main.brs', '') as BrsFile;
program.diagnostics['diagnosticFilterer'].isFileFiltered = () => true;
expect(program.diagnostics.shouldFilterFile(file)).to.be.true;
program.diagnostics['diagnosticFilterer'].isFileCompletelyFiltered = () => true;
expect(program.diagnostics.canSkipScopeValidationForFile(file)).to.be.true;
});

it('returns false if the DiagnosticFilterer says not to filter the file', () => {
const file = program.setFile('source/main.brs', '') as BrsFile;
program.diagnostics['diagnosticFilterer'].isFileFiltered = () => false;
expect(program.diagnostics.shouldFilterFile(file)).to.be.false;
program.diagnostics['diagnosticFilterer'].isFileCompletelyFiltered = () => false;
expect(program.diagnostics.canSkipScopeValidationForFile(file)).to.be.false;
});
});
});
10 changes: 7 additions & 3 deletions src/DiagnosticManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,13 +478,17 @@ export class DiagnosticManager {
}
}

public shouldFilterFile(file: BrsFile): boolean {
/**
* Are the diagnostics for this file completely filtered?
* If so, we can skip any Scope-based validation on this file at all, which can save a lot of time for large files
* with many diagnostics that are being ignored
*/
public canSkipScopeValidationForFile(file: BrsFile): boolean {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed name to make function use more clear

if (this.diagnosticFilterer.options !== this.options) {
this.diagnosticFilterer.options = this.options;
}
return this.diagnosticFilterer.isFileFiltered(file);
return this.diagnosticFilterer.isFileCompletelyFiltered(file);
}

}

interface DiagnosticContextFilter {
Expand Down
96 changes: 96 additions & 0 deletions src/bscPlugin/validation/ScopeValidator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2974,6 +2974,89 @@ describe('ScopeValidator', () => {
]);
});
});

describe('arbitrary members on roSGNode and roSNodeContentNode', () => {
it('allows arbitrary member on node in BrightScript (brs) files', () => {
program.setFile('source/util.brs', `
function doStuff()
node = createObject("roSGNode", "Node")
node.someArbitraryMember = "hello"
return node
end function
`);
program.validate();
expectZeroDiagnostics(program);
});

it('allows arbitrary member on node in BrightScript (bs) files', () => {
program.setFile('components/comp1.xml', `
<component name="Comp1" extends="Group">
<script uri="comp1.bs"/>
<interface>
<field id="myNode" type="node" />
</interface>
</component>
`);
program.setFile('components/comp1.bs', `
function doStuff()
node = createObject("roSGNode", "Node")
node.someArbitraryMember = "hello"
return node
end function
`);
program.validate();
expectZeroDiagnostics(program);
});

it('allows arbitrary member on node as part of component interface', () => {
program.setFile('components/comp1.xml', `
<component name="Comp1" extends="Group">
<interface>
<field id="myNode" type="node" />
</interface>
</component>
`);

program.setFile('source/util.bs', `
function getComp1(node as roSGNode) as roSGNodeComp1
comp1 = createObject("roSGNode", "Comp1")
comp1.myNode = node
comp1.myNode.someArbitraryMember = "hello"
return comp1
end function
`);
program.validate();
expectZeroDiagnostics(program);
});

it('allows arbitrary member on ContentNode', () => {
program.setFile('source/util.bs', `
function buildContentNode(data as roAssociativeArray) as roSGNodeContentNode
content = createObject("roSGNode", "ContentNode")
content.update(data, true)
content.arbitrary = 123
end function
`);
program.validate();
expectZeroDiagnostics(program);
});

it('disallows arbitrary member on node when strictNodeMembers mode is on', () => {
program.options.strictNodeMembers = true;
program.setFile('source/util.bs', `
function doStuff()
node = createObject("roSGNode", "Node")
node.someArbitraryMember = "hello"
return node
end function
`);
program.validate();
expectDiagnostics(program, [
DiagnosticMessages.cannotFindName('someArbitraryMember', 'roSGNodeNode.someArbitraryMember', 'roSGNodeNode')
]);
});
});

});

describe('itemCannotBeUsedAsVariable', () => {
Expand Down Expand Up @@ -6902,6 +6985,19 @@ describe('ScopeValidator', () => {
DiagnosticMessages.notCallable('node@.getName').message
]);
});

it('disallows callfunc on generic node when strictCallFunc mode is enabled', () => {
program.options.strictCallFunc = true;
program.setFile('source/test.bs', `
sub doCallfunc(node as roSGNode)
node.callfunc("someFunc", 1, 2, 3)
end sub
`);
program.validate();
expectDiagnostics(program, [
DiagnosticMessages.cannotFindCallFuncFunction('someFunc', 'roSGNodeNode@.someFunc', 'roSGNodeNode').message
]);
});
});

describe('notIterable', () => {
Expand Down
15 changes: 6 additions & 9 deletions src/bscPlugin/validation/ScopeValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export class ScopeValidator {
this.event.scope.enumerateOwnFiles((file) => {
if (isBrsFile(file)) {

if (this.event.program.diagnostics.shouldFilterFile(file)) {
if (this.event.program.diagnostics.canSkipScopeValidationForFile(file)) {
return;
}

Expand Down Expand Up @@ -678,7 +678,7 @@ export class ScopeValidator {
const firstArgToken = call?.args[0]?.tokens.value;
if (callName === 'createchild') {
this.checkComponentName(firstArgToken);
} else if (callName === 'callfunc' && !util.isGenericNodeType(callerType)) {
} else if (callName === 'callfunc' && (!util.isGenericNodeType(callerType, true) || this.event.program.options.strictCallFunc)) {
const funcType = util.getCallFuncType(call, firstArgToken, { flags: SymbolTypeFlag.runtime, ignoreCall: true });
if (!funcType?.isResolvable()) {
const functionName = firstArgToken.text.replace(/"/g, '');
Expand Down Expand Up @@ -718,7 +718,7 @@ export class ScopeValidator {
}
const functionFullname = `${callerType.toString()}@.${methodName}`;
const callErrorLocation = expression.location;
if (util.isGenericNodeType(callerType) || isObjectType(callerType) || isDynamicType(callerType)) {
if ((util.isGenericNodeType(callerType, true) && !this.event.program.options.strictCallFunc) || isObjectType(callerType) || isDynamicType(callerType)) {
// ignore "general" node
return;
}
Expand Down Expand Up @@ -1783,7 +1783,9 @@ export class ScopeValidator {
* @returns the processed result type
*/
private getNodeTypeWrapper(file: BrsFile, node: AstNode, getTypeOpts: GetTypeOptions) {
const type = node?.getType(getTypeOpts);
const isBrightScriptMode = file.parseMode === ParseMode.BrightScript;

const type = node?.getType({ ...getTypeOpts, changeUnknownNodeMemberToDynamic: isBrightScriptMode || !this.event.program.options.strictNodeMembers });

if (file.parseMode === ParseMode.BrightScript) {
// this is a brightscript file
Expand All @@ -1801,11 +1803,6 @@ export class ScopeValidator {
//this is a union
return DynamicType.instance;
}

if (isComponentType(type)) {
// modify type to allow any member access for Node types
type.changeUnknownMemberToDynamic = true;
}
}

// by default return the result of node.getType()
Expand Down
Loading
Loading