@hyperfrontend/linter/helpers

helpers

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

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

§function

childrenOf(node: Node): Node[]

The children of a node, in source order.
Parameters
NameTypeDescription
§node
NodeAny node
Returns
Node[]
The child nodes
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)
§function

commentsBetween(file: RuleFile, start: number, end: number): SourceComment[]

The comments starting at or after one offset and ending at or before another.
Parameters
NameTypeDescription
§file
RuleFileThe file linted
§start
numberThe first offset
§end
numberThe last offset
Returns
SourceComment[]
The comments, in source order
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', ... }]
§function

compileGlob(pattern: string): Glob

Compiles a pattern.
Parameters
NameTypeDescription
§pattern
stringThe text as a configuration wrote it
Returns
Glob
The same text with a matcher beside it
Example

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') // => false
§function

findVariable(scope: Scope, name: string): Variable

The variable a name resolves to from a scope, looking outward.
Parameters
NameTypeDescription
§scope
ScopeThe scope looked from
§name
stringThe name looked up
Returns
Variable
The variable, or null when no scope in reach declares it
Example

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') // => null
§function

indentationAt(text: string, offset: number): string

The whitespace that precedes a node on its line.
Parameters
NameTypeDescription
§text
stringThe document
§offset
numberThe node's start
Returns
string
The spaces and tabs between the line start and the node
Example

Match a property's indentation in a fix

const text = '{\n  "files": ["dist"]\n}'
indentationAt(text, 4) // => '  '
§function

isGlobalReference(analysis: ScopeAnalysis, identifier: Node): boolean

Whether an identifier refers to a global: a name the language or the configuration declares, which no declaration of the program shadows, or a name nothing declares at all.
Parameters
NameTypeDescription
§analysis
ScopeAnalysisThe program's scope analysis
§identifier
NodeThe identifier tested
Returns
boolean
Whether the reference resolves to no declaration of the program
Example

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)
§function

isParenthesised(program: Node, file: RuleFile, node: Node): boolean

Whether a node sits directly between an opening and a closing parenthesis.
Parameters
NameTypeDescription
§program
NodeThe program whose tokens are searched
§file
RuleFileThe file linted
§node
NodeThe node looked around
Returns
boolean
Whether the tokens around it are parentheses
Example

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)
§function

isString(node: JsonNode): unknown

Whether a node is a string literal.
Parameters
NameTypeDescription
§node
JsonNodeAny value node
Returns
unknown
True for a string
Example

Narrow a property value to a string

// { "name": "@acme/ui", "private": true }
isString(propertyOf(manifest, 'name').value) // => true
isString(propertyOf(manifest, 'private').value) // => false
§function

lineText(map: LineMap, line: number): string

The text of a line.
Parameters
NameTypeDescription
§map
LineMapThe table lineMap built for the file
§line
numberWhich line, counted from 1
Returns
string
That line's characters without the break that ends it
Example

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)
§function

matchesAny(globs: unknown, path: string): boolean

Whether a path matches any of the patterns.
Parameters
NameTypeDescription
§globs
unknownCompiled patterns
§path
stringA POSIX path relative to the patterns' base
Returns
boolean
True when one matches
Example

Check a path against ignore patterns

const ignores = [compileGlob('dist/'), compileGlob('*.config.ts')]
matchesAny(ignores, 'jest.config.ts') // => true
matchesAny(ignores, 'src/app.ts') // => false
§function

mergeEdits(text: string, edits: unknown): Fix

Folds edits that do not overlap into one replacement.
Parameters
NameTypeDescription
§text
stringThe text the edits apply to
§edits
unknownThe edits, in any order; an insertion has start equal to end
Returns
Fix
One replacement covering them all
Example

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' }
§function

parseCode(text: string, extension: string, sourceType: "module" | "script" | "commonjs", declaration: boolean): ParsedCode

Parses a code file.
Parameters
NameTypeDescription
§text
stringThe source text
§extension
stringThe 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
booleanWhether the text is a declaration file, where declarations need no initialiser or body(default: false)
Returns
ParsedCode
The tree, comments and tokens, or the error
Example

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 }
§function

positionAt(map: LineMap, offset: number): Position

