@hyperfrontend/linter/helpershelpers
What a rule asks of a file beyond the tree it is handed: the tokens and comments around a node, scopes and variables, the properties of a JSON document, line and column positions, globs, a fix merged from several edits, and the parse of another file. The packaged rules use the same functions.
Usage
import { defineRule } from '@hyperfrontend/linter'
import { findVariable, scopeAnalysisOf, scopeOf } from '@hyperfrontend/linter/helpers'
export default defineRule({
id: 'local/no-shadowed-name',
language: 'code',
fixable: false,
create: (context) => ({
Identifier(node) {
if (node.name !== 'name') return
const scope = scopeOf(scopeAnalysisOf(context), node)
if (findVariable(scope, 'name') === null) return
const message = 'Rename the local "name".'
context.report({ start: node.start, end: node.end, message })
},
}),
})
What is here
| Area | Functions |
|---|---|
| Tokens | tokenBefore, tokenAfter, tokensBetween, tokenText, commentsBetween, isParenthesised |
| Tree | childrenOf, parseCode |
| Scopes | scopeAnalysisOf, scopeOf, findVariable, referenceOf, isGlobalReference |
| JSON | rootObject, propertyOf, isString, stringsUnder, wholeDocument, indentationAt |
| Positions | positionAt, lineText |
| Globs | compileGlob, matchesAny |
| Fixes | mergeEdits |
Scope analysis is computed once per file and shared by every rule that asks for it, so calling scopeAnalysisOf from several listeners costs one analysis.
API Reference§
ƒ Functions
Returns
Node[ ]Example
List an if statement's children
const { tree } = parseCode('if (ready) start()', '.ts')
childrenOf(tree.body[0]).map((child) => child.kind)
// => ['Identifier', 'ExpressionStatement'] (the null alternate is skipped)Parameters
Returns
SourceComment[ ]Example
Find the comments inside a range
// file.text: 'const total = price * 2 // doubled'
commentsBetween(file, 0, file.text.length)
// => [{ start: 24, end: 34, text: ' doubled', ... }]Parameters
| Name | Type | Description |
|---|---|---|
§pattern | string | The text as a configuration wrote it |
Returns
GlobExample
Match alternatives, everything below, or a whole directory
const entries = compileGlob('src/index.{ts,tsx}')
entries.test('src/index.tsx') // => true
entries.test('src/index.js') // => false
compileGlob('src/**').test('src/ui/button.tsx') // => true
const dist = compileGlob('dist/')
dist.test('dist/index.js') // => true
dist.test('distance.ts') // => falseReturns
VariableExample
Resolve a name outward from a function's scope
const count = findVariable(scopeOf(analysis, step), 'count')
count.scope.kind // => 'module'
count.references.map((reference) => reference.direction)
// => ['write', 'read-write']
findVariable(scopeOf(analysis, step), 'missing') // => nullReturns
stringExample
Match a property's indentation in a fix
const text = '{\n "files": ["dist"]\n}'
indentationAt(text, 4) // => ' 'Parameters
| Name | Type | Description |
|---|---|---|
§analysis | ScopeAnalysis | The program's scope analysis |
§identifier | Node | The identifier tested |
Returns
booleanExample
Tell the language's undefined from a parameter named undefined
// program: limit + undefined
// function read(undefined) { return undefined }
isGlobalReference(analysis, undefinedInSum) // => true
isGlobalReference(analysis, undefinedInRead) // => false (the parameter)Parameters
Returns
booleanExample
Tell a wrapped operand from its expression
// file.text: 'const total = (price) * 2'
isParenthesised(program, file, price) // => true
isParenthesised(program, file, init) // => false (the whole BinaryExpression)Returns
unknownExample
Narrow a property value to a string
// { "name": "@acme/ui", "private": true }
isString(propertyOf(manifest, 'name').value) // => true
isString(propertyOf(manifest, 'private').value) // => falseParameters
Returns
stringExample
Read a line without its break
const map = lineMap('first\r\nsecond\n')
lineText(map, 1) // => 'first'
lineText(map, 3) // => '' (the empty line after the last break)Parameters
Returns
booleanExample
Check a path against ignore patterns
const ignores = [compileGlob('dist/'), compileGlob('*.config.ts')]
matchesAny(ignores, 'jest.config.ts') // => true
matchesAny(ignores, 'src/app.ts') // => falseParameters
Returns
FixExample
Fold a keyword change and an annotation removal into one fix
const text = 'let total: any = 0'
mergeEdits(text, [{ start: 9, end: 14, text: '' }, { start: 0, end: 3, text: 'const' }])
// => { start: 0, end: 14, text: 'const total' }parseCode( text: string, extension: string, sourceType: "module" | "script" | "commonjs", declaration: boolean): ParsedCode
Parameters
| Name | Type | Description |
|---|---|---|
§text | string | The source text |
§extension | string | The file's extension with its dot |
§sourceType | "module" | "script" | "commonjs" | Whether the text is a module, a script or a CommonJS module; a script's strings may hold legacy escapes(default: 'module') |
§declaration | boolean | Whether the text is a declaration file, where declarations need no initialiser or body(default: false) |
Returns
ParsedCodeExample
Parse a file, or read why it did not parse
const parsed = parseCode('const total = price * 2 // doubled', '.ts')
parsed.tree.kind // => 'Program'
parsed.comments[0].text // => ' doubled'
parseCode('const = 1', '.ts').error
// => { message: 'Identifier expected.', offset: 6 }Parameters
Returns
PositionExample
Turn an offset into a 1-based position
const map = lineMap('const a = 1\nconst b = 2')
positionAt(map, 0) // => { line: 1, column: 1 }
positionAt(map, 18) // => { line: 2, column: 7 }Parameters
| Name | Type | Description |
|---|---|---|
§object | JsonObjectNode | The node whose properties are searched |
§name | string | The key to look for |
Returns
JsonPropertyNodeExample
Look up a field of package.json
// { "name": "@acme/ui", "files": ["dist"] }
propertyOf(manifest, 'files')?.value.kind // => 'array'
propertyOf(manifest, 'version') // => nullParameters
| Name | Type | Description |
|---|---|---|
§analysis | ScopeAnalysis | The program's scope analysis |
§identifier | Node | The identifier looked up |
Returns
ReferenceExample
Follow a use of a name back to its variable
// program: const limit = 10 / limit + 1 / function check() {}
referenceOf(analysis, limitInSum)?.resolved?.name // => 'limit'
referenceOf(analysis, checkDeclaration.id) // => null (a declaration, not a use)Parameters
| Name | Type | Description |
|---|---|---|
§node | JsonTreeNode | The node a document listener received |
Returns
JsonObjectNodeExample
Start a rule from the document's root object
document(node) {
const manifest = rootObject(node)
if (manifest === null) return // the document is an array or a literal
}Parameters
| Name | Type | Description |
|---|---|---|
§context | RuleContext< | The rule's context |
Returns
ScopeAnalysisExample
Share one analysis across a rule's handlers
const analysis = scopeAnalysisOf(context)
scopeAnalysisOf(context) === analysis // => true, built once per programParameters
| Name | Type | Description |
|---|---|---|
§analysis | ScopeAnalysis | The program's analysis |
§node | Node | The node whose scope is wanted |
Returns
ScopeExample
Find the scope an identifier sits in
// program: 'let count = 0\nfunction bump(step) {\n count += step\n}'
// step: the Identifier read inside bump
scopeOf(analysis, step).kind // => 'function'
scopeOf(analysis, program).kind // => 'global'Returns
JsonStringNode[ ]Example
Collect the strings of a nested value
// "files": ["dist", { "glob": "src" }, 3]
stringsUnder(files.value).map((node) => node.value) // => ['dist', 'src']Parameters
Returns
SourceTokenExample
Find the token after a node
// file.text: 'const total = (price) * 2', price: its Identifier node
tokenText(file, tokenAfter(program, price.end)) // => ')'
tokenAfter(program, file.text.length) // => nullParameters
Returns
SourceTokenExample
Find the token before a node
// file.text: 'const total = (price) * 2', price: its Identifier node
const open = tokenBefore(program, price.start)
tokenText(file, open) // => '('
tokenBefore(program, 0) // => nullParameters
Returns
SourceToken[ ]Example
List the tokens a node spans
// file.text: 'const total = (price) * 2', init: the BinaryExpression
const tokens = tokensBetween(program, init.start, init.end)
tokens.map((token) => tokenText(file, token))
// => ['(', 'price', ')', '*', '2']Parameters
| Name | Type | Description |
|---|---|---|
§file | RuleFile | The file linted |
§token | SourceToken | The token read |
Returns
stringExample
Read a token's characters
// file.text: 'const total = 2'
tokenText(file, program.tokens[1]) // => 'total'Returns
JsonSpanExample
Report a fact about the whole file
context.report({ ...wholeDocument(context.file), message: 'Missing "license" field.' })
// wholeDocument(file) for an 86-character file => { start: 0, end: 86 }◈ Interfaces
Properties
Properties
Properties
Properties
Properties
Methods
Example
A read keeps no write expression
const reference = new Reference(
identifier, scope, 'read', 'value', initialiser, true
)
reference.writeExpr // => null
reference.init // => false
reference.isRead() // => trueProperties
Example
A block scope's var declarations land in the module scope
const global = new Scope('global', program, null, false)
const module = new Scope('module', program, global, true)
const block = new Scope('block', statement, module, true)
global.children.length // => 1
block.variableScope === module // => trueProperties
readonly globalKind:ReferenceKindFor a global: the positions it is valid in; null for a declared variable.get isType:booleanWhether the variable is valid in a type position; a global with fixed positions keeps them whatever a script declares.get isValue:booleanWhether the variable is valid in a value position; a global with fixed positions keeps them whatever a script declares.Example
A global's positions are fixed, a declared variable's follow its definitions
const options = { kind: 'value', writable: false }
const window = new Variable('window', globalScope, options)
window.isType // => false
window.writable // => false
const total = new Variable('total', moduleScope, null)
total.writable // => null (not a global)
total.isType // => true until a definition says otherwise◆ Types
type SourceType = indexedAccess