Skip to content

Commit

Permalink
Interface optional properties (#946)
Browse files Browse the repository at this point in the history
* Parser allows optional interface properties and adds them as optional members

* Added interface completion tests

* Added optional interface members docs

* Reworked optional members to use a keyword

* Fixed hovers for optional properties

* Fixed merge issue

* updated documentation

* Fixed code in completions test

* remove questionLeftParen in interface method dclaration

* ensure interface field name is an identifier

* Updated comment

* fix package builder

* Fix crash on missing node type

* Fixed issue with using a variable assigned from an unknown variable

* Allow enums to be named `optional.
Add several related tests.

---------

Co-authored-by: Bronley Plumb <bronley@gmail.com>
  • Loading branch information
markwpearce and TwitchBronBron authored Nov 21, 2023
1 parent 378c599 commit 2c484e3
Show file tree
Hide file tree
Showing 21 changed files with 811 additions and 57 deletions.
2 changes: 0 additions & 2 deletions .github/workflows/create-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ jobs:
echo "ARTIFACT_URL=$ARTIFACT_URL" >> $GITHUB_ENV
- run: npm ci
- run: npm config set ignore-scripts true
- run: npm run build
- run: npm version "$BUILD_VERSION" --no-git-tag-version
- run: npm pack

Expand Down
53 changes: 47 additions & 6 deletions docs/classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Traditional BrightScript supports object creation (see [roAssociativeArray](https://developer.roku.com/docs/references/brightscript/components/roassociativearray.md)), but these can get a bit complicated at times, are generally created in a factory function. These objects are also difficult to track from a type-checking perspective, as their shape can morph over time. In BrighterScript, you can declare actual classes, and compile them into pure BrightScript which can be used on any Roku device.

## Classes

Here's an example of a simple BrighterScript class

```vb
Expand Down Expand Up @@ -35,11 +36,12 @@ function Animal()
end function
```

Notice that there are two functions created in the transpiled code for the `Animal` class. At runtime, BrighterScript classes are built in two steps in order to support class inheritance. The first step uses the `__ClassName_Build()` method to create the skeleton structure of the class. Then the class's constructor function will be run. Child classes will call the parent's `__ParentClassName_Build()` method, then rename overridden methods, and then call the child's constructor (without calling the parent constructor). Take a look at the transpiled output of the other examples below for more information on this.

Notice that there are two functions created in the transpiled code for the `Animal` class. At runtime, BrighterScript classes are built in two steps in order to support class inheritance. The first step uses the `__ClassName_Build()` method to create the skeleton structure of the class. Then the class's constructor function will be run. Child classes will call the parent's `__ParentClassName_Build()` method, then rename overridden methods, and then call the child's constructor (without calling the parent constructor). Take a look at the transpiled output of the other examples below for more information on this.

## Inheritance

In BrighterScript, we can use patterns common to other object-oriented languages such as using inheritance to create a new class based off of an existing class.

```BrighterScript
class Animal
sub new(name as string)
Expand Down Expand Up @@ -81,6 +83,7 @@ sub Main()
'> Waddling...\nDewey moved 2 meters\nFell over...I'm new at this
end sub
```

<details>
<summary>View the transpiled BrightScript code</summary>

Expand Down Expand Up @@ -150,10 +153,11 @@ sub Main()
'> Waddling...\nDewey moved 2 meters\nFell over...I'm new at this
end sub
```
</details>

</details>

## Constructor function

The constructor function for a class is called `new`.

```vb
Expand Down Expand Up @@ -185,9 +189,11 @@ function Duck(name as string)
return instance
end function
```

</details>

### Constructor function with inheritance

When constructing a child that inherits from a base class, the first call in the child's constructor must be a call to the parent's constructor

```vb
Expand Down Expand Up @@ -243,9 +249,11 @@ function BabyDuck(name as string, age as integer)
return instance
end function
```

</details>

## Overrides

Child classes can override methods on parent classes. In this example, the `BabyDuck.Eat()` method completely overrides the parent method. Note: the `override` keyword is mandatory, and you will get a compile error if it is not included in the child class and there is a matching method on the base class. Also, you will get a compile error if the override keyword is present in a child class, but that method doesn't exist in the parent class.

```vb
Expand Down Expand Up @@ -298,9 +306,11 @@ function BabyDuck()
return instance
end function
```

</details>

### Calling parent method from child

You can also call the original methods on the base class from within an overridden method on a child class.

```vb
Expand All @@ -318,6 +328,7 @@ class BabyDuck extends Duck
end class

```

<details>
<summary>View the transpiled BrightScript code</summary>

Expand Down Expand Up @@ -355,9 +366,11 @@ function BabyDuck()
return instance
end function
```

</details>

## Public by default

Class fields and methods are public by default, which aligns with the general BrightScript approach that "everything is public".

```vb
Expand All @@ -379,6 +392,7 @@ class Person
end sub
end class
```

<details>
<summary>View the transpiled BrightScript code</summary>

Expand Down Expand Up @@ -407,11 +421,13 @@ function Person()
return instance
end function
```

</details>

You will get compile-time errors whenever you access private members of a class from outside the class. However, be aware that this is only a compile-time restriction. At runtime, all members are public.

## Dynamic type by default

You can specify a type for class fields and a return type for methods. However, this is entirely optional. All fields and methods have a default type of `dynamic`. However, BrighterScript will attempt to infer the type from usage. Take this for example:

```vb
Expand All @@ -428,6 +444,7 @@ class Person
end function
end class
```

<details>
<summary>View the transpiled BrightScript code</summary>

Expand All @@ -452,9 +469,11 @@ function Person()
return instance
end function
```

</details>

## Property initialization

Like most other object-oriented classes, you can initialze a property with a default value.

```BrighterScript
Expand All @@ -463,6 +482,7 @@ class Duck
hasChildren = true
end class
```

<details>
<summary>View the transpiled BrightScript code</summary>

Expand All @@ -481,27 +501,30 @@ function Duck()
return instance
end function
```
</details>

</details>

## Usage

In order to use a class, you need to construct one. Based on our person class above, you can create a new person like this:

```vb
donald = new Person("Donald")
daisy = new Person("Daisy")
```

<details>
<summary>View the transpiled BrightScript code</summary>

```BrightScript
donald = Person("Donald")
daisy = Person("Daisy")
```
</details>

</details>

## Namespaces

Classes can also be contained within a namespace. At runtime, all namespace periods are replaced with underscores.

```BrighterScript
Expand All @@ -513,6 +536,7 @@ namespace Vertibrates.Birds
end class
end namespace
```

<details>
<summary>View the transpiled BrightScript code</summary>

Expand Down Expand Up @@ -542,10 +566,11 @@ function Vertibrates_Birds_Duck()
return instance
end function
```
</details>

</details>

## Instance binding

As you would expect, methods attached to a class will have their `m` assigned to that class instance whenever they are invoked through the object. (i.e. `someClassInstance.doSomething()`. However, keep in mind that functions themselves retain no direct knowledge of what object they are bound to.

This is no different than the way plain BrightScript functions and objects interact, but it was worth mentioning that classes cannot mitigate this fundamental runtime limitation.
Expand Down Expand Up @@ -576,6 +601,7 @@ sub main()
sayHello() ' prints "Main method"
end sub
```

<details>
<summary>View the transpiled BrightScript code</summary>

Expand Down Expand Up @@ -607,4 +633,19 @@ sub main()
sayHello() ' prints "Main method"
end sub
```

</details>

## Optional fields

Classes can include optional fields, denoted with the keyword `optional` before the name. If a class has an optional field, it signifies that the value of the field may be `invalid`. Optional fields in a class do not change how the class or its members are validated.

```brighterscript
class Video
url as string
length as float
optional subtitleUrl as string
optional rating as string
optional genre as string[]
end class
```
36 changes: 36 additions & 0 deletions docs/interfaces.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Interfaces

Interfaces are a way to describe properties and methods on an object.

## Simple example

Interfaces can have both properties and methods.

```BrighterScript
Expand All @@ -24,7 +26,9 @@ end sub
```

## Use in functions

Interfaces can be used as function parameter types and return types. These types will be converted into `dynamic` when the project is transpiled.

```brighterscript
interface Dog
name as string
Expand All @@ -46,9 +50,11 @@ sub walkThePuppy(puppy as dynamic) as dynamic
return puppy
end sub
```

</details>

## Namespaces

Interfaces can also be defined inside namespaces

```BrighterScript
Expand All @@ -73,7 +79,9 @@ end sub
```

## Advanced types

Interface member types can be defined as any valid brighterscript type, and even reference themselves.

```brighterscript
interface Dog
owner as Human
Expand All @@ -91,12 +99,15 @@ end interface
```BrightScript
```

</details>

</details>

## Methods

Interfaces can describe complex methods as well

```brighterscript
interface Dog
sub barkAt(nemesis as Cat)
Expand All @@ -107,5 +118,30 @@ end interface
<summary>View the transpiled BrightScript code</summary>

```BrightScript
```

</details>

## Optional members

Interfaces can include optional members. Optional members are denoted with the keyword `optional` before the name of fields, or before the `function` or `sub` keyword for methods. Interfaces with optional members will not incur a validation diagnostic if an object that is missing that member is passed to it, yet optional members will be included in completion results, and if referenced, they are inferred to be their declared type.

```brighterscript
interface Video
url as string
length as float
optional subtitleUrl as string
optional rating as string
optional genre as string[]
end interface
```

<details>
<summary>View the transpiled BrightScript code</summary>

```BrightScript
```

</details>
2 changes: 1 addition & 1 deletion src/AstValidationSegmenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class AstValidationSegmenter {
const options: GetTypeOptions = { flags: flag, onlyCacheResolvedTypes: true, typeChain: typeChain };

const nodeType = expression.getType(options);
if (!nodeType.isResolvable()) {
if (!nodeType?.isResolvable()) {
let symbolsSet: Set<UnresolvedSymbol>;
if (!assignedSymbols?.has(typeChain[0].name.toLowerCase())) {
if (!this.unresolvedSegmentsSymbols.has(segment)) {
Expand Down
11 changes: 8 additions & 3 deletions src/SymbolTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { isAnyReferenceType, isReferenceType } from './astUtils/reflection';

export enum SymbolTypeFlag {
runtime = 1,
typetime = 2
typetime = 2,
optional = 4
}

/**
Expand Down Expand Up @@ -198,7 +199,7 @@ export class SymbolTable implements SymbolTypeGetter {
if (!symbolArray) {
return undefined;
}
return symbolArray?.map(symbol => ({ type: symbol.type, data: symbol.data }));
return symbolArray?.map(symbol => ({ type: symbol.type, data: symbol.data, flags: symbol.flags }));
}

getSymbolType(name: string, options: GetSymbolTypeOptions): BscType {
Expand All @@ -207,9 +208,11 @@ export class SymbolTable implements SymbolTypeGetter {
let doSetCache = !resolvedType;
const originalIsReferenceType = isAnyReferenceType(resolvedType);
let data = {} as ExtraSymbolData;
let foundFlags: SymbolTypeFlag;
if (!resolvedType || originalIsReferenceType) {
const symbolTypes = this.getSymbolTypes(name, options);
data = symbolTypes?.[0]?.data;
foundFlags = symbolTypes?.[0].flags;
resolvedType = getUniqueType(symbolTypes?.map(symbol => symbol.type), SymbolTable.unionTypeFactory);
}
if (!resolvedType && options.fullName && options.tableProvider) {
Expand All @@ -219,11 +222,12 @@ export class SymbolTable implements SymbolTypeGetter {
const newNonReferenceType = originalIsReferenceType && !isAnyReferenceType(resolvedType);
doSetCache = doSetCache && (options.onlyCacheResolvedTypes ? !resolvedTypeIsReference : true);
if (doSetCache || newNonReferenceType) {
this.setCachedType(name, { type: resolvedType, data: data }, options);
this.setCachedType(name, { type: resolvedType, data: data, flags: foundFlags }, options);
}
if (options.data) {
options.data.definingNode = data?.definingNode;
options.data.description = data?.description;
options.data.flags = foundFlags;
}
return resolvedType;
}
Expand Down Expand Up @@ -379,4 +383,5 @@ export interface GetSymbolTypeOptions extends GetTypeOptions {
export interface TypeCacheEntry {
type: BscType;
data?: ExtraSymbolData;
flags?: SymbolTypeFlag;
}
Loading

0 comments on commit 2c484e3

Please sign in to comment.