npm/ kopytko-linter v1.7.0

BrightScript Linter

Static analysis for BrightScript with 32 rules covering imports, undefined identifiers, type annotations, syntax errors, and callback wiring. Outputs text, JSON, or SARIF for GitHub Code Scanning.

Install & setup

npm install --save-dev kopytko-linter

Add scripts to package.json:

{
  "scripts": {
    "lint": "kopytko-lint",
    "lint:ci": "kopytko-lint --check",
    "lint:fix": "kopytko-lint --fix",
    "lint:sarif": "kopytko-lint --format sarif > results.sarif"
  }
}

Configuration — kopytko-linter.json

kopytko-linter.json
{
  "sourceDir": "src",
  "resolveModules": true,
  "generatedPaths": ["/source/generated/**"],
  "readOnlyPaths": ["**/node_modules/**"],
  "rules": {
    "import/unused": "off",
    "identifier/shadows-builtin": "warning",
    "type/missing-return-type": "error",
    "type/missing-param-type": "error",
    "identifier/unused-function": "hint"
  }
}

Config resolution order: --config flagkopytko-linter.json.vscode/settings.json → defaults

Live playground

Edit the code and toggle rules to see diagnostics update instantly. Try adding as String to a parameter, renaming str to text, or prefixing an unused variable with _.

Linter PlaygroundEdit code and toggle rules — diagnostics update live
2 errors 7 warnings
Code — edit to experiment
Diagnostics
line 5

'str' shadows the built-in global function 'str'. Use a different name to avoid hiding the built-in.

line 6

Variable 'unused' is defined but never used. Prefix with `_` to indicate it is intentionally unused.

line 4

Function "add" is missing a return type annotation.

line 4

Parameter "a" is missing a type annotation.

line 4

Parameter "b" is missing a type annotation.

line 10

Function "multiply" is missing a return type annotation.

line 10

Parameter "x" is missing a type annotation.

line 10

Parameter "y" is missing a type annotation.

line 16

Callback function 'onChanged' is not defined in this file or any reachable @import.

Rules — click to toggle. Greyed = requires import/XML context (won't fire in single-file mode).

Import
Identifier
Syntax
Type
Callback
Test
m.top

CI integration

# .github/workflows/lint.yml
name: Lint BrightScript
on: [push, pull_request]
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 24 }
      - run: npm ci
      - run: npm run lint

# For GitHub Code Scanning (SARIF):
      - run: npx kopytko-lint --format sarif > results.sarif
      - uses: github/codeql-action/upload-sarif@v3
        with: { sarif_file: results.sarif }

Inline suppression

Use comment directives to suppress specific rules. Glob patterns are supported.

' Suppress a specific rule on the next line
' kopytko-disable-next-line identifier/undefined-function
result = dynamicallyRegisteredFunction()

' Suppress multiple rules at once
' kopytko-disable-next-line import/unresolved, identifier/undefined-function
' @import /generated/build-output.brs

' Suppress inline (on same line as the code)
x = getVal()  ' kopytko-disable-line identifier/undefined-function

' Glob patterns work too
' kopytko-disable-next-line import/*
' @import /generated/anything.brs

Rules

32 rules across 8 categories. Severity levels: error warning info hint off by default

Import rules

import/duplicate error

The same import path appears more than once in the file.

❌ Triggers rule
' @import /utils/helper.brs
' @import /utils/helper.brs  ' duplicate — error
✓ OK
' @import /utils/helper.brs  ' only once
import/unresolved error

The @import or @mock path cannot be resolved to a file on disk.

❌ Triggers rule
' @import /components/NonExistent.brs  ' file does not exist
✓ OK
' @import /components/Existing.brs   ' file exists
import/unused warning

An imported file's exported functions are never referenced in this file.

❌ Triggers rule
' @import /utils/math.brs  ' imported but never used

function main()
  print "hello"  ' math functions never called
end function
✓ OK
' @import /utils/math.brs

function main()
  result = add(1, 2)  ' references imported function
end function
import/missing-path error

@import or @mock annotation has an empty path.

❌ Triggers rule
' @import   ' empty path
✓ OK
' @import /components/Widget.brs
import/path-not-absolute warning

Import path does not begin with / (relative paths may behave unexpectedly).

❌ Triggers rule
' @import utils/helper.brs   ' relative path
✓ OK
' @import /utils/helper.brs  ' absolute path
import/build-generated info

Import path matches a configured generatedPaths glob and cannot be resolved — treated as generated-at-build-time.

