1
0
mirror of https://github.com/sasjs/lint.git synced 2025-12-10 09:34:34 +00:00

Compare commits

...

37 Commits

Author SHA1 Message Date
Allan Bowe
a1ebb51230 Merge pull request #205 from sasjs/default-line-endings
fix: lineEndings should be off by default
2023-02-20 15:50:57 +00:00
496e0bc8fc fix: lineEndings should be off by default 2023-02-20 20:07:07 +05:00
Allan Bowe
f8b15c7d4d fix: updating docs to force release 2023-02-20 14:15:22 +00:00
Allan Bowe
74e8df2a7b Merge pull request #204 from sasjs/issue-17
chore: add documentation for lineEndings rule
2023-02-20 13:31:58 +00:00
Allan Bowe
12e4eeb287 Update README.md 2023-02-20 11:24:20 +00:00
bc7a7a7645 chore: add enum for lineEndings in sasjs-lint-schema.json 2023-02-20 16:10:10 +05:00
Allan Bowe
40e90995f8 chore: change single to double quotes 2023-02-20 09:42:28 +00:00
Allan Bowe
80d0b39637 chore(docs): Added default value in explanatory json 2023-02-20 09:41:31 +00:00
c3a466f485 chore: quick fix 2023-02-20 14:37:04 +05:00
38656e9e89 chore: remove empty header 2023-02-15 21:50:34 +05:00
386d0f5ff3 chore: add documentation for lineEndings rule 2023-02-15 21:34:25 +05:00
Allan Bowe
ad59159b62 Merge pull request #200 from sasjs/issue-47
feat: add new config maxDataLineLength
2023-01-12 17:35:22 +01:00
Allan Bowe
591f498d6d fix: readme 2023-01-12 09:29:44 +00:00
b5b8e7b00b chore: quick fix 2023-01-11 21:34:07 +05:00
7a46e9857e feat: add new config maxDataLineLength 2023-01-11 19:51:07 +05:00
Allan Bowe
985ed41a4b Merge pull request #199 from sasjs/issue-45
feat: add a new config maxHeaderLineLength
2023-01-11 15:46:01 +01:00
fa07a7789c chore: quick fix 2023-01-10 22:47:13 +05:00
9a44984264 chore: quick fix 2023-01-10 20:59:00 +05:00
54f887fc6d fix: maxLineLength rule should be off when non positive number is provided 2023-01-10 20:55:21 +05:00
Allan Bowe
5f0ef8616c Update getHeaderLinesCount.ts 2023-01-10 15:02:50 +00:00
Allan Bowe
04858eab99 chore(docs): readme 2023-01-10 14:51:44 +00:00
Allan Bowe
86fc4b8718 chore(docs): readme updates 2023-01-10 14:48:40 +00:00
fef3eb5503 chore: bump jest related packages 2023-01-10 18:39:29 +05:00
9dca298a2f chore: bump all-contributors-cli and @types/node 2023-01-10 18:31:12 +05:00
3ec75cdbfb chore: bump ignore 2023-01-10 18:20:07 +05:00
20476c557f chore: bump @sasjs/utils 2023-01-10 18:18:12 +05:00
ed96ba092b chore: npm audit fix 2023-01-10 18:10:31 +05:00
Allan Bowe
b8b357c514 Update getHeaderLinesCount.ts 2023-01-10 10:39:32 +00:00
Allan Bowe
701c160ec1 fix: support single asterisk comment headers 2023-01-10 10:37:32 +00:00
e6dc319844 chore: quick fix 2023-01-10 15:32:57 +05:00
b6e9ee0825 feat: add a new config maxHeaderLineLength 2023-01-10 14:48:18 +05:00
Allan Bowe
4cb2fe8a69 Merge pull request #198 from sasjs/issue-197
feat: add a new config attribute for allowedGremlins
2023-01-09 14:59:43 +01:00
Allan Bowe
6c3b716988 feat: adding allowedGremlins description to README 2023-01-09 13:15:47 +00:00
844f1ad154 feat: add a new config attribute for allowedGremlins 2023-01-09 17:13:19 +05:00
Allan Bowe
9984a373df Merge pull request #195 from sasjs/issue-140
fix: update regex to handle single quotes in macro options
2023-01-02 12:21:22 +01:00
0c79a1ef85 fix: update regex to handle single quotes in macro options 2023-01-02 16:11:10 +05:00
Allan Bowe
0bd57489b7 Update README.md 2022-12-30 12:25:45 +00:00
22 changed files with 1644 additions and 9812 deletions

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install Dependencies
run: npm ci
- name: Check Code Style
@@ -21,7 +21,7 @@ jobs:
- name: Build Project
run: npm run build
- name: Semantic Release
uses: cycjimmy/semantic-release-action@v2
uses: cycjimmy/semantic-release-action@v3
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,4 +1,3 @@
[![License](https://img.shields.io/apm/l/atomic-design-ui.svg)](/LICENSE)
![GitHub top language](https://img.shields.io/github/languages/top/sasjs/lint)
[![GitHub closed issues](https://img.shields.io/github/issues-closed-raw/sasjs/lint)](https://github.com/sasjs/lint/issues?q=is%3Aissue+is%3Aclosed)
[![GitHub issues](https://img.shields.io/github/issues-raw/sasjs/lint)](https://github.com/sasjs/lint/issues)
@@ -26,7 +25,10 @@ Configuration is via a `.sasjslint` file with the following structure (these are
"hasMacroParentheses": true,
"ignoreList": ["sajsbuild/", "sasjsresults/"],
"indentationMultiple": 2,
"lineEndings": "off",
"lowerCaseFileNames": true,
"maxDataLineLength": 80,
"maxHeaderLineLength": 80,
"maxLineLength": 80,
"noNestedMacros": true,
"noGremlins": true,
@@ -47,9 +49,22 @@ Each setting can have three states:
For more details, and the default state, see the description of each rule below. It is also possible to change whether a rule returns ERROR or WARN using the `severityLevels` object.
### allowedGremlins
An array of hex codes that represents allowed gremlins (invisible / undesirable characters). To allow all gremlins, you can also set the `noGremlins` rule to `false`.
Example:
```json
{
"noGremlins": true,
"allowedGremlins": ["0x0080", "0x3000"]
}
```
### defaultHeader
This isn't actually a rule - but rather a formatting setting, which applies to SAS program that do NOT begin with `/**`. It can be triggered by running `sasjs lint fix` in the SASjs CLI, or by hitting "save" when using the SASjs VS Code extension (with "formatOnSave" in place)
This isn't a rule, but a formatting setting, which applies to SAS program that do NOT begin with `/**`. It can be triggered by running `sasjs lint fix` in the SASjs CLI, or by hitting "save" when using the SASjs VS Code extension (with "formatOnSave" in place)
The default header is as follows:
@@ -108,6 +123,21 @@ This will check each line to ensure that the count of leading spaces can be divi
- Default: 2
- Severity: WARNING
### lineEndings
This setting ensures the line endings in a file to conform the configured type. Possible values are `lf`, `crlf` and `off` (off means rule is set to be off). If the value is missing, null or undefined then the check would also be switched off (no default applied).
- Default: "off"
- Severity: WARNING
Example (to enforce unix line endings):
```json
{
"lineEndings": "lf"
}
```
### lowerCaseFileNames
On *nix systems, it is imperative that autocall macros are in lowercase. When sharing code between windows and *nix systems, the difference in case sensitivity can also be a cause of lost developer time. For this reason, we recommend that sas filenames are always lowercase.
@@ -115,6 +145,45 @@ On *nix systems, it is imperative that autocall macros are in lowercase. When sh
- Default: true
- Severity: WARNING
### maxDataLineLength
Datalines can be very wide, so to avoid the need to increase `maxLineLength` for the entire project, it is possible to raise the line length limit for the data records only. On a related note, as a developer, you should also be aware that code submitted in batch may have a default line length limit which is lower than you expect. See this [usage note](https://support.sas.com/kb/15/883.html) (and thanks to [sasutils for reminding us](https://github.com/sasjs/lint/issues/47#issuecomment-1064340104)).
This feature will work for the following statements:
- cards
- cards4
- datalines
- datalines4
- parmcards
- parmcards4
The `maxDataLineLength` setting is always the _higher_ of `maxDataLineLength` and `maxLineLength` (if you set a lower number, it is ignored).
- Default: 80
- Severity: WARNING
See also:
- [hasDoxygenHeader](#hasdoxygenheader)
- [maxHeaderLineLength](#maxheaderlinelength)
- [maxLineLength](#maxlinelength)
### maxHeaderLineLength
In a program header it can be necessary to insert items such as URLs or markdown tables, that cannot be split over multiple lines. To avoid the need to increase `maxLineLength` for the entire project, it is possible to raise the line length limit for the header section only.
The `maxHeaderLineLength` setting is always the _higher_ of `maxHeaderLineLength` and `maxLineLength` (if you set a lower number, it is ignored).
- Default: 80
- Severity: WARNING
See also:
- [hasDoxygenHeader](#hasdoxygenheader)
- [maxDataLineLength](#maxdatalinelength)
- [maxLineLength](#maxlinelength)
### maxLineLength
Code becomes far more readable when line lengths are short. The most compelling reason for short line lengths is to avoid the need to scroll when performing a side-by-side 'compare' between two files (eg as part of a GIT feature branch review). A longer discussion on optimal code line length can be found [here](https://stackoverflow.com/questions/578059/studies-on-optimal-code-width)
@@ -126,11 +195,16 @@ We strongly recommend a line length limit, and set the bar at 80. To turn this f
- Default: 80
- Severity: WARNING
See also:
- [maxDataLineLength](#maxdatalinelength)
- [maxHeaderLineLength](#maxheaderlinelength)
### noGremlins
Capture zero-width whitespace and other non-standard characters. The logic is borrowed from the [VSCode Gremlins Extension](https://github.com/nhoizey/vscode-gremlins) - if you are looking for more advanced gremlin zapping capabilities, we highly recommend to use their extension instead.
Capture zero-width whitespace and other non-standard characters. The logic is borrowed from the [VSCode Gremlins Extension](https://github.com/nhoizey/vscode-gremlins) - if you are looking for more advanced gremlin zapping capabilities, we highly recommend to use their extension instead.
The list of characters can be found in this file: [https://github.com/sasjs/lint/blob/main/src/rules/line/noGremlins.ts](https://github.com/sasjs/lint/blob/main/src/rules/line/noGremlins.ts)
The list of characters can be found in this file: [https://github.com/sasjs/lint/blob/main/src/utils/gremlinCharacters.ts](https://github.com/sasjs/lint/blob/main/src/utils/gremlinCharacters.ts)
- Default: true
- Severity: WARNING
@@ -189,11 +263,6 @@ This setting allows the default severity to be adjusted. This is helpful when ru
- "warn" - show warning in the log (doesnt affect exit code)
- "error" - show error in the log (exit code is 1 when triggered)
## Upcoming Linting Rules:
- `noGremlins` -> identifies all invisible characters, other than spaces / tabs / line endings. If you really need that bell character, use a hex literal!
- `lineEndings` -> set a standard line ending, such as LF or CRLF
# SAS Formatter
A formatter will automatically apply rules when you hit SAVE, which can save a LOT of time.

10900
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -39,16 +39,16 @@
},
"homepage": "https://github.com/sasjs/lint#readme",
"devDependencies": {
"@types/jest": "^26.0.23",
"@types/node": "^15.12.2",
"all-contributors-cli": "^6.20.0",
"jest": "^26.6.3",
"@types/jest": "29.2.5",
"@types/node": "18.11.18",
"all-contributors-cli": "6.24.0",
"jest": "29.3.1",
"rimraf": "^3.0.2",
"ts-jest": "^26.5.6",
"ts-jest": "29.0.3",
"typescript": "^4.3.2"
},
"dependencies": {
"@sasjs/utils": "^2.19.0",
"ignore": "^5.2.0"
"@sasjs/utils": "2.52.0",
"ignore": "5.2.4"
}
}

View File

@@ -13,12 +13,14 @@
"indentationMultiple": 2,
"lowerCaseFileNames": true,
"maxLineLength": 80,
"maxHeaderLineLength": 80,
"maxDataLineLength": 80,
"noGremlins": true,
"noNestedMacros": true,
"noSpacesInFileNames": true,
"noTabs": true,
"noTrailingSpaces": true,
"lineEndings": "lf",
"lineEndings": "off",
"strictMacroDefinition": true,
"ignoreList": ["sajsbuild", "sasjsresults"]
},
@@ -30,7 +32,10 @@
"noSpacesInFileNames": true,
"lowerCaseFileNames": true,
"maxLineLength": 80,
"maxHeaderLineLength": 80,
"maxDataLineLength": 80,
"noGremlins": true,
"allowedGremlins": ["0x0080", "0x3000"],
"noTabs": true,
"indentationMultiple": 4,
"hasMacroNameInMend": true,
@@ -74,6 +79,18 @@
"default": [true],
"examples": [true, false]
},
"allowedGremlins": {
"$id": "#/properties/allowedGremlins",
"type": "array",
"items": {
"type": "string",
"pattern": "^0x[0-9A-Fa-f]{4}$"
},
"title": "allowedGremlins",
"description": "An array of hex codes that represents allowed gremlins.",
"default": [],
"examples": ["0x0080", "0x3000"]
},
"hasMacroNameInMend": {
"$id": "#/properties/hasMacroNameInMend",
"type": "boolean",
@@ -114,6 +131,22 @@
"default": 80,
"examples": [60, 80, 120]
},
"maxHeaderLineLength": {
"$id": "#/properties/maxHeaderLineLength",
"type": "number",
"title": "maxHeaderLineLength",
"description": "Enforces a configurable maximum line length for header section. Shows a warning for lines exceeding this length.",
"default": 80,
"examples": [60, 80, 120]
},
"maxDataLineLength": {
"$id": "#/properties/maxDataLineLength",
"type": "number",
"title": "maxDataLineLength",
"description": "Enforces a configurable maximum line length for data section. Shows a warning for lines exceeding this length.",
"default": 80,
"examples": [60, 80, 120]
},
"noNestedMacros": {
"$id": "#/properties/noNestedMacros",
"type": "boolean",
@@ -149,9 +182,10 @@
"lineEndings": {
"$id": "#/properties/lineEndings",
"type": "string",
"enum": ["lf", "crlf", "off"],
"title": "lineEndings",
"description": "Enforces the configured terminating character for each line. Shows a warning when incorrect line endings are present.",
"default": "lf",
"default": "off",
"examples": ["lf", "crlf"]
},
"strictMacroDefinition": {

View File

@@ -1,12 +1,19 @@
import { LintConfig, Diagnostic } from '../types'
import { splitText } from '../utils'
import { LintConfig, Diagnostic, LineLintRuleOptions } from '../types'
import { getHeaderLinesCount, splitText } from '../utils'
import { checkIsDataLine, getDataSectionsDetail } from '../utils'
export const processText = (text: string, config: LintConfig) => {
const lines = splitText(text, config)
const headerLinesCount = getHeaderLinesCount(text, config)
const dataSections = getDataSectionsDetail(text, config)
const diagnostics: Diagnostic[] = []
diagnostics.push(...processContent(config, text))
lines.forEach((line, index) => {
diagnostics.push(...processLine(config, line, index + 1))
const isHeaderLine = index + 1 <= headerLinesCount
const isDataLine = checkIsDataLine(dataSections, index)
diagnostics.push(
...processLine(config, line, index + 1, { isHeaderLine, isDataLine })
)
})
return diagnostics
@@ -36,11 +43,12 @@ const processContent = (config: LintConfig, content: string): Diagnostic[] => {
export const processLine = (
config: LintConfig,
line: string,
lineNumber: number
lineNumber: number,
options: LineLintRuleOptions
): Diagnostic[] => {
const diagnostics: Diagnostic[] = []
config.lineLintRules.forEach((rule) => {
diagnostics.push(...rule.test(line, lineNumber, config))
diagnostics.push(...rule.test(line, lineNumber, config, options))
})
return diagnostics

View File

@@ -110,7 +110,7 @@ const processOptions = (
const severity = config?.severityLevel[name] || Severity.Warning
if (optionsPresent) {
const regex = new RegExp(/="(.*?)"/, 'g')
const regex = new RegExp(/=["|'](.*?)["|']/, 'g')
let result = regex.exec(optionsPresent)

View File

@@ -41,4 +41,44 @@ describe('maxLineLength', () => {
'Prow scuttle parrel provost Sail ho shrouds spirits boom mizzenmast yard'
expect(maxLineLength.test(line, 1)).toEqual([])
})
it('should return an array with a single diagnostic when the line in header section exceeds the specified length', () => {
const line = 'This line is from header section'
const config = new LintConfig({
maxLineLength: 10,
maxHeaderLineLength: 15
})
expect(maxLineLength.test(line, 1, config, { isHeaderLine: true })).toEqual(
[
{
message: `Line exceeds maximum length by ${
line.length - config.maxHeaderLineLength
} characters`,
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
}
]
)
})
it('should return an array with a single diagnostic when the line in data section exceeds the specified length', () => {
const line = 'GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8.'
const config = new LintConfig({
maxLineLength: 10,
maxDataLineLength: 15
})
expect(maxLineLength.test(line, 1, config, { isDataLine: true })).toEqual([
{
message: `Line exceeds maximum length by ${
line.length - config.maxDataLineLength
} characters`,
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
}
])
})
})

View File

@@ -1,15 +1,32 @@
import { LintConfig } from '../../types'
import { LineLintRule } from '../../types/LintRule'
import { LineLintRule, LineLintRuleOptions } from '../../types/LintRule'
import { LintRuleType } from '../../types/LintRuleType'
import { Severity } from '../../types/Severity'
import { DefaultLintConfiguration } from '../../utils'
const name = 'maxLineLength'
const description = 'Restrict lines to the specified length.'
const message = 'Line exceeds maximum length'
const test = (value: string, lineNumber: number, config?: LintConfig) => {
const test = (
value: string,
lineNumber: number,
config?: LintConfig,
options?: LineLintRuleOptions
) => {
const severity = config?.severityLevel[name] || Severity.Warning
const maxLineLength = config?.maxLineLength || 80
let maxLineLength = DefaultLintConfiguration.maxLineLength
if (config) {
if (options?.isHeaderLine) {
maxLineLength = Math.max(config.maxLineLength, config.maxHeaderLineLength)
} else if (options?.isDataLine) {
maxLineLength = Math.max(config.maxLineLength, config.maxDataLineLength)
} else {
maxLineLength = config.maxLineLength
}
}
if (value.length <= maxLineLength) return []
return [
{

View File

@@ -1,5 +1,5 @@
import { Severity } from '../../types/Severity'
import { noGremlins } from './noGremlins'
import { noGremlins, charFromHex } from './noGremlins'
import { LintConfig } from '../../types'
describe('noTabs', () => {
it('should return an empty array when the line does not have any gremlin', () => {
@@ -8,8 +8,19 @@ describe('noTabs', () => {
})
it('should return a diagnostic array when the line contains gremlins', () => {
const line = " %put 'hello';"
const line = `${charFromHex('0x0080')} ${charFromHex(
'0x3000'
)} %put 'hello';`
const diagnostics = noGremlins.test(line, 1)
expect(diagnostics.length).toEqual(2)
})
it('should return an empty array when the line contains gremlins but those gremlins are allowed', () => {
const config = new LintConfig({ allowedGremlins: ['0x0080', '0x3000'] })
const line = `${charFromHex('0x0080')} ${charFromHex(
'0x3000'
)} %put 'hello';`
const diagnostics = noGremlins.test(line, 1, config)
expect(diagnostics.length).toEqual(0)
})
})

View File

@@ -10,15 +10,18 @@ const message = 'Line contains a gremlin'
const test = (value: string, lineNumber: number, config?: LintConfig) => {
const severity = config?.severityLevel[name] || Severity.Warning
const allowedGremlins = config?.allowedGremlins || []
const diagnostics: Diagnostic[] = []
const gremlins: any = {}
for (const [hexCode, config] of Object.entries(gremlinCharacters)) {
gremlins[charFromHex(hexCode)] = Object.assign({}, config, {
hexCode
})
for (const [hexCode, gremlinConfig] of Object.entries(gremlinCharacters)) {
if (!allowedGremlins.includes(hexCode)) {
gremlins[charFromHex(hexCode)] = Object.assign({}, gremlinConfig, {
hexCode
})
}
}
const regexpWithAllChars = new RegExp(
@@ -56,4 +59,5 @@ export const noGremlins: LineLintRule = {
test
}
const charFromHex = (hexCode: string) => String.fromCodePoint(parseInt(hexCode))
export const charFromHex = (hexCode: string) =>
String.fromCodePoint(parseInt(hexCode))

View File

@@ -1,4 +1,5 @@
export enum LineEndings {
LF = 'lf',
CRLF = 'crlf'
CRLF = 'crlf',
OFF = 'off'
}

View File

@@ -31,6 +31,28 @@ describe('LintConfig', () => {
).toBeUndefined()
})
it('should create an instance with the maxLineLength flag off by setting value to 0', () => {
const config = new LintConfig({ maxLineLength: 0 })
expect(config).toBeTruthy()
expect(config.lineLintRules.length).toBeGreaterThan(0)
expect(config.fileLintRules.length).toBeGreaterThan(0)
expect(
config.lineLintRules.find((rule) => rule.name === 'maxLineLength')
).toBeUndefined()
})
it('should create an instance with the maxLineLength flag off by setting value to a negative number', () => {
const config = new LintConfig({ maxLineLength: -1 })
expect(config).toBeTruthy()
expect(config.lineLintRules.length).toBeGreaterThan(0)
expect(config.fileLintRules.length).toBeGreaterThan(0)
expect(
config.lineLintRules.find((rule) => rule.name === 'maxLineLength')
).toBeUndefined()
})
it('should create an instance with the hasDoxygenHeader flag off', () => {
const config = new LintConfig({ hasDoxygenHeader: false })
@@ -146,7 +168,8 @@ describe('LintConfig', () => {
hasMacroNameInMend: true,
noNestedMacros: true,
hasMacroParentheses: true,
noGremlins: true
noGremlins: true,
lineEndings: 'lf'
})
expect(config).toBeTruthy()

View File

@@ -29,10 +29,13 @@ import { Severity } from './Severity'
*/
export class LintConfig {
readonly ignoreList: string[] = []
readonly allowedGremlins: string[] = []
readonly lineLintRules: LineLintRule[] = []
readonly fileLintRules: FileLintRule[] = []
readonly pathLintRules: PathLintRule[] = []
readonly maxLineLength: number = 80
readonly maxHeaderLineLength: number = 80
readonly maxDataLineLength: number = 80
readonly indentationMultiple: number = 2
readonly lineEndings: LineEndings = LineEndings.LF
readonly defaultHeader: string = getDefaultHeader()
@@ -66,13 +69,20 @@ export class LintConfig {
this.lineLintRules.pop()
}
this.lineLintRules.push(maxLineLength)
if (!isNaN(json?.maxLineLength)) {
if (json?.maxLineLength > 0) {
this.lineLintRules.push(maxLineLength)
this.maxLineLength = json.maxLineLength
if (!isNaN(json?.maxHeaderLineLength)) {
this.maxHeaderLineLength = json.maxHeaderLineLength
}
if (!isNaN(json?.maxDataLineLength)) {
this.maxDataLineLength = json.maxDataLineLength
}
}
this.fileLintRules.push(lineEndings)
if (json?.lineEndings) {
if (json?.lineEndings && json.lineEndings !== LineEndings.OFF) {
if (
json.lineEndings !== LineEndings.LF &&
json.lineEndings !== LineEndings.CRLF
@@ -81,6 +91,7 @@ export class LintConfig {
`Invalid value for lineEndings: can be ${LineEndings.LF} or ${LineEndings.CRLF}`
)
}
this.fileLintRules.push(lineEndings)
this.lineEndings = json.lineEndings
}
@@ -123,6 +134,23 @@ export class LintConfig {
if (json?.noGremlins !== false) {
this.lineLintRules.push(noGremlins)
if (json?.allowedGremlins) {
if (Array.isArray(json.allowedGremlins)) {
json.allowedGremlins.forEach((item: any) => {
if (typeof item === 'string' && /^0x[0-9a-f]{4}$/i.test(item))
this.allowedGremlins.push(item)
else
throw new Error(
`Property "allowedGremlins" has invalid type of values. It can contain only strings of form hexcode like '["0x0080", "0x3000"]'`
)
})
} else {
throw new Error(
`Property "allowedGremlins" can only be an array of strings of form hexcode like '["0x0080", "0x3000"]'`
)
}
}
}
if (json?.severityLevel) {

View File

@@ -13,12 +13,22 @@ export interface LintRule {
message: string
}
export interface LineLintRuleOptions {
isHeaderLine?: boolean
isDataLine?: boolean
}
/**
* A LineLintRule is run once per line of text.
*/
export interface LineLintRule extends LintRule {
type: LintRuleType.Line
test: (value: string, lineNumber: number, config?: LintConfig) => Diagnostic[]
test: (
value: string,
lineNumber: number,
config?: LintConfig,
options?: LineLintRuleOptions
) => Diagnostic[]
fix?: (value: string, config?: LintConfig) => string
}

View File

@@ -0,0 +1,113 @@
import { LintConfig } from '../types'
import { getDataSectionsDetail, checkIsDataLine } from './getDataSectionsDetail'
import { DefaultLintConfiguration } from './getLintConfig'
const datalines = `GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32. OPERATOR_NM:$10. RAW_VALUE:$4000.
AND,AND,1,LIBREF,CONTAINS,"'DC'"
AND,OR,2,DSN,=,"'MPE_LOCK_ANYTABLE'"`
const datalinesBeginPattern1 = `datalines;`
const datalinesBeginPattern2 = `datalines4;`
const datalinesBeginPattern3 = `cards;`
const datalinesBeginPattern4 = `cards4;`
const datalinesBeginPattern5 = `parmcards;`
const datalinesBeginPattern6 = `parmcards4;`
const datalinesEndPattern1 = `;`
const datalinesEndPattern2 = `;;;;`
describe('getDataSectionsDetail', () => {
const config = new LintConfig(DefaultLintConfiguration)
it(`should return the detail of data section when it begins and ends with '${datalinesBeginPattern1}' and '${datalinesEndPattern1}' markers`, () => {
const text = `%put hello\n${datalinesBeginPattern1}\n${datalines}\n${datalinesEndPattern1}\n%put world;`
expect(getDataSectionsDetail(text, config)).toEqual([
{
start: 1,
end: 5
}
])
})
it(`should return the detail of data section when it begins and ends with '${datalinesBeginPattern2}' and '${datalinesEndPattern2}' markers`, () => {
const text = `%put hello\n${datalinesBeginPattern2}\n${datalines}\n${datalinesEndPattern2}\n%put world;`
expect(getDataSectionsDetail(text, config)).toEqual([
{
start: 1,
end: 5
}
])
})
it(`should return the detail of data section when it begins and ends with '${datalinesBeginPattern3}' and '${datalinesEndPattern1}' markers`, () => {
const text = `%put hello\n${datalinesBeginPattern3}\n${datalines}\n${datalinesEndPattern1}\n%put world;`
expect(getDataSectionsDetail(text, config)).toEqual([
{
start: 1,
end: 5
}
])
})
it(`should return the detail of data section when it begins and ends with '${datalinesBeginPattern4}' and '${datalinesEndPattern2}' markers`, () => {
const text = `%put hello\n${datalinesBeginPattern4}\n${datalines}\n${datalinesEndPattern2}\n%put world;`
expect(getDataSectionsDetail(text, config)).toEqual([
{
start: 1,
end: 5
}
])
})
it(`should return the detail of data section when it begins and ends with '${datalinesBeginPattern5}' and '${datalinesEndPattern1}' markers`, () => {
const text = `%put hello\n${datalinesBeginPattern5}\n${datalines}\n${datalinesEndPattern1}\n%put world;`
expect(getDataSectionsDetail(text, config)).toEqual([
{
start: 1,
end: 5
}
])
})
it(`should return the detail of data section when it begins and ends with '${datalinesBeginPattern6}' and '${datalinesEndPattern2}' markers`, () => {
const text = `%put hello\n${datalinesBeginPattern6}\n${datalines}\n${datalinesEndPattern2}\n%put world;`
expect(getDataSectionsDetail(text, config)).toEqual([
{
start: 1,
end: 5
}
])
})
})
describe('checkIsDataLine', () => {
const config = new LintConfig(DefaultLintConfiguration)
it(`should return true if a line index is in a range of any data section`, () => {
const text = `%put hello\n${datalinesBeginPattern1}\n${datalines}\n${datalinesEndPattern1}\n%put world;`
expect(
checkIsDataLine(
[
{
start: 1,
end: 5
}
],
4
)
).toBe(true)
})
it(`should return false if a line index is not in a range of any of data sections`, () => {
const text = `%put hello\n${datalinesBeginPattern1}\n${datalines}\n${datalinesEndPattern1}\n%put world;`
expect(
checkIsDataLine(
[
{
start: 1,
end: 5
}
],
8
)
).toBe(false)
})
})

View File

@@ -0,0 +1,58 @@
import { LintConfig } from '../types'
import { splitText } from './splitText'
interface DataSectionsDetail {
start: number
end: number
}
export const getDataSectionsDetail = (text: string, config: LintConfig) => {
const dataSections: DataSectionsDetail[] = []
const lines = splitText(text, config)
const dataSectionStartRegex1 = new RegExp(
'^(datalines;)|(cards;)|(parmcards;)'
)
const dataSectionEndRegex1 = new RegExp(';')
const dataSectionStartRegex2 = new RegExp(
'^(datalines4;)|(cards4;)|(parmcards4;)'
)
const dataSectionEndRegex2 = new RegExp(';;;;')
let dataSectionStarted = false
let dataSectionStartIndex = -1
let dataSectionEndRegex = dataSectionEndRegex1
lines.forEach((line, index) => {
if (dataSectionStarted) {
if (dataSectionEndRegex.test(line)) {
dataSections.push({ start: dataSectionStartIndex, end: index })
dataSectionStarted = false
}
} else {
if (dataSectionStartRegex1.test(line)) {
dataSectionStarted = true
dataSectionStartIndex = index
dataSectionEndRegex = dataSectionEndRegex1
} else if (dataSectionStartRegex2.test(line)) {
dataSectionStarted = true
dataSectionStartIndex = index
dataSectionEndRegex = dataSectionEndRegex2
}
}
})
return dataSections
}
export const checkIsDataLine = (
dataSections: DataSectionsDetail[],
lineIndex: number
) => {
for (const dataSection of dataSections) {
if (lineIndex >= dataSection.start && lineIndex <= dataSection.end)
return true
}
return false
}

View File

@@ -0,0 +1,21 @@
import { LintConfig } from '../types'
import { getHeaderLinesCount } from './getHeaderLinesCount'
import { DefaultLintConfiguration } from './getLintConfig'
const sasCodeWithHeader = `/**
@file
@brief <Your brief here>
<h4> SAS Macros </h4>
**/
%put hello world;
`
const sasCodeWithoutHeader = `%put hello world;`
describe('getHeaderLinesCount', () => {
it('should return the number of line header spans upon', () => {
const config = new LintConfig(DefaultLintConfiguration)
expect(getHeaderLinesCount(sasCodeWithHeader, config)).toEqual(5)
expect(getHeaderLinesCount(sasCodeWithoutHeader, config)).toEqual(0)
})
})

View File

@@ -0,0 +1,23 @@
import { LintConfig } from '../types'
import { splitText } from './splitText'
/**
* This function returns the number of lines the header spans upon.
* The file must start with "/*" and the header will finish with ⇙
*/
export const getHeaderLinesCount = (text: string, config: LintConfig) => {
let count = 0
if (text.trimStart().startsWith('/*')) {
const lines = splitText(text, config)
for (const line of lines) {
count++
if (line.match(/\*\//)) {
break
}
}
}
return count
}

View File

@@ -2,7 +2,7 @@ import * as fileModule from '@sasjs/utils/file'
import { LintConfig } from '../types/LintConfig'
import { getLintConfig } from './getLintConfig'
const expectedFileLintRulesCount = 6
const expectedFileLintRulesCount = 5
const expectedLineLintRulesCount = 6
const expectedPathLintRulesCount = 2

View File

@@ -2,6 +2,7 @@ import path from 'path'
import { LintConfig } from '../types/LintConfig'
import { readFile } from '@sasjs/utils/file'
import { getProjectRoot } from './getProjectRoot'
import { LineEndings } from '../types/LineEndings'
export const getDefaultHeader = () =>
`/**{lineEnding} @file{lineEnding} @brief <Your brief here>{lineEnding} <h4> SAS Macros </h4>{lineEnding}**/`
@@ -10,12 +11,15 @@ export const getDefaultHeader = () =>
* Default configuration that is used when a .sasjslint file is not found
*/
export const DefaultLintConfiguration = {
lineEndings: LineEndings.OFF,
noTrailingSpaces: true,
noEncodedPasswords: true,
hasDoxygenHeader: true,
noSpacesInFileNames: true,
lowerCaseFileNames: true,
maxLineLength: 80,
maxHeaderLineLength: 80,
maxDataLineLength: 80,
noTabIndentation: true,
indentationMultiple: 2,
hasMacroNameInMend: true,

View File

@@ -6,3 +6,5 @@ export * from './isIgnored'
export * from './listSasFiles'
export * from './splitText'
export * from './getIndicesOf'
export * from './getHeaderLinesCount'
export * from './getDataSectionsDetail'