Install & setup
npm install --save-dev kopytko-linterAdd 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
{
"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 flag → kopytko-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 _.
line 5'str' shadows the built-in global function 'str'. Use a different name to avoid hiding the built-in.
line 6Variable 'unused' is defined but never used. Prefix with `_` to indicate it is intentionally unused.
line 4Function "add" is missing a return type annotation.
line 4Parameter "a" is missing a type annotation.
line 4Parameter "b" is missing a type annotation.
line 10Function "multiply" is missing a return type annotation.
line 10Parameter "x" is missing a type annotation.
line 10Parameter "y" is missing a type annotation.
line 16Callback 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).
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.brsRules
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.
' @import /utils/helper.brs
' @import /utils/helper.brs ' duplicate — error' @import /utils/helper.brs ' only once import/unresolved error The @import or @mock path cannot be resolved to a file on disk.
' @import /components/NonExistent.brs ' file does not exist' @import /components/Existing.brs ' file exists import/unused warning An imported file's exported functions are never referenced in this file.
' @import /utils/math.brs ' imported but never used
function main()
print "hello" ' math functions never called
end function' @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.
' @import ' empty path' @import /components/Widget.brs import/path-not-absolute warning Import path does not begin with / (relative paths may behave unexpectedly).
' @import utils/helper.brs ' relative path' @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.
rem @import /components/Widget.brs ' rem style — not allowed' @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.
function main()
result = unknownHelper() ' no such function
end function' @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.
function main()
print value ' value never assigned
end functionfunction 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.
function main()
s = Left("hello") ' Left() requires 2 args
end functionfunction main()
s = Left("hello", 3) ' correct
end function identifier/shadows-builtin error A local variable or parameter name shadows a BrightScript built-in function.
function test()
str = "hello" ' shadows built-in Str() function
end functionfunction 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.
' @import /utils/helper.brs ' exports helper()
function main()
helper = "oops" ' shadows imported function
end function' @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.
function process(data as Object, context as Object) as Void
' context parameter is never used
print data.name
end functionfunction 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.
function main()
temp = fetchData() ' assigned, never read
print "done"
end functionfunction 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.
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 functionfunction 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.
function greet() as String
return "hello"
end function
function greet() as String ' duplicate!
return "hi"
end functionfunction greetFormal() as String
return "hello"
end function
function greetCasual() as String
return "hi"
end functionSyntax rules
syntax/trailing-comma error A trailing comma after a return value is a BrightScript syntax error.
function main()
return value, ' trailing comma — syntax error
end functionfunction 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.
function main()
exit for ' not inside a for loop
end functionfunction 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.
function test() as Integer
return 42
print "unreachable" ' never executes
end functionfunction 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.
throw 42 ' number — invalid
throw [1, 2] ' array — invalid
throw invalid ' invalid keyword — invalidthrow "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.
throw { code: 404 } ' no message fieldthrow { message: "Not found", code: 404 } createobject/unknown-component warning CreateObject() is called with a string that doesn't match any known BrightScript component name.
obj = CreateObject("roMyMadeUpType") ' unknown componentobj = CreateObject("roUrlTransfer") ' known componentType annotation rules
type/missing-return-type warning A function declaration is missing the "as Type" return type annotation.
function add(a as Integer, b as Integer) ' no return type
return a + b
end functionfunction 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.
function greet(name) ' no type annotation
return "Hello, " + name
end functionfunction greet(name as String) as String
return "Hello, " + name
end functionCallback rules
callback/undefined-observer-callback error observeFieldScoped() or observeField() references a callback function name that is not declared in any reachable scope.
sub init()
m.top.observeFieldScoped("input", "onInputChanged")
' but onInputChanged() function doesn't exist
end subsub 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.
' @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.
function TestSuite__MyComponent()
ts = {}
ts.test_init = sub()
' ...
end sub
' missing: return ts
end functionfunction TestSuite__MyComponent()
ts = {}
ts.test_init = sub()
' ...
end sub
return ts
end functionComponent 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.
<!-- components/Card.xml -->
<component name="Card" extends="Group" />
<!-- components/catalog/Card.xml -->
<component name="Card" extends="Group" /><!-- 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.
sub init()
m.top.unknownField = "Hello" ' not in own or ancestor interface
end sub' 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 subLibrary 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}`);
}
}