Configure generatedPaths in kopytko-linter.json to suppress this for known build outputs.

import/wrong-comment-style error

@import or @mock annotation uses the rem comment style. Tick (') is the only supported comment style for import annotations.

❌ Triggers rule
rem @import /components/Widget.brs  ' rem style — not allowed
✓ OK
' @import /components/Widget.brs   ' tick style — correct
import/missing-promise-deps warning

Test file uses .resolvedValue() or .rejectedValue() without importing the required PromiseResolve.brs or PromiseReject.brs from @dazn/kopytko-utils.

Identifier rules

identifier/undefined-function error

A function call references a name that is not declared in any reachable scope or import.

❌ Triggers rule
function main()
  result = unknownHelper()  ' no such function
end function
✓ OK
' @import /utils/helper.brs  ' declares helper()

function main()
  result = helper()
end function
identifier/undefined-variable error

A variable is read before it is assigned in the enclosing function scope.

❌ Triggers rule
function main()
  print value  ' value never assigned
end function
✓ OK
function main()
  value = "hello"
  print value
end function
identifier/wrong-arg-count error

A call to a built-in function passes the wrong number of arguments.

❌ Triggers rule
function main()
  s = Left("hello")   ' Left() requires 2 args
end function
✓ OK
function main()
  s = Left("hello", 3)  ' correct
end function
identifier/shadows-builtin error

A local variable or parameter name shadows a BrightScript built-in function.

❌ Triggers rule
function test()
  str = "hello"  ' shadows built-in Str() function
end function
✓ OK
function test()
  text = "hello"    ' safe — no built-in named Text
  uniqueKey = "id"  ' also safe
end function
identifier/shadows-function error

A local variable or parameter shadows a user-defined function from the same scope or an import.

❌ Triggers rule
' @import /utils/helper.brs  ' exports helper()

function main()
  helper = "oops"  ' shadows imported function
end function
✓ OK
' @import /utils/helper.brs

function main()
  helperResult = helper()  ' calls the function
end function
identifier/unused-parameter warning

A function parameter is never read in the function body. Prefix the name with _ to suppress.

❌ Triggers rule
function process(data as Object, context as Object) as Void
  ' context parameter is never used
  print data.name
end function
✓ OK
function process(data as Object, _context as Object) as Void
  ' _context suppresses the warning
  print data.name
end function
identifier/unused-variable warning

A local variable is assigned but never read. Prefix the name with _ to suppress.

❌ Triggers rule
function main()
  temp = fetchData()  ' assigned, never read
  print "done"
end function
✓ OK
function main()
  _temp = fetchData()  ' _ prefix suppresses warning
  print "done"
end function
identifier/unused-function hint

A function is declared but never called anywhere in the project.

Off by default. Enable with "identifier/unused-function": "hint" in your config.

identifier/loop-variable-leak warning

A for-loop variable is first assigned inside the loop but is read after the loop ends. In BrightScript all variables are function-scoped, so the loop variable leaks into the enclosing function scope.

❌ Triggers rule
function test()
  for i = 1 to 5   ' i first assigned inside loop
    print i
  end for
  print i           ' reads leaked loop variable (= 6 after loop)
end function
✓ OK
function test()
  for idx = 1 to 5
    print idx
  end for
  ' don't read idx after the loop if it wasn't defined before it
end function
identifier/duplicate-function error

Two functions with the same name are declared in the same file.

❌ Triggers rule
function greet() as String
  return "hello"
end function

function greet() as String  ' duplicate!
  return "hi"
end function
✓ OK
function greetFormal() as String
  return "hello"
end function

function greetCasual() as String
  return "hi"
end function

Syntax rules

syntax/trailing-comma error

A trailing comma after a return value is a BrightScript syntax error.

❌ Triggers rule
function main()
  return value,  ' trailing comma — syntax error
end function
✓ OK
function main()
  return value
end function
syntax/flow-outside-loop error

exit for, continue for, exit while, or continue while used outside the matching loop construct.

❌ Triggers rule
function main()
  exit for  ' not inside a for loop
end function
✓ OK
function main()
  for i = 1 to 10
    if i = 5 then exit for  ' inside loop — OK
  end for
end function
syntax/unreachable-code warning

Statements that appear after an unconditional return, throw, or stop are unreachable.

❌ Triggers rule
function test() as Integer
  return 42
  print "unreachable"  ' never executes
end function
✓ OK
function test() as Integer
  return 42
end function
throw/invalid-value warning

throw with a numeric literal, array, or invalid — only strings and associative arrays are valid throw values in BrightScript.

❌ Triggers rule
throw 42          ' number — invalid
throw [1, 2]      ' array — invalid
throw invalid     ' invalid keyword — invalid
✓ OK
throw "error message"
throw { message: "something went wrong", code: -1 }
throw/missing-message warning

A thrown associative array does not have a message field, making error handling harder.

❌ Triggers rule
throw { code: 404 }  ' no message field
✓ OK
throw { message: "Not found", code: 404 }
createobject/unknown-component warning

CreateObject() is called with a string that doesn't match any known BrightScript component name.

❌ Triggers rule
obj = CreateObject("roMyMadeUpType")  ' unknown component
✓ OK
obj = CreateObject("roUrlTransfer")   ' known component

Type annotation rules

type/missing-return-type warning

A function declaration is missing the "as Type" return type annotation.

❌ Triggers rule
function add(a as Integer, b as Integer)  ' no return type
  return a + b
end function
✓ OK
function add(a as Integer, b as Integer) as Integer
  return a + b
end function
type/missing-param-type warning

One or more function parameters are missing "as Type" annotations.

❌ Triggers rule
function greet(name)  ' no type annotation
  return "Hello, " + name
end function
✓ OK
function greet(name as String) as String
  return "Hello, " + name
end function

Callback rules

callback/undefined-observer-callback error

observeFieldScoped() or observeField() references a callback function name that is not declared in any reachable scope.

❌ Triggers rule
sub init()
  m.top.observeFieldScoped("input", "onInputChanged")
  ' but onInputChanged() function doesn't exist
end sub
✓ OK
sub init()
  m.top.observeFieldScoped("input", "onInputChanged")
end sub

sub onInputChanged(event as Object)
  print event.getData()
end sub
callback/undefined-event-callback error

An event callback function referenced in the component XML or code does not exist.

Test framework rules

test/missing-mock-annotation warning

mockFunction("X") targets a function not found in any @mock-annotated file in the test.

❌ Triggers rule
' @import /components/Widget.brs
' (no @mock annotations)

function TestSuite__Widget()
  ts = {}
  ts.test_init = sub()
    mockFunction("helper")  ' helper not in any @mock file
  end sub
  return ts
end function
test/missing-return-ts warning

A TestSuite__* function is missing the required "return ts" statement at the end.

❌ Triggers rule
function TestSuite__MyComponent()
  ts = {}
  ts.test_init = sub()
    ' ...
  end sub
  ' missing: return ts
end function
✓ OK
function TestSuite__MyComponent()
  ts = {}
  ts.test_init = sub()
    ' ...
  end sub
  return ts
end function

Component rules

component/duplicate-name warning

Two XML files declare the same SceneGraph component name. Component names are global to the channel, so whichever declaration loads last silently wins.

A project-wide check rather than a per-file rule: it runs once per lint over every component XML in the project and its installed Kopytko packages, and reports on both declarations. A clash with a package component is reported on your file only. Build pipelines that copy the source tree into an output directory should exclude the copy via readOnlyPaths.

❌ Triggers rule
<!-- components/Card.xml -->
<component name="Card" extends="Group" />

<!-- components/catalog/Card.xml -->
<component name="Card" extends="Group" />
✓ OK
<!-- components/Card.xml -->
<component name="Card" extends="Group" />

<!-- components/catalog/CatalogCard.xml -->
<component name="CatalogCard" extends="Group" />

m.top rules

mtop/undefined-field warning

m.top.fieldName accesses a field that is not defined in the component's XML interface (either directly or via ancestor extends).

Only active in extension mode where the XML interface is accessible. Fields defined in ancestor components (via extends) are also considered valid.

❌ Triggers rule
sub init()
  m.top.unknownField = "Hello"   ' not in own or ancestor interface
end sub
✓ OK
' Component XML: <field id="title" type="string" />
' OR an ancestor component defines "title" in its <interface>

sub init()
  m.top.title = "Hello"   ' found in own or inherited interface
end sub

Library API

import { lintProject } from 'kopytko-linter';

const result = lintProject('/path/to/project');

console.log(`${result.errorCount} errors, ${result.warningCount} warnings`);

for (const fileDiagnostics of result.files) {
  for (const diag of fileDiagnostics.diagnostics) {
    console.log(`${diag.file}:${diag.line + 1}: [${diag.rule}] ${diag.message}`);
  }
}