mirror of
https://github.com/sasjs/lint.git
synced 2025-12-10 17:34:36 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7aa4bfc6ba | ||
|
|
ffcd57d5f7 | ||
|
|
86a6d36693 | ||
|
|
28d5e7121a | ||
|
|
c0d27fa254 | ||
|
|
a8ca534b0b |
12
package-lock.json
generated
12
package-lock.json
generated
@@ -648,9 +648,9 @@
|
||||
}
|
||||
},
|
||||
"@sasjs/utils": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.9.0.tgz",
|
||||
"integrity": "sha512-j7ssEmb8OSZHUUL0PGVgoby0j0ClCcsLsydDCk/C4OAoWPAUPFI5HgGFPSEipz9+P8OlL/EBnglj4LGtlFHCpw==",
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.10.1.tgz",
|
||||
"integrity": "sha512-T54jx6NEMLu2+R/ux4qcb3dDJ7nFrKkPCkmPXEfZxPQBkbq4C0kmaZv6dC63RDH68wYhoXR2S5fION5fFh91iw==",
|
||||
"requires": {
|
||||
"@types/prompts": "^2.0.9",
|
||||
"consola": "^2.15.0",
|
||||
@@ -778,9 +778,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@types/prompts": {
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.0.9.tgz",
|
||||
"integrity": "sha512-TORZP+FSjTYMWwKadftmqEn6bziN5RnfygehByGsjxoK5ydnClddtv6GikGWPvCm24oI+YBwck5WDxIIyNxUrA==",
|
||||
"version": "2.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.0.10.tgz",
|
||||
"integrity": "sha512-W3PEl3l4vmxdgfY6LUG7ysh+mLJOTOFYmSpiLe6MCo1OdEm8b5s6ZJfuTQgEpYNwcMiiaRzJespPS5Py2tqLlQ==",
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
|
||||
@@ -44,6 +44,6 @@
|
||||
"typescript": "^4.2.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sasjs/utils": "^2.9.0"
|
||||
"@sasjs/utils": "^2.10.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export { lintText, lintFile } from './lint'
|
||||
export * from './lint'
|
||||
export * from './types'
|
||||
export * from './utils'
|
||||
|
||||
158
src/lint.spec.ts
158
src/lint.spec.ts
@@ -1,158 +0,0 @@
|
||||
import { lintFile, lintText, splitText } from './lint'
|
||||
import { Severity } from './types/Severity'
|
||||
import path from 'path'
|
||||
|
||||
describe('lintText', () => {
|
||||
it('should identify trailing spaces', async () => {
|
||||
const text = `/**
|
||||
@file
|
||||
**/
|
||||
%put 'hello';
|
||||
%put 'world'; `
|
||||
const results = await lintText(text)
|
||||
|
||||
expect(results.length).toEqual(2)
|
||||
expect(results[0]).toEqual({
|
||||
message: 'Line contains trailing spaces',
|
||||
lineNumber: 4,
|
||||
startColumnNumber: 18,
|
||||
endColumnNumber: 18,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results[1]).toEqual({
|
||||
message: 'Line contains trailing spaces',
|
||||
lineNumber: 5,
|
||||
startColumnNumber: 22,
|
||||
endColumnNumber: 23,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
})
|
||||
|
||||
it('should identify encoded passwords', async () => {
|
||||
const text = `/**
|
||||
@file
|
||||
**/
|
||||
%put '{SAS001}';`
|
||||
const results = await lintText(text)
|
||||
|
||||
expect(results.length).toEqual(1)
|
||||
expect(results[0]).toEqual({
|
||||
message: 'Line contains encoded password',
|
||||
lineNumber: 4,
|
||||
startColumnNumber: 11,
|
||||
endColumnNumber: 19,
|
||||
severity: Severity.Error
|
||||
})
|
||||
})
|
||||
|
||||
it('should identify missing doxygen header', async () => {
|
||||
const text = `%put 'hello';`
|
||||
const results = await lintText(text)
|
||||
|
||||
expect(results.length).toEqual(1)
|
||||
expect(results[0]).toEqual({
|
||||
message: 'File missing Doxygen header',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
})
|
||||
|
||||
it('should return an empty list with an empty file', async () => {
|
||||
const text = `/**
|
||||
@file
|
||||
**/`
|
||||
const results = await lintText(text)
|
||||
|
||||
expect(results.length).toEqual(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('lintFile', () => {
|
||||
it('should identify lint issues in a given file', async () => {
|
||||
const results = await lintFile(path.join(__dirname, 'Example File.sas'))
|
||||
|
||||
expect(results.length).toEqual(8)
|
||||
expect(results).toContainEqual({
|
||||
message: 'Line contains trailing spaces',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 2,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'Line contains trailing spaces',
|
||||
lineNumber: 2,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 2,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'File name contains spaces',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'File name contains uppercase characters',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'File missing Doxygen header',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'Line contains encoded password',
|
||||
lineNumber: 5,
|
||||
startColumnNumber: 10,
|
||||
endColumnNumber: 18,
|
||||
severity: Severity.Error
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'Line is indented with a tab',
|
||||
lineNumber: 7,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'Line has incorrect indentation - 3 spaces',
|
||||
lineNumber: 6,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
81
src/lint.ts
81
src/lint.ts
@@ -1,81 +0,0 @@
|
||||
import { readFile } from '@sasjs/utils/file'
|
||||
import { Diagnostic } from './types/Diagnostic'
|
||||
import { LintConfig } from './types/LintConfig'
|
||||
import { getLintConfig } from './utils/getLintConfig'
|
||||
|
||||
/**
|
||||
* Analyses and produces a set of diagnostics for the given text content.
|
||||
* @param {string} text - the text content to be linted.
|
||||
* @returns {Diagnostic[]} array of diagnostic objects, each containing a warning, line number and column number.
|
||||
*/
|
||||
export const lintText = async (text: string) => {
|
||||
const config = await getLintConfig()
|
||||
return processText(text, config)
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyses and produces a set of diagnostics for the file at the given path.
|
||||
* @param {string} filePath - the path to the file to be linted.
|
||||
* @returns {Diagnostic[]} array of diagnostic objects, each containing a warning, line number and column number.
|
||||
*/
|
||||
export const lintFile = async (filePath: string) => {
|
||||
const config = await getLintConfig()
|
||||
const text = await readFile(filePath)
|
||||
|
||||
const fileDiagnostics = processFile(filePath, config)
|
||||
const textDiagnostics = processText(text, config)
|
||||
|
||||
return [...fileDiagnostics, ...textDiagnostics]
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the given content into a list of lines, regardless of CRLF or LF line endings.
|
||||
* @param {string} text - the text content to be split into lines.
|
||||
* @returns {string[]} an array of lines from the given text
|
||||
*/
|
||||
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(...processContent(config, text))
|
||||
lines.forEach((line, index) => {
|
||||
diagnostics.push(...processLine(config, line, index + 1))
|
||||
})
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
const processContent = (config: LintConfig, content: string): Diagnostic[] => {
|
||||
const diagnostics: Diagnostic[] = []
|
||||
config.fileLintRules.forEach((rule) => {
|
||||
diagnostics.push(...rule.test(content))
|
||||
})
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
const processLine = (
|
||||
config: LintConfig,
|
||||
line: string,
|
||||
lineNumber: number
|
||||
): Diagnostic[] => {
|
||||
const diagnostics: Diagnostic[] = []
|
||||
config.lineLintRules.forEach((rule) => {
|
||||
diagnostics.push(...rule.test(line, lineNumber, config))
|
||||
})
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
const processFile = (filePath: string, config: LintConfig): Diagnostic[] => {
|
||||
const diagnostics: Diagnostic[] = []
|
||||
config.pathLintRules.forEach((rule) => {
|
||||
diagnostics.push(...rule.test(filePath))
|
||||
})
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
4
src/lint/index.ts
Normal file
4
src/lint/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './lintText'
|
||||
export * from './lintFile'
|
||||
export * from './lintFolder'
|
||||
export * from './lintProject'
|
||||
69
src/lint/lintFile.spec.ts
Normal file
69
src/lint/lintFile.spec.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { lintFile } from './lintFile'
|
||||
import { Severity } from '../types/Severity'
|
||||
import path from 'path'
|
||||
|
||||
describe('lintFile', () => {
|
||||
it('should identify lint issues in a given file', async () => {
|
||||
const results = await lintFile(
|
||||
path.join(__dirname, '..', 'Example File.sas')
|
||||
)
|
||||
|
||||
expect(results.length).toEqual(8)
|
||||
expect(results).toContainEqual({
|
||||
message: 'Line contains trailing spaces',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 2,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'Line contains trailing spaces',
|
||||
lineNumber: 2,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 2,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'File name contains spaces',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'File name contains uppercase characters',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'File missing Doxygen header',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'Line contains encoded password',
|
||||
lineNumber: 5,
|
||||
startColumnNumber: 10,
|
||||
endColumnNumber: 18,
|
||||
severity: Severity.Error
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'Line is indented with a tab',
|
||||
lineNumber: 7,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'Line has incorrect indentation - 3 spaces',
|
||||
lineNumber: 6,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
})
|
||||
})
|
||||
23
src/lint/lintFile.ts
Normal file
23
src/lint/lintFile.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { readFile } from '@sasjs/utils/file'
|
||||
import { LintConfig } from '../types/LintConfig'
|
||||
import { getLintConfig } from '../utils/getLintConfig'
|
||||
import { processFile, processText } from './shared'
|
||||
|
||||
/**
|
||||
* Analyses and produces a set of diagnostics for the file at the given path.
|
||||
* @param {string} filePath - the path to the file to be linted.
|
||||
* @param {LintConfig} configuration - an optional configuration. When not passed in, this is read from the .sasjslint file.
|
||||
* @returns {Diagnostic[]} array of diagnostic objects, each containing a warning, line number and column number.
|
||||
*/
|
||||
export const lintFile = async (
|
||||
filePath: string,
|
||||
configuration?: LintConfig
|
||||
) => {
|
||||
const config = configuration || (await getLintConfig())
|
||||
const text = await readFile(filePath)
|
||||
|
||||
const fileDiagnostics = processFile(filePath, config)
|
||||
const textDiagnostics = processText(text, config)
|
||||
|
||||
return [...fileDiagnostics, ...textDiagnostics]
|
||||
}
|
||||
67
src/lint/lintFolder.spec.ts
Normal file
67
src/lint/lintFolder.spec.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { lintFolder } from './lintFolder'
|
||||
import { Severity } from '../types/Severity'
|
||||
import path from 'path'
|
||||
|
||||
describe('lintFolder', () => {
|
||||
it('should identify lint issues in a given folder', async () => {
|
||||
const results = await lintFolder(path.join(__dirname, '..'))
|
||||
|
||||
expect(results.length).toEqual(8)
|
||||
expect(results).toContainEqual({
|
||||
message: 'Line contains trailing spaces',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 2,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'Line contains trailing spaces',
|
||||
lineNumber: 2,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 2,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'File name contains spaces',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'File name contains uppercase characters',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'File missing Doxygen header',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'Line contains encoded password',
|
||||
lineNumber: 5,
|
||||
startColumnNumber: 10,
|
||||
endColumnNumber: 18,
|
||||
severity: Severity.Error
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'Line is indented with a tab',
|
||||
lineNumber: 7,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'Line has incorrect indentation - 3 spaces',
|
||||
lineNumber: 6,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
})
|
||||
})
|
||||
49
src/lint/lintFolder.ts
Normal file
49
src/lint/lintFolder.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { listSubFoldersInFolder } from '@sasjs/utils/file'
|
||||
import path from 'path'
|
||||
import { Diagnostic } from '../types/Diagnostic'
|
||||
import { LintConfig } from '../types/LintConfig'
|
||||
import { asyncForEach } from '../utils/asyncForEach'
|
||||
import { getLintConfig } from '../utils/getLintConfig'
|
||||
import { listSasFiles } from '../utils/listSasFiles'
|
||||
import { lintFile } from './lintFile'
|
||||
|
||||
const excludeFolders = [
|
||||
'.git',
|
||||
'.github',
|
||||
'.vscode',
|
||||
'node_modules',
|
||||
'sasjsbuild',
|
||||
'sasjsresults'
|
||||
]
|
||||
|
||||
/**
|
||||
* Analyses and produces a set of diagnostics for the folder at the given path.
|
||||
* @param {string} folderPath - the path to the folder to be linted.
|
||||
* @param {LintConfig} configuration - an optional configuration. When not passed in, this is read from the .sasjslint file.
|
||||
* @returns {Diagnostic[]} array of diagnostic objects, each containing a warning, line number and column number.
|
||||
*/
|
||||
export const lintFolder = async (
|
||||
folderPath: string,
|
||||
configuration?: LintConfig
|
||||
) => {
|
||||
const config = configuration || (await getLintConfig())
|
||||
const diagnostics: Diagnostic[] = []
|
||||
const fileNames = await listSasFiles(folderPath)
|
||||
await asyncForEach(fileNames, async (fileName) => {
|
||||
diagnostics.push(
|
||||
...(await lintFile(path.join(folderPath, fileName), config))
|
||||
)
|
||||
})
|
||||
|
||||
const subFolders = (await listSubFoldersInFolder(folderPath)).filter(
|
||||
(f: string) => !excludeFolders.includes(f)
|
||||
)
|
||||
|
||||
await asyncForEach(subFolders, async (subFolder) => {
|
||||
diagnostics.push(
|
||||
...(await lintFolder(path.join(folderPath, subFolder), config))
|
||||
)
|
||||
})
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
67
src/lint/lintProject.spec.ts
Normal file
67
src/lint/lintProject.spec.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { lintProject } from './lintProject'
|
||||
import { Severity } from '../types/Severity'
|
||||
import path from 'path'
|
||||
|
||||
describe('lintProject', () => {
|
||||
it('should identify lint issues in a given project', async () => {
|
||||
const results = await lintProject()
|
||||
|
||||
expect(results.length).toEqual(8)
|
||||
expect(results).toContainEqual({
|
||||
message: 'Line contains trailing spaces',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 2,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'Line contains trailing spaces',
|
||||
lineNumber: 2,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 2,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'File name contains spaces',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'File name contains uppercase characters',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'File missing Doxygen header',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'Line contains encoded password',
|
||||
lineNumber: 5,
|
||||
startColumnNumber: 10,
|
||||
endColumnNumber: 18,
|
||||
severity: Severity.Error
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'Line is indented with a tab',
|
||||
lineNumber: 7,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results).toContainEqual({
|
||||
message: 'Line has incorrect indentation - 3 spaces',
|
||||
lineNumber: 6,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
})
|
||||
})
|
||||
16
src/lint/lintProject.ts
Normal file
16
src/lint/lintProject.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { getProjectRoot } from '../utils'
|
||||
import { lintFolder } from './lintFolder'
|
||||
|
||||
/**
|
||||
* Analyses and produces a set of diagnostics for the current project.
|
||||
* @returns {Diagnostic[]} array of diagnostic objects, each containing a warning, line number and column number.
|
||||
*/
|
||||
export const lintProject = async () => {
|
||||
const projectRoot =
|
||||
(await getProjectRoot()) || process.projectDir || process.currentDir
|
||||
|
||||
if (!projectRoot) {
|
||||
throw new Error('SASjs Project Root was not found.')
|
||||
}
|
||||
return await lintFolder(projectRoot)
|
||||
}
|
||||
69
src/lint/lintText.spec.ts
Normal file
69
src/lint/lintText.spec.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { lintText } from './lintText'
|
||||
import { Severity } from '../types/Severity'
|
||||
|
||||
describe('lintText', () => {
|
||||
it('should identify trailing spaces', async () => {
|
||||
const text = `/**
|
||||
@file
|
||||
**/
|
||||
%put 'hello';
|
||||
%put 'world'; `
|
||||
const results = await lintText(text)
|
||||
|
||||
expect(results.length).toEqual(2)
|
||||
expect(results[0]).toEqual({
|
||||
message: 'Line contains trailing spaces',
|
||||
lineNumber: 4,
|
||||
startColumnNumber: 18,
|
||||
endColumnNumber: 18,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
expect(results[1]).toEqual({
|
||||
message: 'Line contains trailing spaces',
|
||||
lineNumber: 5,
|
||||
startColumnNumber: 22,
|
||||
endColumnNumber: 23,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
})
|
||||
|
||||
it('should identify encoded passwords', async () => {
|
||||
const text = `/**
|
||||
@file
|
||||
**/
|
||||
%put '{SAS001}';`
|
||||
const results = await lintText(text)
|
||||
|
||||
expect(results.length).toEqual(1)
|
||||
expect(results[0]).toEqual({
|
||||
message: 'Line contains encoded password',
|
||||
lineNumber: 4,
|
||||
startColumnNumber: 11,
|
||||
endColumnNumber: 19,
|
||||
severity: Severity.Error
|
||||
})
|
||||
})
|
||||
|
||||
it('should identify missing doxygen header', async () => {
|
||||
const text = `%put 'hello';`
|
||||
const results = await lintText(text)
|
||||
|
||||
expect(results.length).toEqual(1)
|
||||
expect(results[0]).toEqual({
|
||||
message: 'File missing Doxygen header',
|
||||
lineNumber: 1,
|
||||
startColumnNumber: 1,
|
||||
endColumnNumber: 1,
|
||||
severity: Severity.Warning
|
||||
})
|
||||
})
|
||||
|
||||
it('should return an empty list with an empty file', async () => {
|
||||
const text = `/**
|
||||
@file
|
||||
**/`
|
||||
const results = await lintText(text)
|
||||
|
||||
expect(results.length).toEqual(0)
|
||||
})
|
||||
})
|
||||
12
src/lint/lintText.ts
Normal file
12
src/lint/lintText.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { getLintConfig } from '../utils/getLintConfig'
|
||||
import { processText } from './shared'
|
||||
|
||||
/**
|
||||
* Analyses and produces a set of diagnostics for the given text content.
|
||||
* @param {string} text - the text content to be linted.
|
||||
* @returns {Diagnostic[]} array of diagnostic objects, each containing a warning, line number and column number.
|
||||
*/
|
||||
export const lintText = async (text: string) => {
|
||||
const config = await getLintConfig()
|
||||
return processText(text, config)
|
||||
}
|
||||
25
src/lint/shared.spec.ts
Normal file
25
src/lint/shared.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { splitText } from './shared'
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
56
src/lint/shared.ts
Normal file
56
src/lint/shared.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { LintConfig, Diagnostic } from '../types'
|
||||
|
||||
/**
|
||||
* Splits the given content into a list of lines, regardless of CRLF or LF line endings.
|
||||
* @param {string} text - the text content to be split into lines.
|
||||
* @returns {string[]} an array of lines from the given text
|
||||
*/
|
||||
export const splitText = (text: string): string[] => {
|
||||
if (!text) return []
|
||||
return text.replace(/\r\n/g, '\n').split('\n')
|
||||
}
|
||||
|
||||
export const processText = (text: string, config: LintConfig) => {
|
||||
const lines = splitText(text)
|
||||
const diagnostics: Diagnostic[] = []
|
||||
diagnostics.push(...processContent(config, text))
|
||||
lines.forEach((line, index) => {
|
||||
diagnostics.push(...processLine(config, line, index + 1))
|
||||
})
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
export const processFile = (
|
||||
filePath: string,
|
||||
config: LintConfig
|
||||
): Diagnostic[] => {
|
||||
const diagnostics: Diagnostic[] = []
|
||||
config.pathLintRules.forEach((rule) => {
|
||||
diagnostics.push(...rule.test(filePath))
|
||||
})
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
const processContent = (config: LintConfig, content: string): Diagnostic[] => {
|
||||
const diagnostics: Diagnostic[] = []
|
||||
config.fileLintRules.forEach((rule) => {
|
||||
diagnostics.push(...rule.test(content))
|
||||
})
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
export const processLine = (
|
||||
config: LintConfig,
|
||||
line: string,
|
||||
lineNumber: number
|
||||
): Diagnostic[] => {
|
||||
const diagnostics: Diagnostic[] = []
|
||||
config.lineLintRules.forEach((rule) => {
|
||||
diagnostics.push(...rule.test(line, lineNumber, config))
|
||||
})
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
@@ -11,7 +11,7 @@ const test = (value: string, lineNumber: number, config?: LintConfig) => {
|
||||
|
||||
const indentationMultiple = isNaN(config?.indentationMultiple as number)
|
||||
? 2
|
||||
: config?.indentationMultiple
|
||||
: config!.indentationMultiple
|
||||
|
||||
if (indentationMultiple === 0) return []
|
||||
const numberOfSpaces = value.search(/\S|$/)
|
||||
|
||||
6
src/types/Process.d.ts
vendored
Normal file
6
src/types/Process.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
declare namespace NodeJS {
|
||||
export interface Process {
|
||||
projectDir: string
|
||||
currentDir: string
|
||||
}
|
||||
}
|
||||
15
src/utils/asyncForEach.spec.ts
Normal file
15
src/utils/asyncForEach.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { asyncForEach } from './asyncForEach'
|
||||
|
||||
describe('asyncForEach', () => {
|
||||
it('should execute the async callback for each item in the given array', async () => {
|
||||
const callback = jest.fn().mockImplementation(() => Promise.resolve())
|
||||
const array = [1, 2, 3]
|
||||
|
||||
await asyncForEach(array, callback)
|
||||
|
||||
expect(callback.mock.calls.length).toEqual(3)
|
||||
expect(callback.mock.calls[0]).toEqual([1, 0, array])
|
||||
expect(callback.mock.calls[1]).toEqual([2, 1, array])
|
||||
expect(callback.mock.calls[2]).toEqual([3, 2, array])
|
||||
})
|
||||
})
|
||||
8
src/utils/asyncForEach.ts
Normal file
8
src/utils/asyncForEach.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export async function asyncForEach(
|
||||
array: any[],
|
||||
callback: (item: any, index: number, originalArray: any[]) => any
|
||||
) {
|
||||
for (let index = 0; index < array.length; index++) {
|
||||
await callback(array[index], index, array)
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ export async function getLintConfig(): Promise<LintConfig> {
|
||||
const configuration = await readFile(
|
||||
path.join(projectRoot, '.sasjslint')
|
||||
).catch((_) => {
|
||||
console.warn('Unable to load .sasjslint file. Using default configuration.')
|
||||
return JSON.stringify(DefaultLintConfiguration)
|
||||
})
|
||||
return new LintConfig(JSON.parse(configuration))
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './getLintConfig'
|
||||
export * from './getProjectRoot'
|
||||
export * from './listSasFiles'
|
||||
|
||||
6
src/utils/listSasFiles.ts
Normal file
6
src/utils/listSasFiles.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { listFilesInFolder } from '@sasjs/utils/file'
|
||||
|
||||
export const listSasFiles = async (folderPath: string): Promise<string[]> => {
|
||||
const files = await listFilesInFolder(folderPath)
|
||||
return files.filter((f) => f.endsWith('.sas'))
|
||||
}
|
||||
Reference in New Issue
Block a user