mirror of
https://github.com/sasjs/lint.git
synced 2025-12-10 09:34:34 +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
|
||||
.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