mirror of
https://github.com/sasjs/lint.git
synced 2025-12-10 17:34:36 +00:00
feat(lint): implement v1 with 3 rules - trailing spaces, encoded passwords and Doxygen header
This commit is contained in:
9
.github/reviewer-lottery.yml
vendored
Normal file
9
.github/reviewer-lottery.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
groups:
|
||||||
|
- name: SASjs Devs # name of the group
|
||||||
|
reviewers: 1 # how many reviewers do you want to assign?
|
||||||
|
usernames: # github usernames of the reviewers
|
||||||
|
- krishna-acondy
|
||||||
|
- YuryShkoda
|
||||||
|
- saadjutt01
|
||||||
|
- medjedovicm
|
||||||
|
- allanbowe
|
||||||
13
.github/workflows/assign-reviewer.yml
vendored
Normal file
13
.github/workflows/assign-reviewer.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
name: Assign Reviewer
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: uesteibar/reviewer-lottery@v1
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GH_TOKEN }}
|
||||||
33
.github/workflows/build.yml
vendored
Normal file
33
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||||
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||||
|
|
||||||
|
name: SASjs Lint Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [12.x]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: npm ci
|
||||||
|
- name: Check Code Style
|
||||||
|
run: npm run lint
|
||||||
|
- name: Run Unit Tests
|
||||||
|
run: npm test
|
||||||
|
- name: Build Package
|
||||||
|
run: npm run package:lib
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
27
.github/workflows/publish.yml
vendored
Normal file
27
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
|
||||||
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
|
||||||
|
|
||||||
|
name: SASjs Lint Build and Publish
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: npm ci
|
||||||
|
- name: Check Code Style
|
||||||
|
run: npm run lint
|
||||||
|
- name: Build Project
|
||||||
|
run: npm run build
|
||||||
|
- name: Semantic Release
|
||||||
|
uses: cycjimmy/semantic-release-action@v2
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -102,3 +102,6 @@ dist
|
|||||||
|
|
||||||
# TernJS port file
|
# TernJS port file
|
||||||
.tern-port
|
.tern-port
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
build
|
||||||
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"trailingComma": "none",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
5
.sasjslint
Normal file
5
.sasjslint
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"noTrailingSpaces": true,
|
||||||
|
"noEncodedPasswords": true,
|
||||||
|
"hasDoxygenHeader": true
|
||||||
|
}
|
||||||
13
jest.config.js
Normal file
13
jest.config.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
coverageThreshold: {
|
||||||
|
global: {
|
||||||
|
branches: 80,
|
||||||
|
functions: 80,
|
||||||
|
lines: 80,
|
||||||
|
statements: -10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
collectCoverageFrom: ['src/**/{!(index|example),}.ts']
|
||||||
|
}
|
||||||
4905
package-lock.json
generated
Normal file
4905
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
48
package.json
Normal file
48
package.json
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"name": "@sasjs/lint",
|
||||||
|
"description": "Linting and formatting for SAS code",
|
||||||
|
"scripts": {
|
||||||
|
"test": "jest --coverage",
|
||||||
|
"build": "rimraf build && tsc",
|
||||||
|
"prepublishOnly": "cp -r ./build/* . && rm -rf ./build && rm -rf ./src && rm tsconfig.json",
|
||||||
|
"postpublish": "git clean -fd",
|
||||||
|
"package:lib": "npm run build && cp ./package.json build && cp README.md build && cd build && npm version \"5.0.0\" && npm pack",
|
||||||
|
"lint:fix": "npx prettier --write '{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}'",
|
||||||
|
"lint": "npx prettier --check '{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}'"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"branches": [
|
||||||
|
"main"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/sasjs/lint.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"sas",
|
||||||
|
"SASjs",
|
||||||
|
"lint",
|
||||||
|
"formatting"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/sasjs/lint/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/sasjs/lint#readme",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/jest": "^26.0.21",
|
||||||
|
"@types/node": "^14.14.35",
|
||||||
|
"jest": "^26.6.3",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"ts-jest": "^26.5.4",
|
||||||
|
"typescript": "^4.2.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sasjs/utils": "^2.9.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/example.ts
Normal file
45
src/example.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { lint } from './lint'
|
||||||
|
|
||||||
|
const text = `/**
|
||||||
|
@file
|
||||||
|
@brief Returns an unused libref
|
||||||
|
@details Use as follows:
|
||||||
|
|
||||||
|
libname mclib0 (work);
|
||||||
|
libname mclib1 (work);
|
||||||
|
libname mclib2 (work);
|
||||||
|
|
||||||
|
%let libref=%mf_getuniquelibref({SAS001});
|
||||||
|
%put &=libref;
|
||||||
|
|
||||||
|
which returns:
|
||||||
|
|
||||||
|
> mclib3
|
||||||
|
|
||||||
|
@param prefix= first part of libref. Remember that librefs can only be 8 characters,
|
||||||
|
so a 7 letter prefix would mean that maxtries should be 10.
|
||||||
|
@param maxtries= the last part of the libref. Provide an integer value.
|
||||||
|
|
||||||
|
@version 9.2
|
||||||
|
@author Allan Bowe
|
||||||
|
**/
|
||||||
|
|
||||||
|
|
||||||
|
%macro mf_getuniquelibref(prefix=mclib,maxtries=1000);
|
||||||
|
%local x libref;
|
||||||
|
%let x={SAS002};
|
||||||
|
%do x=0 %to &maxtries;
|
||||||
|
%if %sysfunc(libref(&prefix&x)) ne 0 %then %do;
|
||||||
|
%let libref=&prefix&x;
|
||||||
|
%let rc=%sysfunc(libname(&libref,%sysfunc(pathname(work))));
|
||||||
|
%if &rc %then %put %sysfunc(sysmsg());
|
||||||
|
&prefix&x
|
||||||
|
%*put &sysmacroname: Libref &libref assigned as WORK and returned;
|
||||||
|
%return;
|
||||||
|
%end;
|
||||||
|
%end;
|
||||||
|
%put unable to find available libref in range &prefix.0-&maxtries;
|
||||||
|
%mend;
|
||||||
|
`
|
||||||
|
|
||||||
|
lint(text).then((diagnostics) => console.table(diagnostics))
|
||||||
3
src/format.ts
Normal file
3
src/format.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const format = (text: string) => {
|
||||||
|
|
||||||
|
}
|
||||||
1
src/index.ts
Normal file
1
src/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { lint } from './lint'
|
||||||
84
src/lint.spec.ts
Normal file
84
src/lint.spec.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { lint, splitText } from './lint'
|
||||||
|
|
||||||
|
describe('lint', () => {
|
||||||
|
it('should identify trailing spaces', async () => {
|
||||||
|
const text = `/**
|
||||||
|
@file
|
||||||
|
**/
|
||||||
|
%put 'hello';
|
||||||
|
%put 'world'; `
|
||||||
|
const results = await lint(text)
|
||||||
|
|
||||||
|
expect(results.length).toEqual(2)
|
||||||
|
expect(results[0]).toEqual({
|
||||||
|
warning: 'Line contains trailing spaces',
|
||||||
|
lineNumber: 4,
|
||||||
|
columnNumber: 18
|
||||||
|
})
|
||||||
|
expect(results[1]).toEqual({
|
||||||
|
warning: 'Line contains trailing spaces',
|
||||||
|
lineNumber: 5,
|
||||||
|
columnNumber: 22
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should identify encoded passwords', async () => {
|
||||||
|
const text = `/**
|
||||||
|
@file
|
||||||
|
**/
|
||||||
|
%put '{SAS001}';`
|
||||||
|
const results = await lint(text)
|
||||||
|
|
||||||
|
expect(results.length).toEqual(1)
|
||||||
|
expect(results[0]).toEqual({
|
||||||
|
warning: 'Line contains encoded password',
|
||||||
|
lineNumber: 4,
|
||||||
|
columnNumber: 11
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should identify missing doxygen header', async () => {
|
||||||
|
const text = `%put 'hello';`
|
||||||
|
const results = await lint(text)
|
||||||
|
|
||||||
|
expect(results.length).toEqual(1)
|
||||||
|
expect(results[0]).toEqual({
|
||||||
|
warning: 'File missing Doxygen header',
|
||||||
|
lineNumber: 1,
|
||||||
|
columnNumber: 1
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an empty list with an empty file', async () => {
|
||||||
|
const text = `/**
|
||||||
|
@file
|
||||||
|
**/`
|
||||||
|
const results = await lint(text)
|
||||||
|
|
||||||
|
expect(results.length).toEqual(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('splitText', () => {
|
||||||
|
it('should return an empty array when text is falsy', () => {
|
||||||
|
const lines = splitText('')
|
||||||
|
|
||||||
|
expect(lines.length).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array of lines from text', () => {
|
||||||
|
const lines = splitText(`line 1\nline 2`)
|
||||||
|
|
||||||
|
expect(lines.length).toEqual(2)
|
||||||
|
expect(lines[0]).toEqual('line 1')
|
||||||
|
expect(lines[1]).toEqual('line 2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should work with CRLF line endings', () => {
|
||||||
|
const lines = splitText(`line 1\r\nline 2`)
|
||||||
|
|
||||||
|
expect(lines.length).toEqual(2)
|
||||||
|
expect(lines[0]).toEqual('line 1')
|
||||||
|
expect(lines[1]).toEqual('line 2')
|
||||||
|
})
|
||||||
|
})
|
||||||
46
src/lint.ts
Normal file
46
src/lint.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Diagnostic } from './types/Diagnostic'
|
||||||
|
import { LintConfig } from './types/LintConfig'
|
||||||
|
import { getLintConfig } from './utils/getLintConfig'
|
||||||
|
|
||||||
|
export const lint = async (text: string) => {
|
||||||
|
const config = await getLintConfig()
|
||||||
|
return processText(text, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const splitText = (text: string): string[] => {
|
||||||
|
if (!text) return []
|
||||||
|
return text.replace(/\r\n/g, '\n').split('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
const processText = (text: string, config: LintConfig) => {
|
||||||
|
const lines = splitText(text)
|
||||||
|
const diagnostics: Diagnostic[] = []
|
||||||
|
diagnostics.push(...processFile(config, text))
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
diagnostics.push(...processLine(config, line, index + 1))
|
||||||
|
})
|
||||||
|
|
||||||
|
return diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
const processFile = (config: LintConfig, fileContent: string): Diagnostic[] => {
|
||||||
|
const diagnostics: Diagnostic[] = []
|
||||||
|
config.fileLintRules.forEach((rule) => {
|
||||||
|
diagnostics.push(...rule.test(fileContent))
|
||||||
|
})
|
||||||
|
|
||||||
|
return diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
const processLine = (
|
||||||
|
config: LintConfig,
|
||||||
|
line: string,
|
||||||
|
lineNumber: number
|
||||||
|
): Diagnostic[] => {
|
||||||
|
const diagnostics: Diagnostic[] = []
|
||||||
|
config.lineLintRules.forEach((rule) => {
|
||||||
|
diagnostics.push(...rule.test(line, lineNumber))
|
||||||
|
})
|
||||||
|
|
||||||
|
return diagnostics
|
||||||
|
}
|
||||||
37
src/rules/hasDoxygenHeader.spec.ts
Normal file
37
src/rules/hasDoxygenHeader.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { hasDoxygenHeader } from './hasDoxygenHeader'
|
||||||
|
|
||||||
|
describe('hasDoxygenHeader', () => {
|
||||||
|
it('should return an empty array when the file starts with a doxygen header', () => {
|
||||||
|
const content = `/**
|
||||||
|
@file
|
||||||
|
@brief Returns an unused libref
|
||||||
|
**/
|
||||||
|
|
||||||
|
%macro mf_getuniquelibref(prefix=mclib,maxtries=1000);
|
||||||
|
%local x libref;
|
||||||
|
%let x={SAS002};
|
||||||
|
%do x=0 %to &maxtries;`
|
||||||
|
|
||||||
|
expect(hasDoxygenHeader.test(content)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with a single diagnostic when the file has no header', () => {
|
||||||
|
const content = `
|
||||||
|
%macro mf_getuniquelibref(prefix=mclib,maxtries=1000);
|
||||||
|
%local x libref;
|
||||||
|
%let x={SAS002};
|
||||||
|
%do x=0 %to &maxtries;`
|
||||||
|
|
||||||
|
expect(hasDoxygenHeader.test(content)).toEqual([
|
||||||
|
{ warning: 'File missing Doxygen header', lineNumber: 1, columnNumber: 1 }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with a single diagnostic when the file is undefined', () => {
|
||||||
|
const content = undefined
|
||||||
|
|
||||||
|
expect(hasDoxygenHeader.test((content as unknown) as string)).toEqual([
|
||||||
|
{ warning: 'File missing Doxygen header', lineNumber: 1, columnNumber: 1 }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
24
src/rules/hasDoxygenHeader.ts
Normal file
24
src/rules/hasDoxygenHeader.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { FileLintRule } from '../types/LintRule'
|
||||||
|
import { LintRuleType } from '../types/LintRuleType'
|
||||||
|
|
||||||
|
const name = 'hasDoxygenHeader'
|
||||||
|
const description =
|
||||||
|
'Enforce the presence of a Doxygen header at the start of each file.'
|
||||||
|
const warning = 'File missing Doxygen header'
|
||||||
|
const test = (value: string) => {
|
||||||
|
try {
|
||||||
|
const hasFileHeader = value.split('/**')[0] !== value
|
||||||
|
if (hasFileHeader) return []
|
||||||
|
return [{ warning, lineNumber: 1, columnNumber: 1 }]
|
||||||
|
} catch (e) {
|
||||||
|
return [{ warning, lineNumber: 1, columnNumber: 1 }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hasDoxygenHeader: FileLintRule = {
|
||||||
|
type: LintRuleType.File,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
warning,
|
||||||
|
test
|
||||||
|
}
|
||||||
35
src/rules/noEncodedPasswords.spec.ts
Normal file
35
src/rules/noEncodedPasswords.spec.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { noEncodedPasswords } from './noEncodedPasswords'
|
||||||
|
|
||||||
|
describe('noEncodedPasswords', () => {
|
||||||
|
it('should return an empty array when the line has no encoded passwords', () => {
|
||||||
|
const line = "%put 'hello';"
|
||||||
|
expect(noEncodedPasswords.test(line, 1)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with a single diagnostic when the line has an encoded password', () => {
|
||||||
|
const line = "%put '{SAS001}'; "
|
||||||
|
expect(noEncodedPasswords.test(line, 1)).toEqual([
|
||||||
|
{
|
||||||
|
warning: 'Line contains encoded password',
|
||||||
|
lineNumber: 1,
|
||||||
|
columnNumber: 7
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with multiple diagnostics when the line has encoded passwords', () => {
|
||||||
|
const line = "%put '{SAS001} {SAS002}'; "
|
||||||
|
expect(noEncodedPasswords.test(line, 1)).toEqual([
|
||||||
|
{
|
||||||
|
warning: 'Line contains encoded password',
|
||||||
|
lineNumber: 1,
|
||||||
|
columnNumber: 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
warning: 'Line contains encoded password',
|
||||||
|
lineNumber: 1,
|
||||||
|
columnNumber: 16
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
24
src/rules/noEncodedPasswords.ts
Normal file
24
src/rules/noEncodedPasswords.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { LineLintRule } from '../types/LintRule'
|
||||||
|
import { LintRuleType } from '../types/LintRuleType'
|
||||||
|
|
||||||
|
const name = 'noEncodedPasswords'
|
||||||
|
const description = 'Disallow encoded passwords in SAS code.'
|
||||||
|
const warning = 'Line contains encoded password'
|
||||||
|
const test = (value: string, lineNumber: number) => {
|
||||||
|
const regex = new RegExp(/{sas\d{2,4}}[^;"'\s]*/, 'gi')
|
||||||
|
const matches = value.match(regex)
|
||||||
|
if (!matches || !matches.length) return []
|
||||||
|
return matches.map((match) => ({
|
||||||
|
warning,
|
||||||
|
lineNumber,
|
||||||
|
columnNumber: value.indexOf(match) + 1
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const noEncodedPasswords: LineLintRule = {
|
||||||
|
type: LintRuleType.Line,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
warning,
|
||||||
|
test
|
||||||
|
}
|
||||||
19
src/rules/noTrailingSpaces.spec.ts
Normal file
19
src/rules/noTrailingSpaces.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { noTrailingSpaces } from './noTrailingSpaces'
|
||||||
|
|
||||||
|
describe('noTrailingSpaces', () => {
|
||||||
|
it('should return an empty array when the line has no trailing spaces', () => {
|
||||||
|
const line = "%put 'hello';"
|
||||||
|
expect(noTrailingSpaces.test(line, 1)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with a single diagnostic when the line has trailing spaces', () => {
|
||||||
|
const line = "%put 'hello'; "
|
||||||
|
expect(noTrailingSpaces.test(line, 1)).toEqual([
|
||||||
|
{
|
||||||
|
warning: 'Line contains trailing spaces',
|
||||||
|
lineNumber: 1,
|
||||||
|
columnNumber: 14
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
18
src/rules/noTrailingSpaces.ts
Normal file
18
src/rules/noTrailingSpaces.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { LineLintRule } from '../types/LintRule'
|
||||||
|
import { LintRuleType } from '../types/LintRuleType'
|
||||||
|
|
||||||
|
const name = 'noTrailingSpaces'
|
||||||
|
const description = 'Disallow trailing spaces on lines.'
|
||||||
|
const warning = 'Line contains trailing spaces'
|
||||||
|
const test = (value: string, lineNumber: number) =>
|
||||||
|
value.trimEnd() === value
|
||||||
|
? []
|
||||||
|
: [{ warning, lineNumber, columnNumber: value.trimEnd().length + 1 }]
|
||||||
|
|
||||||
|
export const noTrailingSpaces: LineLintRule = {
|
||||||
|
type: LintRuleType.Line,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
warning,
|
||||||
|
test
|
||||||
|
}
|
||||||
5
src/types/Diagnostic.ts
Normal file
5
src/types/Diagnostic.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface Diagnostic {
|
||||||
|
lineNumber: number
|
||||||
|
columnNumber: number
|
||||||
|
warning: string
|
||||||
|
}
|
||||||
61
src/types/LintConfig.spec.ts
Normal file
61
src/types/LintConfig.spec.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { LintConfig } from './LintConfig'
|
||||||
|
import { LintRuleType } from './LintRuleType'
|
||||||
|
|
||||||
|
describe('LintConfig', () => {
|
||||||
|
it('should create an empty instance', () => {
|
||||||
|
const config = new LintConfig()
|
||||||
|
|
||||||
|
expect(config).toBeTruthy()
|
||||||
|
expect(config.fileLintRules.length).toEqual(0)
|
||||||
|
expect(config.lineLintRules.length).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create an instance with the noTrailingSpaces flag set', () => {
|
||||||
|
const config = new LintConfig({ noTrailingSpaces: true })
|
||||||
|
|
||||||
|
expect(config).toBeTruthy()
|
||||||
|
expect(config.lineLintRules.length).toEqual(1)
|
||||||
|
expect(config.lineLintRules[0].name).toEqual('noTrailingSpaces')
|
||||||
|
expect(config.lineLintRules[0].type).toEqual(LintRuleType.Line)
|
||||||
|
expect(config.fileLintRules.length).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create an instance with the noEncodedPasswords flag set', () => {
|
||||||
|
const config = new LintConfig({ noEncodedPasswords: true })
|
||||||
|
|
||||||
|
expect(config).toBeTruthy()
|
||||||
|
expect(config.lineLintRules.length).toEqual(1)
|
||||||
|
expect(config.lineLintRules[0].name).toEqual('noEncodedPasswords')
|
||||||
|
expect(config.lineLintRules[0].type).toEqual(LintRuleType.Line)
|
||||||
|
expect(config.fileLintRules.length).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create an instance with the hasDoxygenHeader flag set', () => {
|
||||||
|
const config = new LintConfig({ hasDoxygenHeader: true })
|
||||||
|
|
||||||
|
expect(config).toBeTruthy()
|
||||||
|
expect(config.lineLintRules.length).toEqual(0)
|
||||||
|
expect(config.fileLintRules.length).toEqual(1)
|
||||||
|
expect(config.fileLintRules[0].name).toEqual('hasDoxygenHeader')
|
||||||
|
expect(config.fileLintRules[0].type).toEqual(LintRuleType.File)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create an instance with all flags set', () => {
|
||||||
|
const config = new LintConfig({
|
||||||
|
noTrailingSpaces: true,
|
||||||
|
noEncodedPasswords: true,
|
||||||
|
hasDoxygenHeader: true
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(config).toBeTruthy()
|
||||||
|
expect(config.lineLintRules.length).toEqual(2)
|
||||||
|
expect(config.lineLintRules[0].name).toEqual('noTrailingSpaces')
|
||||||
|
expect(config.lineLintRules[0].type).toEqual(LintRuleType.Line)
|
||||||
|
expect(config.lineLintRules[1].name).toEqual('noEncodedPasswords')
|
||||||
|
expect(config.lineLintRules[1].type).toEqual(LintRuleType.Line)
|
||||||
|
|
||||||
|
expect(config.fileLintRules.length).toEqual(1)
|
||||||
|
expect(config.fileLintRules[0].name).toEqual('hasDoxygenHeader')
|
||||||
|
expect(config.fileLintRules[0].type).toEqual(LintRuleType.File)
|
||||||
|
})
|
||||||
|
})
|
||||||
23
src/types/LintConfig.ts
Normal file
23
src/types/LintConfig.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { hasDoxygenHeader } from '../rules/hasDoxygenHeader'
|
||||||
|
import { noEncodedPasswords } from '../rules/noEncodedPasswords'
|
||||||
|
import { noTrailingSpaces } from '../rules/noTrailingSpaces'
|
||||||
|
import { FileLintRule, LineLintRule } from './LintRule'
|
||||||
|
|
||||||
|
export class LintConfig {
|
||||||
|
readonly lineLintRules: LineLintRule[] = []
|
||||||
|
readonly fileLintRules: FileLintRule[] = []
|
||||||
|
|
||||||
|
constructor(json?: any) {
|
||||||
|
if (json?.noTrailingSpaces) {
|
||||||
|
this.lineLintRules.push(noTrailingSpaces)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json?.noEncodedPasswords) {
|
||||||
|
this.lineLintRules.push(noEncodedPasswords)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json?.hasDoxygenHeader) {
|
||||||
|
this.fileLintRules.push(hasDoxygenHeader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/types/LintRule.ts
Normal file
19
src/types/LintRule.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Diagnostic } from './Diagnostic'
|
||||||
|
import { LintRuleType } from './LintRuleType'
|
||||||
|
|
||||||
|
export interface LintRule {
|
||||||
|
type: LintRuleType
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
warning: string
|
||||||
|
test: (value: string, lineNumber: number) => Diagnostic[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LineLintRule extends LintRule {
|
||||||
|
type: LintRuleType.Line
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileLintRule extends LintRule {
|
||||||
|
type: LintRuleType.File
|
||||||
|
test: (value: string) => Diagnostic[]
|
||||||
|
}
|
||||||
4
src/types/LintRuleType.ts
Normal file
4
src/types/LintRuleType.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export enum LintRuleType {
|
||||||
|
Line,
|
||||||
|
File
|
||||||
|
}
|
||||||
10
src/utils/getLintConfig.spec.ts
Normal file
10
src/utils/getLintConfig.spec.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { LintConfig } from '../types/LintConfig'
|
||||||
|
import { getLintConfig } from './getLintConfig'
|
||||||
|
|
||||||
|
describe('getLintConfig', () => {
|
||||||
|
it('should get the lint config', async () => {
|
||||||
|
const config = await getLintConfig()
|
||||||
|
|
||||||
|
expect(config).toBeInstanceOf(LintConfig)
|
||||||
|
})
|
||||||
|
})
|
||||||
10
src/utils/getLintConfig.ts
Normal file
10
src/utils/getLintConfig.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import { LintConfig } from '../types/LintConfig'
|
||||||
|
import { readFile } from '@sasjs/utils/file'
|
||||||
|
import { getProjectRoot } from './getProjectRoot'
|
||||||
|
|
||||||
|
export async function getLintConfig(): Promise<LintConfig> {
|
||||||
|
const projectRoot = await getProjectRoot()
|
||||||
|
const configuration = await readFile(path.join(projectRoot, '.sasjslint'))
|
||||||
|
return new LintConfig(JSON.parse(configuration))
|
||||||
|
}
|
||||||
22
src/utils/getProjectRoot.spec.ts
Normal file
22
src/utils/getProjectRoot.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { getProjectRoot } from './getProjectRoot'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
describe('getProjectRoot', () => {
|
||||||
|
it('should return the current location if it contains the lint config file', async () => {
|
||||||
|
const projectRoot = await getProjectRoot()
|
||||||
|
|
||||||
|
expect(projectRoot).toEqual(process.cwd())
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return the parent folder if it contains the lint config file', async () => {
|
||||||
|
const currentLocation = process.cwd()
|
||||||
|
jest
|
||||||
|
.spyOn(process, 'cwd')
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
path.join(currentLocation, 'folder', 'subfolder')
|
||||||
|
)
|
||||||
|
const projectRoot = await getProjectRoot()
|
||||||
|
|
||||||
|
expect(projectRoot).toEqual(currentLocation)
|
||||||
|
})
|
||||||
|
})
|
||||||
27
src/utils/getProjectRoot.ts
Normal file
27
src/utils/getProjectRoot.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import { fileExists } from '@sasjs/utils/file'
|
||||||
|
|
||||||
|
export async function getProjectRoot() {
|
||||||
|
let root = ''
|
||||||
|
let rootFound = false
|
||||||
|
let i = 1
|
||||||
|
let currentLocation = process.cwd()
|
||||||
|
|
||||||
|
const maxLevels = currentLocation.split(path.sep).length
|
||||||
|
|
||||||
|
while (i <= maxLevels && !rootFound) {
|
||||||
|
const isRoot = await fileExists(path.join(currentLocation, '.sasjslint'))
|
||||||
|
|
||||||
|
if (isRoot) {
|
||||||
|
rootFound = true
|
||||||
|
root = currentLocation
|
||||||
|
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
currentLocation = path.join(currentLocation, '..')
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return root
|
||||||
|
}
|
||||||
24
tsconfig.json
Normal file
24
tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": [
|
||||||
|
"ES2018",
|
||||||
|
"DOM",
|
||||||
|
"ES2019.String"
|
||||||
|
],
|
||||||
|
"target": "es5",
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "./build",
|
||||||
|
"strict": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"**/*.spec.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user