mcpField
S
sasjs-lint

sasjs-lint

github.com/sasjsTesting & quality3657 installsUpdated 2d ago
Repository

About sasjs-lint

sasjs-lint is a skill reference for linting and formatting SAS code with @sasjs/lint. It explains configuration, rules, formatter behavior, and integrations across the CLI, VS Code extension, Server Studio, and library API.

Key features

  1. 01Lint rules for SAS source files
  2. 02Project and per-file configuration
  3. 03Severity settings and diagnostic behavior
  4. 04Formatter for supported automatic fixes
  5. 05Use through CLI, VS Code, Server Studio, or API

Use cases

  1. 01Configure .sasjslint for a SAS project
  2. 02Understand or adjust a lint warning
  3. 03Add linting or formatting to an editor workflow or CI

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/sasjs/skills --skill sasjs-lint

After installing, type/sasjs-lintin Claude Code to confirm it loaded.

Details

From the project README

@sasjs/lint

@sasjs/lint is the linting and formatting engine for SAS code. It is a TypeScript library with no SAS dependency - it reads .sas files as text - and it is the single engine behind sasjs lint, the SASjs VS Code extension's Problems panel, and the SASjs Server Studio editor.

This skill covers the rules, the .sasjslint file, the formatter, and the ways to invoke it. It does not cover the SAS language itself (see sas) or the rest of the CLI (see sasjs-cli).

The rules and defaults below were checked against @sasjs/lint 4.5.0, the version bundled by @sasjs/cli 6.2.5. The library gains rules over time, so a surface that pins an older copy will not have all of them - see Where it runs.

When to use

  • Writing or changing a .sasjslint file
  • A sasjs lint run reports a warning and you need to know what it means or how to silence it
  • Adding lint to a CI pipeline or a git pre-commit hook
  • Running the formatter (sasjs lint fix, format-on-save, formatText)
  • Explaining why a rule does or does not fire in an editor

Where it runs

SurfaceHow it is invokedNotes
SASjs CLIsasjs lint, sasjs lint find, sasjs lint fix, sasjs lint initfind is the default subcommand. init writes a .sasjslint at the project root holding the full default config
VS Code extension (sasjs/sasjs-for-vscode, published on Open VSX)Problems panel (ctrl+shift+M), Format Document, format-on-saveConfig precedence: project .sasjslint > setting sasjs-for-vscode.lintConfig > ~/.sasjslint > defaults
SASjs Server StudioPOST /SASjsApi/code/lint and POST /SASjsApi/code/format, body { code, filePath }Rules come from the nearest .sasjslint at or above the file's folder on the SASjs Drive, walking up to the drive root. A drive-root .sasjslint is seeded on first start and shows in the file tree as /.sasjslint
LibrarylintText, lintFile, formatText, formatFile, and the folder/project variantsSee the API section below

All four call the same rules. The CLI, extension and server each pin a version of @sasjs/lint, so a rule added to the library only appears in a surface once that surface bumps its dependency - check the pinned version before telling a user a rule should fire in their editor.

Config resolution

When no configuration is passed in, the library resolves it in this order:

  1. .sasjslint at the project root (the library walks up to find the project)
  2. .sasjslint in the user's home directory
  3. DefaultLintConfiguration - the built-in defaults

A setting that is absent takes its default, and so does a project with no .sasjslint at all. DefaultLintConfiguration is exported from @sasjs/lint/utils/getLintConfig, and it is what sasjs lint init writes out.

The .sasjslint file

{
  "noTrailingSpaces": true,
  "maxLineLength": 100,
  "noSingleAsteriskComments": true,
  "severityLevel": {
    "hasDoxygenHeader": "warn",
    "noEncodedPasswords": "error"
  }
}

Rules and defaults

Two groups, and the way you switch a rule off depends on which group it is in.

On by default - switch off with false (except where noted):