The line and column of an offset.
Parameters
NameTypeDescription
§map
LineMapThe text's line map
§offset
numberAn offset into the text, up to and including its length
Returns
Position
The 1-based position
Example

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 }
§function

propertyOf(object: JsonObjectNode, name: string): JsonPropertyNode

A property of an object by name.
Parameters
NameTypeDescription
§object
JsonObjectNodeThe node whose properties are searched
§name
stringThe key to look for
Returns
JsonPropertyNode
The first property with that name, or null
Example

Look up a field of package.json

// { "name": "@acme/ui", "files": ["dist"] }
propertyOf(manifest, 'files')?.value.kind // => 'array'
propertyOf(manifest, 'version') // => null
§function

referenceOf(analysis: ScopeAnalysis, identifier: Node): Reference

The reference an identifier makes, found through the analysis of its program.
Parameters
NameTypeDescription
§analysis
ScopeAnalysisThe program's scope analysis
§identifier
NodeThe identifier looked up
Returns
Reference
The reference, or null when the identifier makes none
Example

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)
§function

rootObject(node: JsonTreeNode): JsonObjectNode

The root object of a document.
Parameters
NameTypeDescription
§node
JsonTreeNodeThe node a document listener received
Returns
JsonObjectNode
The root when it is an object, or null
Example

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
}
§function

scopeAnalysisOf(context: RuleContext<unknown, Node>): ScopeAnalysis

The scope analysis of the program a rule is linting, built on first request.
Parameters
NameTypeDescription
§context
RuleContext<unknown, Node>The rule's context
Returns
ScopeAnalysis
The analysis, the same object on every request for the program
Example

Share one analysis across a rule's handlers

const analysis = scopeAnalysisOf(context)
scopeAnalysisOf(context) === analysis // => true, built once per program
§function

scopeOf(analysis: ScopeAnalysis, node: Node): Scope

The innermost scope a node sits in: for the program itself the global scope, for any other node the innermost scope its nearest scope-opening ancestor opened, which for a named function expression is the function's scope rather than its name's.
Parameters
NameTypeDescription
§analysis
ScopeAnalysisThe program's analysis
§node
NodeThe node whose scope is wanted
Returns
Scope
The innermost scope enclosing the node
Example

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'
§function

stringsUnder(node: JsonNode): JsonStringNode[]

Every string literal under a node, in source order: the node itself, or the values nested in its objects and arrays.
Parameters
NameTypeDescription
§node
JsonNodeThe node to search
Returns
JsonStringNode[]
The string nodes
Example

Collect the strings of a nested value

// "files": ["dist", { "glob": "src" }, 3]
stringsUnder(files.value).map((node) => node.value) // => ['dist', 'src']
§function

tokenAfter(program: Node, offset: number): SourceToken

The first token starting at or after an offset.
Parameters
NameTypeDescription
§program
NodeThe program whose tokens are searched
§offset
numberThe offset searched from
Returns
SourceToken
The token, or null when none starts there or later
Example

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) // => null
§function

tokenBefore(program: Node, offset: number): SourceToken

The last token ending at or before an offset.
Parameters
NameTypeDescription
§program
NodeThe program whose tokens are searched
§offset
numberThe offset searched back from
Returns
SourceToken
The token, or null when none ends before it
Example

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) // => null
§function

tokensBetween(program: Node, start: number, end: number): SourceToken[]

The tokens starting at or after one offset and ending at or before another.
Parameters
NameTypeDescription
§program
NodeThe program whose tokens are searched
§start
numberThe first offset
§end
numberThe last offset
Returns
SourceToken[]
The tokens, in source order
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']
§function

tokenText(file: RuleFile, token: SourceToken): string

The text of a token.
Parameters
NameTypeDescription
§file
RuleFileThe file linted
§token
SourceTokenThe token read
Returns
string
Its characters
Example

Read a token's characters

// file.text: 'const total = 2'
tokenText(file, program.tokens[1]) // => 'total'
§function

wholeDocument(file: RuleFile): JsonSpan

The span of the whole document, where a rule reports a fact about the file as a whole.
Parameters
NameTypeDescription
§file
RuleFileThe file being linted
Returns
JsonSpan
A problem's start and end
Example

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

§interface

Definition

