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

chore(*): split lint module into smaller submodules, added tests

This commit is contained in:
Krishna Acondy
2021-03-31 08:32:42 +01:00
parent a8ca534b0b
commit c0d27fa254
16 changed files with 472 additions and 298 deletions

View File

@@ -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')
})
})

View File

@@ -1,139 +0,0 @@
import { readFile, listSubFoldersInFolder } from '@sasjs/utils/file'
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 path from 'path'
import { getProjectRoot } from './utils'
const excludeFolders = [
'.git',
'.github',
'.vscode',
'node_modules',
'sasjsbuild',
'sasjsresults'
]
/**
* 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.
* @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]
}
/**
* 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
}
/**
* 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()
return await lintFolder(projectRoot)
}
/**
* 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
}

1
src/lint/index.ts Normal file
View File

@@ -0,0 +1 @@
export { lintText, lintFile, lintFolder, lintProject } from './lint'

69
src/lint/lintFile.spec.ts Normal file
View 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
View 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]
}

View 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
View 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
}

View 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
})
})
})

12
src/lint/lintProject.ts Normal file
View File

@@ -0,0 +1,12 @@
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
return await lintFolder(projectRoot)
}

69
src/lint/lintText.spec.ts Normal file
View 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
View 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
View 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
View 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
}

View File

@@ -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
View File

@@ -0,0 +1,6 @@
declare namespace NodeJS {
export interface Process {
projectDir: string
currentDir: string
}
}

View 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])
})
})