SettingDefaultSeverityWhat it checks
hasDoxygenHeadertrueWARNINGthe file begins with a /** Doxygen header
hasMacroNameInMendtrueWARNING%mend carries the macro name
hasMacroParenthesestrueWARNINGmacros are defined with parentheses, and no space before the (
noNestedMacrostrueWARNINGno %macro defined inside another macro
strictMacroDefinitiontrueWARNINGno space in a parameter name (%macro m(my var);), no unrecognised option (%macro m()/nonsense;)
noUndeclaredMacrostrueWARNINGevery macro a file calls is declared in the file or its header
noUnusedMacrostrueWARNINGevery macro in <h4> SAS Macros </h4> is actually called
noNestedCommentsInHeadertrueWARNINGthe opening Doxygen header holds no */, which SAS reads as the end of the comment - anything after it, @param lines and usage examples included, is compiled as SAS
noBannedStringstrueWARNINGno configured string on a line, by default the log-lookalike words error , error:, warning , warning:. Case-insensitive, comments included
noTrailingSpacestrueWARNINGno line ends in whitespace
noTabstrueWARNINGno tab characters. Alias: noTabIndentation (deprecated)
noGremlinstrueWARNINGno zero-width or otherwise non-standard characters
indentationMultiple2WARNINGleading spaces divide cleanly by this number
lowerCaseFileNamestrueWARNINGfilenames are lowercase (autocall on *nix requires it)
noSpacesInFileNamestrueWARNINGfilenames contain no spaces
noEncodedPasswordstrueERRORno {sas00X} or {sasenc} encoded password
maxLineLength80WARNINGline length. Off with 0

Off by default - switch on with true:

SettingDefaultSeverityWhat it checks
noSingleAsteriskCommentsfalseWARNINGcomment statements of the form * text;
noUnusedLibnamesfalseWARNINGa libref the file assigns and never uses
hasRequiredMacroOptionsfalseWARNINGmacros carry the options in requiredMacroOptions
lineEndings"off"WARNINGline endings conform to lf or crlf

Supporting settings (not rules):