One declaration of a variable.
Properties
§readonly kind:DefinitionKindWhat declared it.
§readonly name:NodeThe identifier declared, or the string literal naming an enum member.
§readonly node:NodeThe declaring node: the declarator, the function or class, the import specifier, the enum member, the module, the type.
§readonly parent:NodeThe statement the declaring node belongs to, when one exists: the variable or import declaration.
§readonly rest:booleanWhether a parameter is the rest parameter.
§readonly isType:booleanWhether the declaration is valid in a type position.
§readonly isValue:booleanWhether the declaration is valid in a value position.
§interface

Glob

A compiled pattern.
Properties
§readonly pattern:stringThe pattern as written.
§readonly test:(path: string) => booleanWhether a path matches.
§interface

JsonStringNode

A string literal node.
Properties
§readonly start:numberThe offset of the first character.
§readonly end:numberThe offset after the last character.
§readonly kind:"string"Marks the literal as a string.
§readonly value:stringThe decoded text.
§readonly raw:stringThe text as written, quotes and escapes included.
§interface

ParsedCode

What parsing a text produced.
Properties
§readonly tree:NodeThe program, or null when the text did not parse.
§readonly comments:unknownThe comments found, in source order.
§readonly tokens:unknownThe tokens consumed, in source order.
§readonly error:ParseErrorThe error that stopped parsing, or null.
§interface

ParseError

Why a text did not parse.
Properties
§readonly message:stringWhat was wrong.
§readonly offset:numberThe offset where it was found.
§interface

Reference

One use of a name.
Properties
§readonly identifier:NodeThe identifier, or JSX identifier, making the reference; a declaration's own identifier for a JSX pragma reference.
§readonly from:ScopeThe scope the reference was made from.
§resolved:VariableThe variable it resolved to, or null.
§readonly direction:DirectionWhether it reads, writes, or both.
§readonly kind:ReferenceKindWhether it stands in a value position, a type position, or both.
§readonly writeExpr:NodeFor a write: the expression assigned, when one exists.
§readonly init:booleanFor a write: whether it is the declaration's own initialisation.
§get isTypeReference:booleanWhether the reference stands in a type position.
§get isValueReference:booleanWhether the reference stands in a value position.
Methods
§isRead(): boolean
Whether the reference reads.

Returns

boolean
True for a read or a read-write
§isWrite(): boolean
Whether the reference writes.

Returns

boolean
True for a write or a read-write
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() // => true
§interface

Scope

One scope.
Properties
§readonly kind:ScopeKindWhich construct opened it.
§readonly block:NodeThe node that opened it.
§readonly upper:ScopeThe enclosing scope, or null for the global scope.
§readonly children:Scope[]The scopes nested in it, in source order.
§readonly variables:Variable[]The variables it declares, in declaration order.
§readonly set:Map<string, Variable>Its variables by name.
§readonly references:Reference[]The references made from it, in source order.
§readonly through:Reference[]The references that passed through it unresolved.
§readonly variableScope:ScopeThe nearest scope, this one included, that a var declaration lands in.
§isStrict:booleanWhether its code is strict.
§left:Reference[]The references made in it or passed up to it, still to resolve; null once closed.
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 // => true
§interface

ScopeAnalysis

The scopes of one program.
Properties
§readonly global:ScopeThe global scope.
§readonly scopes:unknownEvery scope, in the order opened.
§readonly scopesOf:(block: Node) => unknownThe scopes a node opened, in the order opened; none for most nodes.
§readonly declaredVariables:(node: Node) => unknownThe variables a node declares: a declaration's declarators, a function's name and parameters, an import's bindings.
§interface

Variable

A name declared in a scope, or known to the global scope.
Properties
§readonly name:stringThe name as declared.
§readonly scope:ScopeThe scope that declares it.
§readonly identifiers:Node[]The identifiers that declare it, in source order.
§readonly defs:Definition[]Its declarations.
§readonly references:Reference[]The references that resolved to it, in resolution order.
§writable:booleanFor a global: whether it may be assigned; null for a declared variable.
§readonly globalKind:ReferenceKindFor a global: the positions it is valid in; null for a declared variable.
§used:booleanWhether a rule marked it used.
§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

How a text is read as a module or a script.
type SourceType = indexedAccess