mirror of
https://github.com/sasjs/lint.git
synced 2025-12-10 17:34:36 +00:00
Compare commits
1 Commits
revert-acc
...
origin/ref
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33a57c3163 |
@@ -9,5 +9,6 @@
|
|||||||
"indentationMultiple": 2,
|
"indentationMultiple": 2,
|
||||||
"hasMacroNameInMend": true,
|
"hasMacroNameInMend": true,
|
||||||
"noNestedMacros": true,
|
"noNestedMacros": true,
|
||||||
"hasMacroParentheses": true
|
"hasMacroParentheses": true,
|
||||||
|
"lineEndings": "lf"
|
||||||
}
|
}
|
||||||
@@ -9,5 +9,5 @@ module.exports = {
|
|||||||
statements: -10
|
statements: -10
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
collectCoverageFrom: ['src/**/{!(index|example),}.ts']
|
collectCoverageFrom: ['src/**/{!(index|formatExample|lintExample),}.ts']
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export const format = (text: string) => {}
|
|
||||||
48
src/format/formatText.spec.ts
Normal file
48
src/format/formatText.spec.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { formatText } from './formatText'
|
||||||
|
import * as getLintConfigModule from '../utils/getLintConfig'
|
||||||
|
import { LintConfig } from '../types'
|
||||||
|
jest.mock('../utils/getLintConfig')
|
||||||
|
|
||||||
|
describe('formatText', () => {
|
||||||
|
it('should format the given text based on configured rules', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(getLintConfigModule, 'getLintConfig')
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve(
|
||||||
|
new LintConfig(getLintConfigModule.DefaultLintConfiguration)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
const text = `%macro test
|
||||||
|
%put 'hello';\r\n%mend; `
|
||||||
|
|
||||||
|
const expectedOutput = `/**
|
||||||
|
@file
|
||||||
|
@brief <Your brief here>
|
||||||
|
**/\n%macro test
|
||||||
|
%put 'hello';\n%mend;`
|
||||||
|
|
||||||
|
const output = await formatText(text)
|
||||||
|
|
||||||
|
expect(output).toEqual(expectedOutput)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use CRLF line endings when configured', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(getLintConfigModule, 'getLintConfig')
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve(
|
||||||
|
new LintConfig({
|
||||||
|
...getLintConfigModule.DefaultLintConfiguration,
|
||||||
|
lineEndings: 'crlf'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
const text = `%macro test\n %put 'hello';\r\n%mend; `
|
||||||
|
|
||||||
|
const expectedOutput = `/**\r\n @file\r\n @brief <Your brief here>\r\n**/\r\n%macro test\r\n %put 'hello';\r\n%mend;`
|
||||||
|
|
||||||
|
const output = await formatText(text)
|
||||||
|
|
||||||
|
expect(output).toEqual(expectedOutput)
|
||||||
|
})
|
||||||
|
})
|
||||||
7
src/format/formatText.ts
Normal file
7
src/format/formatText.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { getLintConfig } from '../utils'
|
||||||
|
import { processText } from './shared'
|
||||||
|
|
||||||
|
export const formatText = async (text: string) => {
|
||||||
|
const config = await getLintConfig()
|
||||||
|
return processText(text, config)
|
||||||
|
}
|
||||||
37
src/format/shared.ts
Normal file
37
src/format/shared.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { LintConfig } from '../types'
|
||||||
|
import { LineEndings } from '../types/LineEndings'
|
||||||
|
import { splitText } from '../utils/splitText'
|
||||||
|
|
||||||
|
export const processText = (text: string, config: LintConfig) => {
|
||||||
|
const processedText = processContent(config, text)
|
||||||
|
const lines = splitText(processedText, config)
|
||||||
|
const formattedLines = lines.map((line) => {
|
||||||
|
return processLine(config, line)
|
||||||
|
})
|
||||||
|
|
||||||
|
const configuredLineEnding =
|
||||||
|
config.lineEndings === LineEndings.LF ? '\n' : '\r\n'
|
||||||
|
return formattedLines.join(configuredLineEnding)
|
||||||
|
}
|
||||||
|
|
||||||
|
const processContent = (config: LintConfig, content: string): string => {
|
||||||
|
let processedContent = content
|
||||||
|
config.fileLintRules
|
||||||
|
.filter((r) => !!r.fix)
|
||||||
|
.forEach((rule) => {
|
||||||
|
processedContent = rule.fix!(processedContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
return processedContent
|
||||||
|
}
|
||||||
|
|
||||||
|
export const processLine = (config: LintConfig, line: string): string => {
|
||||||
|
let processedLine = line
|
||||||
|
config.lineLintRules
|
||||||
|
.filter((r) => !!r.fix)
|
||||||
|
.forEach((rule) => {
|
||||||
|
processedLine = rule.fix!(line)
|
||||||
|
})
|
||||||
|
|
||||||
|
return processedLine
|
||||||
|
}
|
||||||
21
src/formatExample.ts
Normal file
21
src/formatExample.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { formatText } from './format/formatText'
|
||||||
|
import { lintText } from './lint'
|
||||||
|
|
||||||
|
const content = `%put 'Hello';
|
||||||
|
%put 'World';
|
||||||
|
%macro somemacro()
|
||||||
|
%put 'test';
|
||||||
|
%mend;\r\n`
|
||||||
|
|
||||||
|
console.log(content)
|
||||||
|
lintText(content).then((diagnostics) => {
|
||||||
|
console.log('Before Formatting:')
|
||||||
|
console.table(diagnostics)
|
||||||
|
formatText(content).then((formattedText) => {
|
||||||
|
lintText(formattedText).then((newDiagnostics) => {
|
||||||
|
console.log('After Formatting:')
|
||||||
|
console.log(formattedText)
|
||||||
|
console.table(newDiagnostics)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -2,87 +2,68 @@ import { lintFile } from './lintFile'
|
|||||||
import { Severity } from '../types/Severity'
|
import { Severity } from '../types/Severity'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
const expectedDiagnostics = [
|
|
||||||
{
|
|
||||||
message: 'Line contains trailing spaces',
|
|
||||||
lineNumber: 1,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 2,
|
|
||||||
severity: Severity.Warning
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'Line contains trailing spaces',
|
|
||||||
lineNumber: 2,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 2,
|
|
||||||
severity: Severity.Warning
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'File name contains spaces',
|
|
||||||
lineNumber: 1,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'File name contains uppercase characters',
|
|
||||||
lineNumber: 1,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'File missing Doxygen header',
|
|
||||||
lineNumber: 1,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'Line contains encoded password',
|
|
||||||
lineNumber: 5,
|
|
||||||
startColumnNumber: 10,
|
|
||||||
endColumnNumber: 18,
|
|
||||||
severity: Severity.Error
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'Line is indented with a tab',
|
|
||||||
lineNumber: 7,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'Line has incorrect indentation - 3 spaces',
|
|
||||||
lineNumber: 6,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: '%mend statement is missing macro name - mf_getuniquelibref',
|
|
||||||
lineNumber: 17,
|
|
||||||
startColumnNumber: 3,
|
|
||||||
endColumnNumber: 9,
|
|
||||||
severity: Severity.Warning
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
describe('lintFile', () => {
|
describe('lintFile', () => {
|
||||||
it('should identify lint issues in a given file', async () => {
|
it('should identify lint issues in a given file', async () => {
|
||||||
const results = await lintFile(
|
const results = await lintFile(
|
||||||
path.join(__dirname, '..', 'Example File.sas')
|
path.join(__dirname, '..', 'Example File.sas')
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(results.length).toEqual(expectedDiagnostics.length)
|
expect(results.length).toEqual(8)
|
||||||
expect(results).toContainEqual(expectedDiagnostics[0])
|
expect(results).toContainEqual({
|
||||||
expect(results).toContainEqual(expectedDiagnostics[1])
|
message: 'Line contains trailing spaces',
|
||||||
expect(results).toContainEqual(expectedDiagnostics[2])
|
lineNumber: 1,
|
||||||
expect(results).toContainEqual(expectedDiagnostics[3])
|
startColumnNumber: 1,
|
||||||
expect(results).toContainEqual(expectedDiagnostics[4])
|
endColumnNumber: 2,
|
||||||
expect(results).toContainEqual(expectedDiagnostics[5])
|
severity: Severity.Warning
|
||||||
expect(results).toContainEqual(expectedDiagnostics[6])
|
})
|
||||||
expect(results).toContainEqual(expectedDiagnostics[7])
|
expect(results).toContainEqual({
|
||||||
expect(results).toContainEqual(expectedDiagnostics[8])
|
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
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,90 +2,70 @@ import { lintFolder } from './lintFolder'
|
|||||||
import { Severity } from '../types/Severity'
|
import { Severity } from '../types/Severity'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
const expectedFilesCount = 1
|
|
||||||
const expectedDiagnostics = [
|
|
||||||
{
|
|
||||||
message: 'Line contains trailing spaces',
|
|
||||||
lineNumber: 1,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 2,
|
|
||||||
severity: Severity.Warning
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'Line contains trailing spaces',
|
|
||||||
lineNumber: 2,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 2,
|
|
||||||
severity: Severity.Warning
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'File name contains spaces',
|
|
||||||
lineNumber: 1,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'File name contains uppercase characters',
|
|
||||||
lineNumber: 1,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'File missing Doxygen header',
|
|
||||||
lineNumber: 1,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'Line contains encoded password',
|
|
||||||
lineNumber: 5,
|
|
||||||
startColumnNumber: 10,
|
|
||||||
endColumnNumber: 18,
|
|
||||||
severity: Severity.Error
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'Line is indented with a tab',
|
|
||||||
lineNumber: 7,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'Line has incorrect indentation - 3 spaces',
|
|
||||||
lineNumber: 6,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: '%mend statement is missing macro name - mf_getuniquelibref',
|
|
||||||
lineNumber: 17,
|
|
||||||
startColumnNumber: 3,
|
|
||||||
endColumnNumber: 9,
|
|
||||||
severity: Severity.Warning
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
describe('lintFolder', () => {
|
describe('lintFolder', () => {
|
||||||
it('should identify lint issues in a given folder', async () => {
|
it('should identify lint issues in a given folder', async () => {
|
||||||
const results = await lintFolder(path.join(__dirname, '..'))
|
const results = await lintFolder(path.join(__dirname, '..'))
|
||||||
|
|
||||||
expect(results.size).toEqual(expectedFilesCount)
|
expect(results.size).toEqual(1)
|
||||||
const diagnostics = results.get(
|
const diagnostics = results.get(
|
||||||
path.join(__dirname, '..', 'Example File.sas')
|
path.join(__dirname, '..', 'Example File.sas')
|
||||||
)!
|
)!
|
||||||
expect(diagnostics.length).toEqual(expectedDiagnostics.length)
|
expect(diagnostics.length).toEqual(8)
|
||||||
expect(diagnostics).toContainEqual(expectedDiagnostics[0])
|
expect(diagnostics).toContainEqual({
|
||||||
expect(diagnostics).toContainEqual(expectedDiagnostics[1])
|
message: 'Line contains trailing spaces',
|
||||||
expect(diagnostics).toContainEqual(expectedDiagnostics[2])
|
lineNumber: 1,
|
||||||
expect(diagnostics).toContainEqual(expectedDiagnostics[3])
|
startColumnNumber: 1,
|
||||||
expect(diagnostics).toContainEqual(expectedDiagnostics[4])
|
endColumnNumber: 2,
|
||||||
expect(diagnostics).toContainEqual(expectedDiagnostics[5])
|
severity: Severity.Warning
|
||||||
expect(diagnostics).toContainEqual(expectedDiagnostics[6])
|
})
|
||||||
expect(diagnostics).toContainEqual(expectedDiagnostics[7])
|
expect(diagnostics).toContainEqual({
|
||||||
expect(diagnostics).toContainEqual(expectedDiagnostics[8])
|
message: 'Line contains trailing spaces',
|
||||||
|
lineNumber: 2,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 2,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
expect(diagnostics).toContainEqual({
|
||||||
|
message: 'File name contains spaces',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
expect(diagnostics).toContainEqual({
|
||||||
|
message: 'File name contains uppercase characters',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
expect(diagnostics).toContainEqual({
|
||||||
|
message: 'File missing Doxygen header',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
expect(diagnostics).toContainEqual({
|
||||||
|
message: 'Line contains encoded password',
|
||||||
|
lineNumber: 5,
|
||||||
|
startColumnNumber: 10,
|
||||||
|
endColumnNumber: 18,
|
||||||
|
severity: Severity.Error
|
||||||
|
})
|
||||||
|
expect(diagnostics).toContainEqual({
|
||||||
|
message: 'Line is indented with a tab',
|
||||||
|
lineNumber: 7,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
expect(diagnostics).toContainEqual({
|
||||||
|
message: 'Line has incorrect indentation - 3 spaces',
|
||||||
|
lineNumber: 6,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,102 +1,82 @@
|
|||||||
import { lintProject } from './lintProject'
|
import { lintProject } from './lintProject'
|
||||||
import { Severity } from '../types/Severity'
|
import { Severity } from '../types/Severity'
|
||||||
import * as utils from '../utils'
|
import * as getProjectRootModule from '../utils/getProjectRoot'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
jest.mock('../utils')
|
jest.mock('../utils/getProjectRoot')
|
||||||
|
|
||||||
const expectedFilesCount = 1
|
|
||||||
const expectedDiagnostics = [
|
|
||||||
{
|
|
||||||
message: 'Line contains trailing spaces',
|
|
||||||
lineNumber: 1,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 2,
|
|
||||||
severity: Severity.Warning
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'Line contains trailing spaces',
|
|
||||||
lineNumber: 2,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 2,
|
|
||||||
severity: Severity.Warning
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'File name contains spaces',
|
|
||||||
lineNumber: 1,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'File name contains uppercase characters',
|
|
||||||
lineNumber: 1,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'File missing Doxygen header',
|
|
||||||
lineNumber: 1,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'Line contains encoded password',
|
|
||||||
lineNumber: 5,
|
|
||||||
startColumnNumber: 10,
|
|
||||||
endColumnNumber: 18,
|
|
||||||
severity: Severity.Error
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'Line is indented with a tab',
|
|
||||||
lineNumber: 7,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'Line has incorrect indentation - 3 spaces',
|
|
||||||
lineNumber: 6,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: '%mend statement is missing macro name - mf_getuniquelibref',
|
|
||||||
lineNumber: 17,
|
|
||||||
startColumnNumber: 3,
|
|
||||||
endColumnNumber: 9,
|
|
||||||
severity: Severity.Warning
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
describe('lintProject', () => {
|
describe('lintProject', () => {
|
||||||
it('should identify lint issues in a given project', async () => {
|
it('should identify lint issues in a given project', async () => {
|
||||||
jest
|
jest
|
||||||
.spyOn(utils, 'getProjectRoot')
|
.spyOn(getProjectRootModule, 'getProjectRoot')
|
||||||
.mockImplementationOnce(() => Promise.resolve(path.join(__dirname, '..')))
|
.mockImplementation(() => Promise.resolve(path.join(__dirname, '..')))
|
||||||
const results = await lintProject()
|
const results = await lintProject()
|
||||||
|
|
||||||
expect(results.size).toEqual(expectedFilesCount)
|
expect(results.size).toEqual(1)
|
||||||
const diagnostics = results.get(
|
const diagnostics = results.get(
|
||||||
path.join(__dirname, '..', 'Example File.sas')
|
path.join(__dirname, '..', 'Example File.sas')
|
||||||
)!
|
)!
|
||||||
expect(diagnostics.length).toEqual(expectedDiagnostics.length)
|
expect(diagnostics.length).toEqual(8)
|
||||||
expect(diagnostics).toContainEqual(expectedDiagnostics[0])
|
expect(diagnostics).toContainEqual({
|
||||||
expect(diagnostics).toContainEqual(expectedDiagnostics[1])
|
message: 'Line contains trailing spaces',
|
||||||
expect(diagnostics).toContainEqual(expectedDiagnostics[2])
|
lineNumber: 1,
|
||||||
expect(diagnostics).toContainEqual(expectedDiagnostics[3])
|
startColumnNumber: 1,
|
||||||
expect(diagnostics).toContainEqual(expectedDiagnostics[4])
|
endColumnNumber: 2,
|
||||||
expect(diagnostics).toContainEqual(expectedDiagnostics[5])
|
severity: Severity.Warning
|
||||||
expect(diagnostics).toContainEqual(expectedDiagnostics[6])
|
})
|
||||||
expect(diagnostics).toContainEqual(expectedDiagnostics[7])
|
expect(diagnostics).toContainEqual({
|
||||||
expect(diagnostics).toContainEqual(expectedDiagnostics[8])
|
message: 'Line contains trailing spaces',
|
||||||
|
lineNumber: 2,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 2,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
expect(diagnostics).toContainEqual({
|
||||||
|
message: 'File name contains spaces',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
expect(diagnostics).toContainEqual({
|
||||||
|
message: 'File name contains uppercase characters',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
expect(diagnostics).toContainEqual({
|
||||||
|
message: 'File missing Doxygen header',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
expect(diagnostics).toContainEqual({
|
||||||
|
message: 'Line contains encoded password',
|
||||||
|
lineNumber: 5,
|
||||||
|
startColumnNumber: 10,
|
||||||
|
endColumnNumber: 18,
|
||||||
|
severity: Severity.Error
|
||||||
|
})
|
||||||
|
expect(diagnostics).toContainEqual({
|
||||||
|
message: 'Line is indented with a tab',
|
||||||
|
lineNumber: 7,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
expect(diagnostics).toContainEqual({
|
||||||
|
message: 'Line has incorrect indentation - 3 spaces',
|
||||||
|
lineNumber: 6,
|
||||||
|
startColumnNumber: 1,
|
||||||
|
endColumnNumber: 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw an error when a project root is not found', async () => {
|
it('should throw an error when a project root is not found', async () => {
|
||||||
jest
|
jest
|
||||||
.spyOn(utils, 'getProjectRoot')
|
.spyOn(getProjectRootModule, 'getProjectRoot')
|
||||||
.mockImplementationOnce(() => Promise.resolve(''))
|
.mockImplementationOnce(() => Promise.resolve(''))
|
||||||
|
|
||||||
await expect(lintProject()).rejects.toThrowError(
|
await expect(lintProject()).rejects.toThrowError(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getProjectRoot } from '../utils'
|
import { getProjectRoot } from '../utils/getProjectRoot'
|
||||||
import { lintFolder } from './lintFolder'
|
import { lintFolder } from './lintFolder'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -8,7 +8,6 @@ import { lintFolder } from './lintFolder'
|
|||||||
export const lintProject = async () => {
|
export const lintProject = async () => {
|
||||||
const projectRoot =
|
const projectRoot =
|
||||||
(await getProjectRoot()) || process.projectDir || process.currentDir
|
(await getProjectRoot()) || process.projectDir || process.currentDir
|
||||||
|
|
||||||
if (!projectRoot) {
|
if (!projectRoot) {
|
||||||
throw new Error('SASjs Project Root was not found.')
|
throw new Error('SASjs Project Root was not found.')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
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')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,17 +1,8 @@
|
|||||||
import { LintConfig, Diagnostic } from '../types'
|
import { LintConfig, Diagnostic } from '../types'
|
||||||
|
import { splitText } from '../utils'
|
||||||
/**
|
|
||||||
* 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) => {
|
export const processText = (text: string, config: LintConfig) => {
|
||||||
const lines = splitText(text)
|
const lines = splitText(text, config)
|
||||||
const diagnostics: Diagnostic[] = []
|
const diagnostics: Diagnostic[] = []
|
||||||
diagnostics.push(...processContent(config, text))
|
diagnostics.push(...processContent(config, text))
|
||||||
lines.forEach((line, index) => {
|
lines.forEach((line, index) => {
|
||||||
|
|||||||
@@ -1,58 +1,58 @@
|
|||||||
import { lintFile, lintText } from './lint'
|
import { lintFile, lintText } from './lint'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example which tests a piece of text with all known violations.
|
* Example which tests a piece of text with all known violations.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const text = `/**
|
const text = `/**
|
||||||
@file
|
@file
|
||||||
@brief Returns an unused libref
|
@brief Returns an unused libref
|
||||||
@details Use as follows:
|
@details Use as follows:
|
||||||
|
|
||||||
libname mclib0 (work);
|
libname mclib0 (work);
|
||||||
libname mclib1 (work);
|
libname mclib1 (work);
|
||||||
libname mclib2 (work);
|
libname mclib2 (work);
|
||||||
|
|
||||||
%let libref=%mf_getuniquelibref({SAS001});
|
%let libref=%mf_getuniquelibref({SAS001});
|
||||||
%put &=libref;
|
%put &=libref;
|
||||||
|
|
||||||
which returns:
|
which returns:
|
||||||
|
|
||||||
> mclib3
|
> mclib3
|
||||||
|
|
||||||
@param prefix= first part of libref. Remember that librefs can only be 8 characters,
|
@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.
|
so a 7 letter prefix would mean that maxtries should be 10.
|
||||||
@param maxtries= the last part of the libref. Provide an integer value.
|
@param maxtries= the last part of the libref. Provide an integer value.
|
||||||
|
|
||||||
@version 9.2
|
@version 9.2
|
||||||
@author Allan Bowe
|
@author Allan Bowe
|
||||||
**/
|
**/
|
||||||
|
|
||||||
|
|
||||||
%macro mf_getuniquelibref(prefix=mclib,maxtries=1000);
|
%macro mf_getuniquelibref(prefix=mclib,maxtries=1000);
|
||||||
%local x libref;
|
%local x libref;
|
||||||
%let x={SAS002};
|
%let x={SAS002};
|
||||||
%do x=0 %to &maxtries;
|
%do x=0 %to &maxtries;
|
||||||
%if %sysfunc(libref(&prefix&x)) ne 0 %then %do;
|
%if %sysfunc(libref(&prefix&x)) ne 0 %then %do;
|
||||||
%let libref=&prefix&x;
|
%let libref=&prefix&x;
|
||||||
%let rc=%sysfunc(libname(&libref,%sysfunc(pathname(work))));
|
%let rc=%sysfunc(libname(&libref,%sysfunc(pathname(work))));
|
||||||
%if &rc %then %put %sysfunc(sysmsg());
|
%if &rc %then %put %sysfunc(sysmsg());
|
||||||
&prefix&x
|
&prefix&x
|
||||||
%*put &sysmacroname: Libref &libref assigned as WORK and returned;
|
%*put &sysmacroname: Libref &libref assigned as WORK and returned;
|
||||||
%return;
|
%return;
|
||||||
%end;
|
%end;
|
||||||
%end;
|
%end;
|
||||||
%put unable to find available libref in range &prefix.0-&maxtries;
|
%put unable to find available libref in range &prefix.0-&maxtries;
|
||||||
%mend;
|
%mend;
|
||||||
`
|
`
|
||||||
|
|
||||||
lintText(text).then((diagnostics) => {
|
lintText(text).then((diagnostics) => {
|
||||||
console.log('Text lint results:')
|
console.log('Text lint results:')
|
||||||
console.table(diagnostics)
|
console.table(diagnostics)
|
||||||
})
|
})
|
||||||
|
|
||||||
lintFile(path.join(__dirname, 'Example File.sas')).then((diagnostics) => {
|
lintFile(path.join(__dirname, 'Example File.sas')).then((diagnostics) => {
|
||||||
console.log('File lint results:')
|
console.log('File lint results:')
|
||||||
console.table(diagnostics)
|
console.table(diagnostics)
|
||||||
})
|
})
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { LintConfig } from '../../types'
|
||||||
import { Severity } from '../../types/Severity'
|
import { Severity } from '../../types/Severity'
|
||||||
import { hasDoxygenHeader } from './hasDoxygenHeader'
|
import { hasDoxygenHeader } from './hasDoxygenHeader'
|
||||||
|
|
||||||
@@ -68,4 +69,43 @@ describe('hasDoxygenHeader', () => {
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should not alter the text if a doxygen header is already present', () => {
|
||||||
|
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.fix!(content)).toEqual(content)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should should add a doxygen header if not present', () => {
|
||||||
|
const content = `%macro mf_getuniquelibref(prefix=mclib,maxtries=1000);
|
||||||
|
%local x libref;
|
||||||
|
%let x={SAS002};
|
||||||
|
%do x=0 %to &maxtries;`
|
||||||
|
|
||||||
|
expect(hasDoxygenHeader.fix!(content)).toEqual(
|
||||||
|
`/**
|
||||||
|
@file
|
||||||
|
@brief <Your brief here>
|
||||||
|
**/` +
|
||||||
|
'\n' +
|
||||||
|
content
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use CRLF line endings when configured', () => {
|
||||||
|
const content = `%macro mf_getuniquelibref(prefix=mclib,maxtries=1000);\n%local x libref;\n%let x={SAS002};\n%do x=0 %to &maxtries;`
|
||||||
|
const config = new LintConfig({ lineEndings: 'crlf' })
|
||||||
|
|
||||||
|
expect(hasDoxygenHeader.fix!(content, config)).toEqual(
|
||||||
|
`/**\r\n @file\r\n @brief <Your brief here>\r\n**/` + '\r\n' + content
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
|
import { LintConfig } from '../../types'
|
||||||
|
import { LineEndings } from '../../types/LineEndings'
|
||||||
import { FileLintRule } from '../../types/LintRule'
|
import { FileLintRule } from '../../types/LintRule'
|
||||||
import { LintRuleType } from '../../types/LintRuleType'
|
import { LintRuleType } from '../../types/LintRuleType'
|
||||||
import { Severity } from '../../types/Severity'
|
import { Severity } from '../../types/Severity'
|
||||||
|
|
||||||
|
const DoxygenHeader = `/**{lineEnding} @file{lineEnding} @brief <Your brief here>{lineEnding}**/`
|
||||||
|
|
||||||
const name = 'hasDoxygenHeader'
|
const name = 'hasDoxygenHeader'
|
||||||
const description =
|
const description =
|
||||||
'Enforce the presence of a Doxygen header at the start of each file.'
|
'Enforce the presence of a Doxygen header at the start of each file.'
|
||||||
@@ -32,6 +36,19 @@ const test = (value: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fix = (value: string, config?: LintConfig): string => {
|
||||||
|
if (test(value).length === 0) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
const lineEndingConfig = config?.lineEndings || LineEndings.LF
|
||||||
|
const lineEnding = lineEndingConfig === LineEndings.LF ? '\n' : '\r\n'
|
||||||
|
|
||||||
|
return `${DoxygenHeader.replace(
|
||||||
|
/{lineEnding}/g,
|
||||||
|
lineEnding
|
||||||
|
)}${lineEnding}${value}`
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lint rule that checks for the presence of a Doxygen header in a given file.
|
* Lint rule that checks for the presence of a Doxygen header in a given file.
|
||||||
*/
|
*/
|
||||||
@@ -40,5 +57,6 @@ export const hasDoxygenHeader: FileLintRule = {
|
|||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
message,
|
message,
|
||||||
test
|
test,
|
||||||
|
fix
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,99 +2,97 @@ import { Diagnostic } from '../../types/Diagnostic'
|
|||||||
import { FileLintRule } from '../../types/LintRule'
|
import { FileLintRule } from '../../types/LintRule'
|
||||||
import { LintRuleType } from '../../types/LintRuleType'
|
import { LintRuleType } from '../../types/LintRuleType'
|
||||||
import { Severity } from '../../types/Severity'
|
import { Severity } from '../../types/Severity'
|
||||||
import { trimComments } from '../../utils/trimComments'
|
|
||||||
import { getColumnNumber } from '../../utils/getColumnNumber'
|
import { getColumnNumber } from '../../utils/getColumnNumber'
|
||||||
|
import { LintConfig } from '../../types'
|
||||||
|
import { LineEndings } from '../../types/LineEndings'
|
||||||
|
import { parseMacros } from '../../utils/parseMacros'
|
||||||
|
|
||||||
const name = 'hasMacroNameInMend'
|
const name = 'hasMacroNameInMend'
|
||||||
const description =
|
const description =
|
||||||
'Enforces the presence of the macro name in each %mend statement.'
|
'Enforces the presence of the macro name in each %mend statement.'
|
||||||
const message = '%mend statement has missing or incorrect macro name'
|
const message = '%mend statement has missing or incorrect macro name'
|
||||||
const test = (value: string) => {
|
const test = (value: string, config?: LintConfig) => {
|
||||||
|
const lineEnding = config?.lineEndings === LineEndings.CRLF ? '\r\n' : '\n'
|
||||||
|
const lines: string[] = value ? value.split(lineEnding) : []
|
||||||
|
const macros = parseMacros(value, config)
|
||||||
const diagnostics: Diagnostic[] = []
|
const diagnostics: Diagnostic[] = []
|
||||||
|
macros.forEach((macro) => {
|
||||||
const lines: string[] = value ? value.split('\n') : []
|
if (macro.startLineNumber === null && macro.endLineNumber !== null) {
|
||||||
|
diagnostics.push({
|
||||||
const declaredMacros: { name: string; lineNumber: number }[] = []
|
message: `%mend statement is redundant`,
|
||||||
let isCommentStarted = false
|
lineNumber: macro.endLineNumber,
|
||||||
lines.forEach((line, lineIndex) => {
|
startColumnNumber: getColumnNumber(
|
||||||
const { statement: trimmedLine, commentStarted } = trimComments(
|
lines[macro.endLineNumber - 1],
|
||||||
line,
|
'%mend'
|
||||||
isCommentStarted
|
),
|
||||||
)
|
endColumnNumber:
|
||||||
isCommentStarted = commentStarted
|
getColumnNumber(lines[macro.endLineNumber - 1], '%mend') +
|
||||||
const statements: string[] = trimmedLine ? trimmedLine.split(';') : []
|
lines[macro.endLineNumber - 1].trim().length -
|
||||||
|
1,
|
||||||
statements.forEach((statement) => {
|
severity: Severity.Warning
|
||||||
const { statement: trimmedStatement, commentStarted } = trimComments(
|
})
|
||||||
statement,
|
} else if (macro.endLineNumber === null && macro.startLineNumber !== null) {
|
||||||
isCommentStarted
|
diagnostics.push({
|
||||||
)
|
message: `Missing %mend statement for macro - ${macro.name}`,
|
||||||
isCommentStarted = commentStarted
|
lineNumber: macro.startLineNumber,
|
||||||
|
startColumnNumber: 1,
|
||||||
if (trimmedStatement.startsWith('%macro ')) {
|
endColumnNumber: 1,
|
||||||
const macroName = trimmedStatement
|
severity: Severity.Warning
|
||||||
.slice(7, trimmedStatement.length)
|
})
|
||||||
.trim()
|
} else if (macro.mismatchedMendMacroName) {
|
||||||
.split('(')[0]
|
diagnostics.push({
|
||||||
if (macroName)
|
message: `%mend statement has mismatched macro name, it should be '${
|
||||||
declaredMacros.push({
|
macro!.name
|
||||||
name: macroName,
|
}'`,
|
||||||
lineNumber: lineIndex + 1
|
lineNumber: macro.endLineNumber as number,
|
||||||
})
|
startColumnNumber: getColumnNumber(
|
||||||
} else if (trimmedStatement.startsWith('%mend')) {
|
lines[(macro.endLineNumber as number) - 1],
|
||||||
const declaredMacro = declaredMacros.pop()
|
macro.mismatchedMendMacroName
|
||||||
const macroName = trimmedStatement
|
),
|
||||||
.split(' ')
|
endColumnNumber:
|
||||||
.filter((s: string) => !!s)[1]
|
getColumnNumber(
|
||||||
|
lines[(macro.endLineNumber as number) - 1],
|
||||||
if (!declaredMacro) {
|
macro.mismatchedMendMacroName
|
||||||
diagnostics.push({
|
) +
|
||||||
message: `%mend statement is redundant`,
|
macro.mismatchedMendMacroName.length -
|
||||||
lineNumber: lineIndex + 1,
|
1,
|
||||||
startColumnNumber: getColumnNumber(line, '%mend'),
|
severity: Severity.Warning
|
||||||
endColumnNumber:
|
})
|
||||||
getColumnNumber(line, '%mend') + trimmedStatement.length,
|
} else if (!macro.hasMacroNameInMend) {
|
||||||
severity: Severity.Warning
|
diagnostics.push({
|
||||||
})
|
message: `%mend statement is missing macro name - ${macro.name}`,
|
||||||
} else if (!macroName) {
|
lineNumber: macro.endLineNumber as number,
|
||||||
diagnostics.push({
|
startColumnNumber: getColumnNumber(
|
||||||
message: `%mend statement is missing macro name - ${
|
lines[(macro.endLineNumber as number) - 1],
|
||||||
declaredMacro!.name
|
'%mend'
|
||||||
}`,
|
),
|
||||||
lineNumber: lineIndex + 1,
|
endColumnNumber:
|
||||||
startColumnNumber: getColumnNumber(line, '%mend'),
|
getColumnNumber(lines[(macro.endLineNumber as number) - 1], '%mend') +
|
||||||
endColumnNumber: getColumnNumber(line, '%mend') + 6,
|
6,
|
||||||
severity: Severity.Warning
|
severity: Severity.Warning
|
||||||
})
|
})
|
||||||
} else if (macroName !== declaredMacro!.name) {
|
}
|
||||||
diagnostics.push({
|
|
||||||
message: `%mend statement has mismatched macro name, it should be '${
|
|
||||||
declaredMacro!.name
|
|
||||||
}'`,
|
|
||||||
lineNumber: lineIndex + 1,
|
|
||||||
startColumnNumber: getColumnNumber(line, macroName),
|
|
||||||
endColumnNumber:
|
|
||||||
getColumnNumber(line, macroName) + macroName.length - 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
declaredMacros.forEach((declaredMacro) => {
|
|
||||||
diagnostics.push({
|
|
||||||
message: `Missing %mend statement for macro - ${declaredMacro.name}`,
|
|
||||||
lineNumber: declaredMacro.lineNumber,
|
|
||||||
startColumnNumber: 1,
|
|
||||||
endColumnNumber: 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return diagnostics
|
return diagnostics
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fix = (value: string, config?: LintConfig): string => {
|
||||||
|
const lineEnding = config?.lineEndings === LineEndings.CRLF ? '\r\n' : '\n'
|
||||||
|
let formattedText = value
|
||||||
|
const macros = parseMacros(value, config)
|
||||||
|
macros
|
||||||
|
.filter((macro) => !macro.hasMacroNameInMend)
|
||||||
|
.forEach((macro) => {
|
||||||
|
formattedText = formattedText.replace(
|
||||||
|
macro.termination,
|
||||||
|
`%mend ${macro.name};${lineEnding}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return formattedText
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lint rule that checks for the presence of macro name in %mend statement.
|
* Lint rule that checks for the presence of macro name in %mend statement.
|
||||||
*/
|
*/
|
||||||
@@ -103,5 +101,6 @@ export const hasMacroNameInMend: FileLintRule = {
|
|||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
message,
|
message,
|
||||||
test
|
test,
|
||||||
|
fix
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ describe('hasMacroParentheses', () => {
|
|||||||
%macro somemacro;
|
%macro somemacro;
|
||||||
%put &sysmacroname;
|
%put &sysmacroname;
|
||||||
%mend somemacro;`
|
%mend somemacro;`
|
||||||
|
|
||||||
expect(hasMacroParentheses.test(content)).toEqual([
|
expect(hasMacroParentheses.test(content)).toEqual([
|
||||||
{
|
{
|
||||||
message: 'Macro definition missing parentheses',
|
message: 'Macro definition missing parentheses',
|
||||||
@@ -28,7 +27,7 @@ describe('hasMacroParentheses', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return an array with a single diagnostics when macro defined without name', () => {
|
it('should return an array with a single diagnostic when macro defined without name', () => {
|
||||||
const content = `
|
const content = `
|
||||||
%macro ();
|
%macro ();
|
||||||
%put &sysmacroname;
|
%put &sysmacroname;
|
||||||
@@ -45,7 +44,7 @@ describe('hasMacroParentheses', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return an array with a single diagnostics when macro defined without name and parentheses', () => {
|
it('should return an array with a single diagnostic when macro defined without name and parentheses', () => {
|
||||||
const content = `
|
const content = `
|
||||||
%macro ;
|
%macro ;
|
||||||
%put &sysmacroname;
|
%put &sysmacroname;
|
||||||
@@ -56,7 +55,7 @@ describe('hasMacroParentheses', () => {
|
|||||||
message: 'Macro definition missing name',
|
message: 'Macro definition missing name',
|
||||||
lineNumber: 2,
|
lineNumber: 2,
|
||||||
startColumnNumber: 3,
|
startColumnNumber: 3,
|
||||||
endColumnNumber: 9,
|
endColumnNumber: 10,
|
||||||
severity: Severity.Warning
|
severity: Severity.Warning
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -2,74 +2,51 @@ import { Diagnostic } from '../../types/Diagnostic'
|
|||||||
import { FileLintRule } from '../../types/LintRule'
|
import { FileLintRule } from '../../types/LintRule'
|
||||||
import { LintRuleType } from '../../types/LintRuleType'
|
import { LintRuleType } from '../../types/LintRuleType'
|
||||||
import { Severity } from '../../types/Severity'
|
import { Severity } from '../../types/Severity'
|
||||||
import { trimComments } from '../../utils/trimComments'
|
|
||||||
import { getColumnNumber } from '../../utils/getColumnNumber'
|
import { getColumnNumber } from '../../utils/getColumnNumber'
|
||||||
|
import { parseMacros } from '../../utils/parseMacros'
|
||||||
|
import { LintConfig } from '../../types'
|
||||||
|
|
||||||
const name = 'hasMacroParentheses'
|
const name = 'hasMacroParentheses'
|
||||||
const description = 'Enforces the presence of parentheses in macro definitions.'
|
const description = 'Enforces the presence of parentheses in macro definitions.'
|
||||||
const message = 'Macro definition missing parentheses'
|
const message = 'Macro definition missing parentheses'
|
||||||
const test = (value: string) => {
|
const test = (value: string, config?: LintConfig) => {
|
||||||
const diagnostics: Diagnostic[] = []
|
const diagnostics: Diagnostic[] = []
|
||||||
|
const macros = parseMacros(value, config)
|
||||||
const lines: string[] = value ? value.split('\n') : []
|
macros.forEach((macro) => {
|
||||||
let isCommentStarted = false
|
if (!macro.name) {
|
||||||
lines.forEach((line, lineIndex) => {
|
diagnostics.push({
|
||||||
const { statement: trimmedLine, commentStarted } = trimComments(
|
message: 'Macro definition missing name',
|
||||||
line,
|
lineNumber: macro.startLineNumber!,
|
||||||
isCommentStarted
|
startColumnNumber: getColumnNumber(macro.declaration, '%macro'),
|
||||||
)
|
endColumnNumber: macro.declaration.length,
|
||||||
isCommentStarted = commentStarted
|
severity: Severity.Warning
|
||||||
const statements: string[] = trimmedLine ? trimmedLine.split(';') : []
|
})
|
||||||
|
} else if (!macro.declaration.includes('(')) {
|
||||||
statements.forEach((statement) => {
|
diagnostics.push({
|
||||||
const { statement: trimmedStatement, commentStarted } = trimComments(
|
message,
|
||||||
statement,
|
lineNumber: macro.startLineNumber!,
|
||||||
isCommentStarted
|
startColumnNumber: getColumnNumber(macro.declaration, macro.name),
|
||||||
)
|
endColumnNumber:
|
||||||
isCommentStarted = commentStarted
|
getColumnNumber(macro.declaration, macro.name) +
|
||||||
|
macro.name.length -
|
||||||
if (trimmedStatement.startsWith('%macro')) {
|
1,
|
||||||
const macroNameDefinition = trimmedStatement
|
severity: Severity.Warning
|
||||||
.slice(7, trimmedStatement.length)
|
})
|
||||||
.trim()
|
} else if (macro.name !== macro.name.trim()) {
|
||||||
|
diagnostics.push({
|
||||||
const macroNameDefinitionParts = macroNameDefinition.split('(')
|
message: 'Macro definition contains space(s)',
|
||||||
const macroName = macroNameDefinitionParts[0]
|
lineNumber: macro.startLineNumber!,
|
||||||
|
startColumnNumber: getColumnNumber(macro.declaration, macro.name),
|
||||||
if (!macroName)
|
endColumnNumber:
|
||||||
diagnostics.push({
|
getColumnNumber(macro.declaration, macro.name) +
|
||||||
message: 'Macro definition missing name',
|
macro.name.length -
|
||||||
lineNumber: lineIndex + 1,
|
1 +
|
||||||
startColumnNumber: getColumnNumber(line, '%macro'),
|
`()`.length,
|
||||||
endColumnNumber:
|
severity: Severity.Warning
|
||||||
getColumnNumber(line, '%macro') + trimmedStatement.length,
|
})
|
||||||
severity: Severity.Warning
|
}
|
||||||
})
|
|
||||||
else if (macroNameDefinitionParts.length === 1)
|
|
||||||
diagnostics.push({
|
|
||||||
message,
|
|
||||||
lineNumber: lineIndex + 1,
|
|
||||||
startColumnNumber: getColumnNumber(line, macroNameDefinition),
|
|
||||||
endColumnNumber:
|
|
||||||
getColumnNumber(line, macroNameDefinition) +
|
|
||||||
macroNameDefinition.length -
|
|
||||||
1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
else if (macroName !== macroName.trim())
|
|
||||||
diagnostics.push({
|
|
||||||
message: 'Macro definition contains space(s)',
|
|
||||||
lineNumber: lineIndex + 1,
|
|
||||||
startColumnNumber: getColumnNumber(line, macroNameDefinition),
|
|
||||||
endColumnNumber:
|
|
||||||
getColumnNumber(line, macroNameDefinition) +
|
|
||||||
macroNameDefinition.length -
|
|
||||||
1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return diagnostics
|
return diagnostics
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export { hasDoxygenHeader } from './hasDoxygenHeader'
|
export { hasDoxygenHeader } from './hasDoxygenHeader'
|
||||||
export { hasMacroNameInMend } from './hasMacroNameInMend'
|
export { hasMacroNameInMend } from './hasMacroNameInMend'
|
||||||
export { hasMacroParentheses } from './hasMacroParentheses'
|
export { hasMacroParentheses } from './hasMacroParentheses'
|
||||||
|
export { lineEndings } from './lineEndings'
|
||||||
export { noNestedMacros } from './noNestedMacros'
|
export { noNestedMacros } from './noNestedMacros'
|
||||||
|
|||||||
139
src/rules/file/lineEndings.spec.ts
Normal file
139
src/rules/file/lineEndings.spec.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { LintConfig, Severity } from '../../types'
|
||||||
|
import { LineEndings } from '../../types/LineEndings'
|
||||||
|
import { lineEndings } from './lineEndings'
|
||||||
|
|
||||||
|
describe('lineEndings', () => {
|
||||||
|
it('should return an empty array when the text contains the configured line endings', () => {
|
||||||
|
const text = "%put 'hello';\n%put 'world';\n"
|
||||||
|
const config = new LintConfig({ lineEndings: LineEndings.LF })
|
||||||
|
expect(lineEndings.test(text, config)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with a single diagnostic when a line is terminated with a CRLF ending', () => {
|
||||||
|
const text = "%put 'hello';\n%put 'world';\r\n"
|
||||||
|
const config = new LintConfig({ lineEndings: LineEndings.LF })
|
||||||
|
expect(lineEndings.test(text, config)).toEqual([
|
||||||
|
{
|
||||||
|
message: 'Incorrect line ending - CRLF instead of LF',
|
||||||
|
lineNumber: 2,
|
||||||
|
startColumnNumber: 13,
|
||||||
|
endColumnNumber: 14,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with a single diagnostic when a line is terminated with an LF ending', () => {
|
||||||
|
const text = "%put 'hello';\n%put 'world';\r\n"
|
||||||
|
const config = new LintConfig({ lineEndings: LineEndings.CRLF })
|
||||||
|
expect(lineEndings.test(text, config)).toEqual([
|
||||||
|
{
|
||||||
|
message: 'Incorrect line ending - LF instead of CRLF',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 13,
|
||||||
|
endColumnNumber: 14,
|
||||||
|
severity: Severity.Warning
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with a diagnostic for each line terminated with an LF ending', () => {
|
||||||
|
const text = "%put 'hello';\n%put 'test';\r\n%put 'world';\n"
|
||||||
|
const config = new LintConfig({ lineEndings: LineEndings.CRLF })
|
||||||
|
expect(lineEndings.test(text, config)).toContainEqual({
|
||||||
|
message: 'Incorrect line ending - LF instead of CRLF',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 13,
|
||||||
|
endColumnNumber: 14,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
expect(lineEndings.test(text, config)).toContainEqual({
|
||||||
|
message: 'Incorrect line ending - LF instead of CRLF',
|
||||||
|
lineNumber: 3,
|
||||||
|
startColumnNumber: 13,
|
||||||
|
endColumnNumber: 14,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with a diagnostic for each line terminated with a CRLF ending', () => {
|
||||||
|
const text = "%put 'hello';\r\n%put 'test';\n%put 'world';\r\n"
|
||||||
|
const config = new LintConfig({ lineEndings: LineEndings.LF })
|
||||||
|
expect(lineEndings.test(text, config)).toContainEqual({
|
||||||
|
message: 'Incorrect line ending - CRLF instead of LF',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 13,
|
||||||
|
endColumnNumber: 14,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
expect(lineEndings.test(text, config)).toContainEqual({
|
||||||
|
message: 'Incorrect line ending - CRLF instead of LF',
|
||||||
|
lineNumber: 3,
|
||||||
|
startColumnNumber: 13,
|
||||||
|
endColumnNumber: 14,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with a diagnostic for lines terminated with a CRLF ending', () => {
|
||||||
|
const text =
|
||||||
|
"%put 'hello';\r\n%put 'test';\r\n%put 'world';\n%put 'test2';\n%put 'world2';\r\n"
|
||||||
|
const config = new LintConfig({ lineEndings: LineEndings.LF })
|
||||||
|
expect(lineEndings.test(text, config)).toContainEqual({
|
||||||
|
message: 'Incorrect line ending - CRLF instead of LF',
|
||||||
|
lineNumber: 1,
|
||||||
|
startColumnNumber: 13,
|
||||||
|
endColumnNumber: 14,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
expect(lineEndings.test(text, config)).toContainEqual({
|
||||||
|
message: 'Incorrect line ending - CRLF instead of LF',
|
||||||
|
lineNumber: 2,
|
||||||
|
startColumnNumber: 12,
|
||||||
|
endColumnNumber: 13,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
expect(lineEndings.test(text, config)).toContainEqual({
|
||||||
|
message: 'Incorrect line ending - CRLF instead of LF',
|
||||||
|
lineNumber: 5,
|
||||||
|
startColumnNumber: 14,
|
||||||
|
endColumnNumber: 15,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should transform line endings to LF', () => {
|
||||||
|
const text =
|
||||||
|
"%put 'hello';\r\n%put 'test';\r\n%put 'world';\n%put 'test2';\n%put 'world2';\r\n"
|
||||||
|
const config = new LintConfig({ lineEndings: LineEndings.LF })
|
||||||
|
|
||||||
|
const formattedText = lineEndings.fix!(text, config)
|
||||||
|
|
||||||
|
expect(formattedText).toEqual(
|
||||||
|
"%put 'hello';\n%put 'test';\n%put 'world';\n%put 'test2';\n%put 'world2';\n"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should transform line endings to CRLF', () => {
|
||||||
|
const text =
|
||||||
|
"%put 'hello';\r\n%put 'test';\r\n%put 'world';\n%put 'test2';\n%put 'world2';\r\n"
|
||||||
|
const config = new LintConfig({ lineEndings: LineEndings.CRLF })
|
||||||
|
|
||||||
|
const formattedText = lineEndings.fix!(text, config)
|
||||||
|
|
||||||
|
expect(formattedText).toEqual(
|
||||||
|
"%put 'hello';\r\n%put 'test';\r\n%put 'world';\r\n%put 'test2';\r\n%put 'world2';\r\n"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use LF line endings by default', () => {
|
||||||
|
const text =
|
||||||
|
"%put 'hello';\r\n%put 'test';\r\n%put 'world';\n%put 'test2';\n%put 'world2';\r\n"
|
||||||
|
|
||||||
|
const formattedText = lineEndings.fix!(text)
|
||||||
|
|
||||||
|
expect(formattedText).toEqual(
|
||||||
|
"%put 'hello';\n%put 'test';\n%put 'world';\n%put 'test2';\n%put 'world2';\n"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
83
src/rules/file/lineEndings.ts
Normal file
83
src/rules/file/lineEndings.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { Diagnostic, LintConfig } from '../../types'
|
||||||
|
import { LineEndings } from '../../types/LineEndings'
|
||||||
|
import { FileLintRule } from '../../types/LintRule'
|
||||||
|
import { LintRuleType } from '../../types/LintRuleType'
|
||||||
|
import { Severity } from '../../types/Severity'
|
||||||
|
|
||||||
|
const name = 'lineEndings'
|
||||||
|
const description = 'Ensures line endings conform to the configured type.'
|
||||||
|
const message = 'Incorrect line ending - {actual} instead of {expected}'
|
||||||
|
const test = (value: string, config?: LintConfig) => {
|
||||||
|
const lineEndingConfig = config?.lineEndings || LineEndings.LF
|
||||||
|
const expectedLineEnding =
|
||||||
|
lineEndingConfig === LineEndings.LF ? '{lf}' : '{crlf}'
|
||||||
|
const incorrectLineEnding = expectedLineEnding === '{lf}' ? '{crlf}' : '{lf}'
|
||||||
|
|
||||||
|
const lines = value
|
||||||
|
.replace(/\r\n/g, '{crlf}')
|
||||||
|
.replace(/\n/g, '{lf}')
|
||||||
|
.split(new RegExp(`(?<=${expectedLineEnding})`))
|
||||||
|
const diagnostics: Diagnostic[] = []
|
||||||
|
|
||||||
|
let indexOffset = 0
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
if (line.endsWith(incorrectLineEnding)) {
|
||||||
|
diagnostics.push({
|
||||||
|
message: message
|
||||||
|
.replace('{expected}', expectedLineEnding === '{lf}' ? 'LF' : 'CRLF')
|
||||||
|
.replace('{actual}', incorrectLineEnding === '{lf}' ? 'LF' : 'CRLF'),
|
||||||
|
lineNumber: index + 1 + indexOffset,
|
||||||
|
startColumnNumber: line.indexOf(incorrectLineEnding),
|
||||||
|
endColumnNumber: line.indexOf(incorrectLineEnding) + 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const splitLine = line.split(new RegExp(`(?<=${incorrectLineEnding})`))
|
||||||
|
if (splitLine.length > 1) {
|
||||||
|
indexOffset += splitLine.length - 1
|
||||||
|
}
|
||||||
|
splitLine.forEach((l, i) => {
|
||||||
|
if (l.endsWith(incorrectLineEnding)) {
|
||||||
|
diagnostics.push({
|
||||||
|
message: message
|
||||||
|
.replace(
|
||||||
|
'{expected}',
|
||||||
|
expectedLineEnding === '{lf}' ? 'LF' : 'CRLF'
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
'{actual}',
|
||||||
|
incorrectLineEnding === '{lf}' ? 'LF' : 'CRLF'
|
||||||
|
),
|
||||||
|
lineNumber: index + i + 1,
|
||||||
|
startColumnNumber: l.indexOf(incorrectLineEnding),
|
||||||
|
endColumnNumber: l.indexOf(incorrectLineEnding) + 1,
|
||||||
|
severity: Severity.Warning
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
const fix = (value: string, config?: LintConfig): string => {
|
||||||
|
const lineEndingConfig = config?.lineEndings || LineEndings.LF
|
||||||
|
|
||||||
|
return value
|
||||||
|
.replace(/\r\n/g, '{crlf}')
|
||||||
|
.replace(/\n/g, '{lf}')
|
||||||
|
.replace(/{crlf}/g, lineEndingConfig === LineEndings.LF ? '\n' : '\r\n')
|
||||||
|
.replace(/{lf}/g, lineEndingConfig === LineEndings.LF ? '\n' : '\r\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lint rule that checks if line endings in a file match the configured type.
|
||||||
|
*/
|
||||||
|
export const lineEndings: FileLintRule = {
|
||||||
|
type: LintRuleType.File,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
message,
|
||||||
|
test,
|
||||||
|
fix
|
||||||
|
}
|
||||||
@@ -29,13 +29,13 @@ describe('noNestedMacros', () => {
|
|||||||
message: "Macro definition for 'inner' present in macro 'outer'",
|
message: "Macro definition for 'inner' present in macro 'outer'",
|
||||||
lineNumber: 4,
|
lineNumber: 4,
|
||||||
startColumnNumber: 7,
|
startColumnNumber: 7,
|
||||||
endColumnNumber: 20,
|
endColumnNumber: 21,
|
||||||
severity: Severity.Warning
|
severity: Severity.Warning
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return an array with a single diagnostic when nested macros are defined at 2 levels', () => {
|
it('should return an array with two diagnostics when nested macros are defined at 2 levels', () => {
|
||||||
const content = `
|
const content = `
|
||||||
%macro outer();
|
%macro outer();
|
||||||
/* any amount of arbitrary code */
|
/* any amount of arbitrary code */
|
||||||
@@ -52,22 +52,20 @@ describe('noNestedMacros', () => {
|
|||||||
|
|
||||||
%outer()`
|
%outer()`
|
||||||
|
|
||||||
expect(noNestedMacros.test(content)).toEqual([
|
expect(noNestedMacros.test(content)).toContainEqual({
|
||||||
{
|
message: "Macro definition for 'inner' present in macro 'outer'",
|
||||||
message: "Macro definition for 'inner' present in macro 'outer'",
|
lineNumber: 4,
|
||||||
lineNumber: 4,
|
startColumnNumber: 7,
|
||||||
startColumnNumber: 7,
|
endColumnNumber: 21,
|
||||||
endColumnNumber: 20,
|
severity: Severity.Warning
|
||||||
severity: Severity.Warning
|
})
|
||||||
},
|
expect(noNestedMacros.test(content)).toContainEqual({
|
||||||
{
|
message: "Macro definition for 'inner2' present in macro 'inner'",
|
||||||
message: "Macro definition for 'inner2' present in macro 'inner'",
|
lineNumber: 7,
|
||||||
lineNumber: 7,
|
startColumnNumber: 17,
|
||||||
startColumnNumber: 17,
|
endColumnNumber: 32,
|
||||||
endColumnNumber: 31,
|
severity: Severity.Warning
|
||||||
severity: Severity.Warning
|
})
|
||||||
}
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return an empty array when the file is undefined', () => {
|
it('should return an empty array when the file is undefined', () => {
|
||||||
|
|||||||
@@ -2,57 +2,41 @@ import { Diagnostic } from '../../types/Diagnostic'
|
|||||||
import { FileLintRule } from '../../types/LintRule'
|
import { FileLintRule } from '../../types/LintRule'
|
||||||
import { LintRuleType } from '../../types/LintRuleType'
|
import { LintRuleType } from '../../types/LintRuleType'
|
||||||
import { Severity } from '../../types/Severity'
|
import { Severity } from '../../types/Severity'
|
||||||
import { trimComments } from '../../utils/trimComments'
|
|
||||||
import { getColumnNumber } from '../../utils/getColumnNumber'
|
import { getColumnNumber } from '../../utils/getColumnNumber'
|
||||||
|
import { parseMacros } from '../../utils/parseMacros'
|
||||||
|
import { LintConfig } from '../../types'
|
||||||
|
import { LineEndings } from '../../types/LineEndings'
|
||||||
|
|
||||||
const name = 'noNestedMacros'
|
const name = 'noNestedMacros'
|
||||||
const description = 'Enfoces the absence of nested macro definitions.'
|
const description = 'Enfoces the absence of nested macro definitions.'
|
||||||
const message = `Macro definition for '{macro}' present in macro '{parent}'`
|
const message = `Macro definition for '{macro}' present in macro '{parent}'`
|
||||||
const test = (value: string) => {
|
const test = (value: string, config?: LintConfig) => {
|
||||||
|
const lineEnding = config?.lineEndings === LineEndings.CRLF ? '\r\n' : '\n'
|
||||||
|
const lines: string[] = value ? value.split(lineEnding) : []
|
||||||
const diagnostics: Diagnostic[] = []
|
const diagnostics: Diagnostic[] = []
|
||||||
const declaredMacros: string[] = []
|
const macros = parseMacros(value, config)
|
||||||
|
macros
|
||||||
const lines: string[] = value ? value.split('\n') : []
|
.filter((m) => !!m.parentMacro)
|
||||||
let isCommentStarted = false
|
.forEach((macro) => {
|
||||||
lines.forEach((line, lineIndex) => {
|
diagnostics.push({
|
||||||
const { statement: trimmedLine, commentStarted } = trimComments(
|
message: message
|
||||||
line,
|
.replace('{macro}', macro.name)
|
||||||
isCommentStarted
|
.replace('{parent}', macro.parentMacro),
|
||||||
)
|
lineNumber: macro.startLineNumber as number,
|
||||||
isCommentStarted = commentStarted
|
startColumnNumber: getColumnNumber(
|
||||||
const statements: string[] = trimmedLine ? trimmedLine.split(';') : []
|
lines[(macro.startLineNumber as number) - 1],
|
||||||
|
'%macro'
|
||||||
statements.forEach((statement) => {
|
),
|
||||||
const { statement: trimmedStatement, commentStarted } = trimComments(
|
endColumnNumber:
|
||||||
statement,
|
getColumnNumber(
|
||||||
isCommentStarted
|
lines[(macro.startLineNumber as number) - 1],
|
||||||
)
|
'%macro'
|
||||||
isCommentStarted = commentStarted
|
) +
|
||||||
|
lines[(macro.startLineNumber as number) - 1].trim().length -
|
||||||
if (trimmedStatement.startsWith('%macro ')) {
|
1,
|
||||||
const macroName = trimmedStatement
|
severity: Severity.Warning
|
||||||
.slice(7, trimmedStatement.length)
|
})
|
||||||
.trim()
|
|
||||||
.split('(')[0]
|
|
||||||
if (declaredMacros.length) {
|
|
||||||
const parentMacro = declaredMacros.slice(-1).pop()
|
|
||||||
diagnostics.push({
|
|
||||||
message: message
|
|
||||||
.replace('{macro}', macroName)
|
|
||||||
.replace('{parent}', parentMacro!),
|
|
||||||
lineNumber: lineIndex + 1,
|
|
||||||
startColumnNumber: getColumnNumber(line, '%macro'),
|
|
||||||
endColumnNumber:
|
|
||||||
getColumnNumber(line, '%macro') + trimmedStatement.length - 1,
|
|
||||||
severity: Severity.Warning
|
|
||||||
})
|
|
||||||
}
|
|
||||||
declaredMacros.push(macroName)
|
|
||||||
} else if (trimmedStatement.startsWith('%mend')) {
|
|
||||||
declaredMacros.pop()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
|
||||||
return diagnostics
|
return diagnostics
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const test = (value: string, lineNumber: number) =>
|
|||||||
severity: Severity.Warning
|
severity: Severity.Warning
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
const fix = (value: string) => value.trimEnd()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lint rule that checks for the presence of trailing space(s) in a given line of text.
|
* Lint rule that checks for the presence of trailing space(s) in a given line of text.
|
||||||
@@ -26,5 +27,6 @@ export const noTrailingSpaces: LineLintRule = {
|
|||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
message,
|
message,
|
||||||
test
|
test,
|
||||||
|
fix
|
||||||
}
|
}
|
||||||
|
|||||||
4
src/types/LineEndings.ts
Normal file
4
src/types/LineEndings.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export enum LineEndings {
|
||||||
|
LF = 'lf',
|
||||||
|
CRLF = 'crlf'
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { LineEndings } from './LineEndings'
|
||||||
import { LintConfig } from './LintConfig'
|
import { LintConfig } from './LintConfig'
|
||||||
import { LintRuleType } from './LintRuleType'
|
import { LintRuleType } from './LintRuleType'
|
||||||
|
|
||||||
@@ -108,6 +109,33 @@ describe('LintConfig', () => {
|
|||||||
expect(config.indentationMultiple).toEqual(0)
|
expect(config.indentationMultiple).toEqual(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should create an instance with the line endings set to LF', () => {
|
||||||
|
const config = new LintConfig({ lineEndings: 'lf' })
|
||||||
|
|
||||||
|
expect(config).toBeTruthy()
|
||||||
|
expect(config.lineEndings).toEqual(LineEndings.LF)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create an instance with the line endings set to CRLF', () => {
|
||||||
|
const config = new LintConfig({ lineEndings: 'crlf' })
|
||||||
|
|
||||||
|
expect(config).toBeTruthy()
|
||||||
|
expect(config.lineEndings).toEqual(LineEndings.CRLF)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create an instance with the line endings set to LF by default', () => {
|
||||||
|
const config = new LintConfig({})
|
||||||
|
|
||||||
|
expect(config).toBeTruthy()
|
||||||
|
expect(config.lineEndings).toEqual(LineEndings.LF)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw an error with an invalid value for line endings', () => {
|
||||||
|
expect(() => new LintConfig({ lineEndings: 'test' })).toThrowError(
|
||||||
|
`Invalid value for lineEndings: can be ${LineEndings.LF} or ${LineEndings.CRLF}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('should create an instance with all flags set', () => {
|
it('should create an instance with all flags set', () => {
|
||||||
const config = new LintConfig({
|
const config = new LintConfig({
|
||||||
noTrailingSpaces: true,
|
noTrailingSpaces: true,
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import {
|
|||||||
hasDoxygenHeader,
|
hasDoxygenHeader,
|
||||||
hasMacroNameInMend,
|
hasMacroNameInMend,
|
||||||
noNestedMacros,
|
noNestedMacros,
|
||||||
hasMacroParentheses
|
hasMacroParentheses,
|
||||||
|
lineEndings
|
||||||
} from '../rules/file'
|
} from '../rules/file'
|
||||||
import {
|
import {
|
||||||
indentationMultiple,
|
indentationMultiple,
|
||||||
@@ -12,6 +13,7 @@ import {
|
|||||||
noTrailingSpaces
|
noTrailingSpaces
|
||||||
} from '../rules/line'
|
} from '../rules/line'
|
||||||
import { lowerCaseFileNames, noSpacesInFileNames } from '../rules/path'
|
import { lowerCaseFileNames, noSpacesInFileNames } from '../rules/path'
|
||||||
|
import { LineEndings } from './LineEndings'
|
||||||
import { FileLintRule, LineLintRule, PathLintRule } from './LintRule'
|
import { FileLintRule, LineLintRule, PathLintRule } from './LintRule'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,6 +29,7 @@ export class LintConfig {
|
|||||||
readonly pathLintRules: PathLintRule[] = []
|
readonly pathLintRules: PathLintRule[] = []
|
||||||
readonly maxLineLength: number = 80
|
readonly maxLineLength: number = 80
|
||||||
readonly indentationMultiple: number = 2
|
readonly indentationMultiple: number = 2
|
||||||
|
readonly lineEndings: LineEndings = LineEndings.LF
|
||||||
|
|
||||||
constructor(json?: any) {
|
constructor(json?: any) {
|
||||||
if (json?.noTrailingSpaces) {
|
if (json?.noTrailingSpaces) {
|
||||||
@@ -46,6 +49,19 @@ export class LintConfig {
|
|||||||
this.lineLintRules.push(maxLineLength)
|
this.lineLintRules.push(maxLineLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (json?.lineEndings) {
|
||||||
|
if (
|
||||||
|
json.lineEndings !== LineEndings.LF &&
|
||||||
|
json.lineEndings !== LineEndings.CRLF
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid value for lineEndings: can be ${LineEndings.LF} or ${LineEndings.CRLF}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.lineEndings = json.lineEndings
|
||||||
|
this.fileLintRules.push(lineEndings)
|
||||||
|
}
|
||||||
|
|
||||||
if (!isNaN(json?.indentationMultiple)) {
|
if (!isNaN(json?.indentationMultiple)) {
|
||||||
this.indentationMultiple = json.indentationMultiple as number
|
this.indentationMultiple = json.indentationMultiple as number
|
||||||
this.lineLintRules.push(indentationMultiple)
|
this.lineLintRules.push(indentationMultiple)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export interface LintRule {
|
|||||||
export interface LineLintRule extends LintRule {
|
export interface LineLintRule extends LintRule {
|
||||||
type: LintRuleType.Line
|
type: LintRuleType.Line
|
||||||
test: (value: string, lineNumber: number, config?: LintConfig) => Diagnostic[]
|
test: (value: string, lineNumber: number, config?: LintConfig) => Diagnostic[]
|
||||||
|
fix?: (value: string, config?: LintConfig) => string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,7 +27,8 @@ export interface LineLintRule extends LintRule {
|
|||||||
*/
|
*/
|
||||||
export interface FileLintRule extends LintRule {
|
export interface FileLintRule extends LintRule {
|
||||||
type: LintRuleType.File
|
type: LintRuleType.File
|
||||||
test: (value: string) => Diagnostic[]
|
test: (value: string, config?: LintConfig) => Diagnostic[]
|
||||||
|
fix?: (value: string, config?: LintConfig) => string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,10 +2,6 @@ import * as fileModule from '@sasjs/utils/file'
|
|||||||
import { LintConfig } from '../types/LintConfig'
|
import { LintConfig } from '../types/LintConfig'
|
||||||
import { getLintConfig } from './getLintConfig'
|
import { getLintConfig } from './getLintConfig'
|
||||||
|
|
||||||
const expectedFileLintRulesCount = 4
|
|
||||||
const expectedLineLintRulesCount = 5
|
|
||||||
const expectedPathLintRulesCount = 2
|
|
||||||
|
|
||||||
describe('getLintConfig', () => {
|
describe('getLintConfig', () => {
|
||||||
it('should get the lint config', async () => {
|
it('should get the lint config', async () => {
|
||||||
const config = await getLintConfig()
|
const config = await getLintConfig()
|
||||||
@@ -21,8 +17,8 @@ describe('getLintConfig', () => {
|
|||||||
const config = await getLintConfig()
|
const config = await getLintConfig()
|
||||||
|
|
||||||
expect(config).toBeInstanceOf(LintConfig)
|
expect(config).toBeInstanceOf(LintConfig)
|
||||||
expect(config.fileLintRules.length).toEqual(expectedFileLintRulesCount)
|
expect(config.fileLintRules.length).toEqual(3)
|
||||||
expect(config.lineLintRules.length).toEqual(expectedLineLintRulesCount)
|
expect(config.lineLintRules.length).toEqual(5)
|
||||||
expect(config.pathLintRules.length).toEqual(expectedPathLintRulesCount)
|
expect(config.pathLintRules.length).toEqual(2)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const DefaultLintConfiguration = {
|
|||||||
maxLineLength: 80,
|
maxLineLength: 80,
|
||||||
noTabIndentation: true,
|
noTabIndentation: true,
|
||||||
indentationMultiple: 2,
|
indentationMultiple: 2,
|
||||||
hasMacroNameInMend: true,
|
hasMacroNameInMend: false,
|
||||||
noNestedMacros: true,
|
noNestedMacros: true,
|
||||||
hasMacroParentheses: true
|
hasMacroParentheses: true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './getLintConfig'
|
export * from './getLintConfig'
|
||||||
export * from './getProjectRoot'
|
export * from './getProjectRoot'
|
||||||
export * from './listSasFiles'
|
export * from './listSasFiles'
|
||||||
|
export * from './splitText'
|
||||||
|
|||||||
95
src/utils/parseMacros.spec.ts
Normal file
95
src/utils/parseMacros.spec.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { LintConfig } from '../types'
|
||||||
|
import { parseMacros } from './parseMacros'
|
||||||
|
|
||||||
|
describe('parseMacros', () => {
|
||||||
|
it('should return an array with a single macro', () => {
|
||||||
|
const text = `%macro test;
|
||||||
|
%put 'hello';
|
||||||
|
%mend`
|
||||||
|
|
||||||
|
const macros = parseMacros(text, new LintConfig())
|
||||||
|
|
||||||
|
expect(macros.length).toEqual(1)
|
||||||
|
expect(macros).toContainEqual({
|
||||||
|
name: 'test',
|
||||||
|
declaration: '%macro test;',
|
||||||
|
termination: '%mend',
|
||||||
|
startLineNumber: 1,
|
||||||
|
endLineNumber: 3,
|
||||||
|
parentMacro: '',
|
||||||
|
hasMacroNameInMend: false,
|
||||||
|
hasParentheses: false,
|
||||||
|
mismatchedMendMacroName: ''
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array with multiple macros', () => {
|
||||||
|
const text = `%macro foo;
|
||||||
|
%put 'foo';
|
||||||
|
%mend;
|
||||||
|
%macro bar();
|
||||||
|
%put 'bar';
|
||||||
|
%mend bar;`
|
||||||
|
|
||||||
|
const macros = parseMacros(text, new LintConfig())
|
||||||
|
|
||||||
|
expect(macros.length).toEqual(2)
|
||||||
|
expect(macros).toContainEqual({
|
||||||
|
name: 'foo',
|
||||||
|
declaration: '%macro foo;',
|
||||||
|
termination: '%mend;',
|
||||||
|
startLineNumber: 1,
|
||||||
|
endLineNumber: 3,
|
||||||
|
parentMacro: '',
|
||||||
|
hasMacroNameInMend: false,
|
||||||
|
hasParentheses: false,
|
||||||
|
mismatchedMendMacroName: ''
|
||||||
|
})
|
||||||
|
expect(macros).toContainEqual({
|
||||||
|
name: 'bar',
|
||||||
|
declaration: '%macro bar();',
|
||||||
|
termination: '%mend bar;',
|
||||||
|
startLineNumber: 4,
|
||||||
|
endLineNumber: 6,
|
||||||
|
parentMacro: '',
|
||||||
|
hasMacroNameInMend: true,
|
||||||
|
hasParentheses: true,
|
||||||
|
mismatchedMendMacroName: ''
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should detect nested macro definitions', () => {
|
||||||
|
const text = `%macro test()
|
||||||
|
%put 'hello';
|
||||||
|
%macro test2
|
||||||
|
%put 'world;
|
||||||
|
%mend
|
||||||
|
%mend test`
|
||||||
|
|
||||||
|
const macros = parseMacros(text, new LintConfig())
|
||||||
|
|
||||||
|
expect(macros.length).toEqual(2)
|
||||||
|
expect(macros).toContainEqual({
|
||||||
|
name: 'test',
|
||||||
|
declaration: '%macro test()',
|
||||||
|
termination: '%mend test',
|
||||||
|
startLineNumber: 1,
|
||||||
|
endLineNumber: 6,
|
||||||
|
parentMacro: '',
|
||||||
|
hasMacroNameInMend: true,
|
||||||
|
hasParentheses: true,
|
||||||
|
mismatchedMendMacroName: ''
|
||||||
|
})
|
||||||
|
expect(macros).toContainEqual({
|
||||||
|
name: 'test2',
|
||||||
|
declaration: ' %macro test2',
|
||||||
|
termination: ' %mend',
|
||||||
|
startLineNumber: 3,
|
||||||
|
endLineNumber: 5,
|
||||||
|
parentMacro: 'test',
|
||||||
|
hasMacroNameInMend: false,
|
||||||
|
hasParentheses: false,
|
||||||
|
mismatchedMendMacroName: ''
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
90
src/utils/parseMacros.ts
Normal file
90
src/utils/parseMacros.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { LintConfig } from '../types/LintConfig'
|
||||||
|
import { LineEndings } from '../types/LineEndings'
|
||||||
|
import { trimComments } from './trimComments'
|
||||||
|
|
||||||
|
interface Macro {
|
||||||
|
name: string
|
||||||
|
startLineNumber: number | null
|
||||||
|
endLineNumber: number | null
|
||||||
|
declaration: string
|
||||||
|
termination: string
|
||||||
|
parentMacro: string
|
||||||
|
hasMacroNameInMend: boolean
|
||||||
|
hasParentheses: boolean
|
||||||
|
mismatchedMendMacroName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseMacros = (text: string, config?: LintConfig): Macro[] => {
|
||||||
|
const lineEnding = config?.lineEndings === LineEndings.CRLF ? '\r\n' : '\n'
|
||||||
|
const lines: string[] = text ? text.split(lineEnding) : []
|
||||||
|
const macros: Macro[] = []
|
||||||
|
|
||||||
|
let isCommentStarted = false
|
||||||
|
let macroStack: Macro[] = []
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
const { statement: trimmedLine, commentStarted } = trimComments(
|
||||||
|
line,
|
||||||
|
isCommentStarted
|
||||||
|
)
|
||||||
|
isCommentStarted = commentStarted
|
||||||
|
const statements: string[] = trimmedLine ? trimmedLine.split(';') : []
|
||||||
|
|
||||||
|
statements.forEach((statement) => {
|
||||||
|
const { statement: trimmedStatement, commentStarted } = trimComments(
|
||||||
|
statement,
|
||||||
|
isCommentStarted
|
||||||
|
)
|
||||||
|
isCommentStarted = commentStarted
|
||||||
|
|
||||||
|
if (trimmedStatement.startsWith('%macro')) {
|
||||||
|
const startLineNumber = index + 1
|
||||||
|
const name = trimmedStatement
|
||||||
|
.slice(7, trimmedStatement.length)
|
||||||
|
.trim()
|
||||||
|
.split('(')[0]
|
||||||
|
macroStack.push({
|
||||||
|
name,
|
||||||
|
startLineNumber,
|
||||||
|
endLineNumber: null,
|
||||||
|
parentMacro: macroStack.length
|
||||||
|
? macroStack[macroStack.length - 1].name
|
||||||
|
: '',
|
||||||
|
hasParentheses: trimmedStatement.endsWith('()'),
|
||||||
|
hasMacroNameInMend: false,
|
||||||
|
mismatchedMendMacroName: '',
|
||||||
|
declaration: line,
|
||||||
|
termination: ''
|
||||||
|
})
|
||||||
|
} else if (trimmedStatement.startsWith('%mend')) {
|
||||||
|
if (macroStack.length) {
|
||||||
|
const macro = macroStack.pop() as Macro
|
||||||
|
const mendMacroName =
|
||||||
|
trimmedStatement.split(' ').filter((s: string) => !!s)[1] || ''
|
||||||
|
macro.endLineNumber = index + 1
|
||||||
|
macro.hasMacroNameInMend = trimmedStatement.includes(macro.name)
|
||||||
|
macro.mismatchedMendMacroName = macro.hasMacroNameInMend
|
||||||
|
? ''
|
||||||
|
: mendMacroName
|
||||||
|
macro.termination = line
|
||||||
|
macros.push(macro)
|
||||||
|
} else {
|
||||||
|
macros.push({
|
||||||
|
name: '',
|
||||||
|
startLineNumber: null,
|
||||||
|
endLineNumber: index + 1,
|
||||||
|
parentMacro: '',
|
||||||
|
hasParentheses: false,
|
||||||
|
hasMacroNameInMend: false,
|
||||||
|
mismatchedMendMacroName: '',
|
||||||
|
declaration: '',
|
||||||
|
termination: line
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
macros.push(...macroStack)
|
||||||
|
|
||||||
|
return macros
|
||||||
|
}
|
||||||
41
src/utils/splitText.spec.ts
Normal file
41
src/utils/splitText.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { LintConfig } from '../types'
|
||||||
|
import { splitText } from './splitText'
|
||||||
|
|
||||||
|
describe('splitText', () => {
|
||||||
|
const config = new LintConfig({
|
||||||
|
noTrailingSpaces: true,
|
||||||
|
noEncodedPasswords: true,
|
||||||
|
hasDoxygenHeader: true,
|
||||||
|
noSpacesInFileNames: true,
|
||||||
|
maxLineLength: 80,
|
||||||
|
lowerCaseFileNames: true,
|
||||||
|
noTabIndentation: true,
|
||||||
|
indentationMultiple: 2,
|
||||||
|
hasMacroNameInMend: true,
|
||||||
|
noNestedMacros: true,
|
||||||
|
hasMacroParentheses: true,
|
||||||
|
lineEndings: 'lf'
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an empty array when text is falsy', () => {
|
||||||
|
const lines = splitText('', config)
|
||||||
|
|
||||||
|
expect(lines.length).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an array of lines from text', () => {
|
||||||
|
const lines = splitText(`line 1\nline 2`, config)
|
||||||
|
|
||||||
|
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`, config)
|
||||||
|
|
||||||
|
expect(lines.length).toEqual(2)
|
||||||
|
expect(lines[0]).toEqual('line 1')
|
||||||
|
expect(lines[1]).toEqual('line 2')
|
||||||
|
})
|
||||||
|
})
|
||||||
17
src/utils/splitText.ts
Normal file
17
src/utils/splitText.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { LintConfig } from '../types/LintConfig'
|
||||||
|
import { LineEndings } from '../types/LineEndings'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, config: LintConfig): string[] => {
|
||||||
|
if (!text) return []
|
||||||
|
const expectedLineEndings =
|
||||||
|
config.lineEndings === LineEndings.LF ? '\n' : '\r\n'
|
||||||
|
const incorrectLineEndings = expectedLineEndings === '\n' ? '\r\n' : '\n'
|
||||||
|
return text
|
||||||
|
.replace(new RegExp(incorrectLineEndings, 'g'), expectedLineEndings)
|
||||||
|
.split(expectedLineEndings)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user