SettingDefaultPurpose
ignoreList[]paths to skip. Files in .gitignore are skipped too
allowedGremlins[]4-hex-digit strings such as "0x0080"
defaultHeadersee belowheader template applied to a file with no /** header
severityLevel{}per-rule override of "warn" / "error"
maxHeaderLineLength80line limit inside the header
maxDataLineLength80line limit inside datalines / cards / parmcards records
requiredMacroOptions[]options required by hasRequiredMacroOptions
ignoredLibnames[]librefs noUnusedLibnames leaves alone
ignoredMacros["webout"]macro names noUndeclaredMacros treats as declared. Replaces the default when set - see below
bannedStrings["error ", "error:", "warning ", "warning:"]strings noBannedStrings reports. Replaces the default when set - see below

Severity and exit codes

Every rule has three states: off, WARNING, or ERROR. sasjs lint returns a non-zero exit code only when an ERROR is reported - warnings leave the exit code at 0, so a hook or pipeline keyed on the exit code passes a file that only warns. noEncodedPasswords is the only rule that ships as ERROR.

severityLevel promotes or demotes a rule by name, and accepts only "warn" and "error"; any other value is ignored without complaint:

{
  "severityLevel": {
    "maxLineLength": "error",
    "hasDoxygenHeader": "warn"
  }
}

Per-file overrides

A file adjusts the rules that apply to itself with a @sasjslint block in its Doxygen header:

/**
  @file
  @brief Calls the settlement API

  @sasjslint {"maxHeaderLineLength": 200, "noUndeclaredMacros": false}

  <h4> SAS Macros </h4>
  @li mf_trim.sas

**/
  • Scalars replace, arrays are additive (allowedGremlins, requiredMacroOptions, ignoredLibnames gain entries rather than replacing), objects merge key by key (severityLevel).
  • The override applies to the formatter as well, so sasjs lint fix honours it - a file that switches noUnusedMacros off keeps the header entries it would otherwise lose.
  • Only the header is read, and only a line that starts with the tag followed immediately by {. A malformed or unterminated block is ignored rather than raised.
  • The header wins over the configuration the caller passes.
  • ignoreList is the one setting a header cannot use: it is evaluated before the file is read.
  • Place the block outside the <h4> SAS Macros </h4> section, which the formatter rewrites.

ignoredMacros and bannedStrings

Both are lists whose project-level value replaces the built-in default rather than extending it: an explicit ignoredMacros drops webout unless it is restated, and "bannedStrings": [] reports nothing at all. A per-file @sasjslint block is the opposite - arrays are additive there, so a file adds entries to the project's list without restating them.

ignoredMacros is read by noUndeclaredMacros only. It does nothing for noUnusedMacros, so the mirror-image warning - a macro declared in the header and never called - cannot be silenced that way; remove the entry or switch that rule off for the file.

Why ignoredMacros exists: macros that arrive through a provider

The linter reads one file plus the config. It knows nothing about the rest of the build, and sasjs lint passes it nothing from sasjsconfig.json. A macro therefore has to be declared in the file that calls it, even when the compile makes it available some other way:

  • a serviceInit program (serviceInit in the target config, compiled into every service) declares macro files in its own <h4> SAS Macros </h4> - a service body may call those macros without declaring them, and is warned once per call;
  • a testSetUp / testInit program, the same shape for sasjs test;
  • whatever the compiler injects itself: %webout is the built-in case, which is why webout is in the default list.

For those, the project .sasjslint is the right place:

{
  "ignoredMacros": ["webout", "mpeinit", "mpeterm"]
}

Two cautions. Restate webout, since the list replaces the default and dropping it reports %webout in every service that streams. And a project-wide entry also silences the check in files where the macro is genuinely missing, so list only a macro that reaches every file which may call it; a file-local @sasjslint {"ignoredMacros": [...]} is the tighter option when the provider covers a few files rather than all of them.

bannedStrings

The four defaults are words that look like a log message: a comment or a %put holding one turns up in a grep of the SAS log, and in any log scanner built on one, so a real problem is easy to lose among matches that were only prose. The match is case-insensitive, runs longest-string-first without double-reporting overlapping spans, and skips two things:

  • datalines / cards / parmcards records, where the text is data rather than code;
  • the line carrying the @sasjslint block itself, so a file may configure a banned string in its own override.

Reword prose. When the text has to survive verbatim in the log, split the word with a macro function: %put %str(WARN)ING: message prints WARNING: message and no longer holds the literal. A doxygen @warning tag is content rather than prose, so that file wants @sasjslint {"noBannedStrings": false}.

The formatter

The formatter applies the fixable rules on save. Implemented today:

  • add the macro name to %mend
  • add a Doxygen header template when the file has none
  • add macros a file uses to <h4> SAS Macros </h4>, de-duplicated and sorted
  • remove macros from that section that the file does not use
  • remove trailing spaces

Not implemented (do not promise these): tabs to spaces, gremlin removal, line-ending correction, converting single-asterisk comments to block comments.

defaultHeader uses {lineEnding} as the newline placeholder - a literal \n does not work:

{
  "defaultHeader": "/**{lineEnding}  @file{lineEnding}  @brief Our Company Brief{lineEnding}**/"
}

Library API

import { lintText, lintFile, formatText, formatFile, LintConfig, Severity } from '@sasjs/lint'

const diagnostics = await lintText(code, config)   // config optional
const fixed = await formatText(code, config)       // config optional
  • lintText(text, configuration?), lintFile(path, configuration?), lintFolder, lintProject
  • formatText(text, configuration?), formatFile, formatFolder, formatProject
  • new LintConfig(json) builds a config from a plain object; config.override(json) returns a new config with an override merged in
  • Diagnostic carries message, lineNumber, startColumnNumber, endColumnNumber and severity
  • Severity is Info (0), Warning (1), Error (2)

Passing an explicit LintConfig matters when the caller knows the rules - a server or editor linting a buffer with no path on disk cannot rely on the working-directory walk.

Pitfalls

  • Off means something different per rule. The on-by-default rules switch off with false; maxLineLength switches off with 0; lineEndings switches off with "off"; the off-by-default rules ignore false (it is the same as leaving them out) and need true. Getting this wrong is the most common reason a rule "does not work".
  • maxHeaderLineLength and maxDataLineLength are inert when maxLineLength is off, because they are scoped to it. The effective limit is the higher of the two values, so setting one lower than maxLineLength is silently ignored.
  • An explicit list replaces the default, for ignoredMacros and bannedStrings alike. The default is seeded only when the setting is absent, so "ignoredMacros": ["mpeinit"] stops ignoring webout, and "bannedStrings": [] reports nothing rather than falling back to the defaults. Restate the defaults you still want.
  • noUnusedMacros plus the formatter removes @li dependencies. Those entries are consumed by the compiler, so a macro referenced only indirectly - through a dynamic %&name call, or supplied by an %included program - loses its declaration and the job can fail to assemble. Switch the rule off for the project or the file when that applies.
  • A header entry can be load-bearing without the file calling the macro, so verify before pruning. The compiler also inlines whatever another declared macro's own <h4> asks for, so an entry may be a duplicate of that chain (safe to drop, and the generated program does not change) or the only route by which the macro reaches the file (dropping it breaks the build). The check is to compile and grep the artefact: sasjs compile then grep -c "%macro <name>" sasjsbuild/services/<path>.sas before and after the edit.
  • noUndeclaredMacros does not see SASAUTOS. Macros that ship with SAS are always treated as declared (the list is generated from @sasjs/sas-language), but macros made available through SASAUTOS are invisible to the linter and are reported. Declare them in the header or switch the rule off.
  • noBannedStrings reports the word in a comment or in header prose, not only in code. A file can read as clean and still trip it, and the [line, column] points at the word itself. See bannedStrings for the three options - reword, %str(WARN)ING, or a per-file override for a doxygen tag.
  • noUnusedLibnames is deliberately generous. A libref counts as used if it appears anywhere outside a LIBNAME statement - including inside a double-quoted string, as in pathname("outData", "L"). It is off by default because a file is not a complete job, and it has no formatter fix.
  • An empty .sasjslint is not the same as no .sasjslint only for settings whose default was previously applied conditionally. Since 4.0.0 an absent maxLineLength or hasMacroNameInMend falls back to its documented default (80, and true), so a project that relied on omitting them to disable those rules must now set "maxLineLength": 0 or "hasMacroNameInMend": false.
  • allowedGremlins entries are strings, not numbers: "0x0080", exactly four hex digits.
  • The CLI output is a box-drawn table. It is meant for a terminal; do not paste it into a plain-text or ASCII-only document without expecting the border glyphs to be a problem.
  • sasjs lint with no subcommand behaves as sasjs lint find, linting every .sas file in the project.

Verification

  • Confirm a rule fires: put the offending construct in a .sas file and run terminal(command="npx sasjs lint"). A hit prints a row with the message and a [line, column] position; no row means the rule is off or the construct is excluded (check the config first, then the rule's exclusions).
  • Confirm a config change took effect: run npx sasjs lint before and after editing .sasjslint, and compare the rows. A rule that is off by default needs true, not false.
  • Confirm the formatter's effect: run npx sasjs lint fix and inspect the diff - the <h4> SAS Macros </h4> section and %mend lines are the ones it rewrites.
  • Confirm a per-file override: add the @sasjslint block, run sasjs lint, and check that the rule it switches off no longer reports for that file while still reporting for its neighbours.
  • Confirm a banned string: put %put WARNING: test; in a file and run sasjs lint - a row appears at that line and column. Rewrite it as %put %str(WARN)ING: test; and the row goes while the log line still reads WARNING: test.
  • Confirm ignoredMacros: lint a file that calls a macro it does not declare, add the name to the project .sasjslint, and the row disappears. Then remove webout from that same list and confirm %webout starts reporting in a service that calls it - the list replaces the default rather than extending it.

Comments

Markdown supported