1
0
mirror of https://github.com/sasjs/lint.git synced 2026-01-17 09:10:06 +00:00

feat: add new config maxDataLineLength

This commit is contained in:
2023-01-11 19:51:07 +05:00
parent 985ed41a4b
commit 7a46e9857e
10 changed files with 251 additions and 13 deletions

View File

@@ -14,6 +14,7 @@
"lowerCaseFileNames": true, "lowerCaseFileNames": true,
"maxLineLength": 80, "maxLineLength": 80,
"maxHeaderLineLength": 80, "maxHeaderLineLength": 80,
"maxDataLineLength": 80,
"noGremlins": true, "noGremlins": true,
"noNestedMacros": true, "noNestedMacros": true,
"noSpacesInFileNames": true, "noSpacesInFileNames": true,
@@ -32,6 +33,7 @@
"lowerCaseFileNames": true, "lowerCaseFileNames": true,
"maxLineLength": 80, "maxLineLength": 80,
"maxHeaderLineLength": 80, "maxHeaderLineLength": 80,
"maxDataLineLength": 80,
"noGremlins": true, "noGremlins": true,
"allowedGremlins": ["0x0080", "0x3000"], "allowedGremlins": ["0x0080", "0x3000"],
"noTabs": true, "noTabs": true,
@@ -137,6 +139,14 @@
"default": 80, "default": 80,
"examples": [60, 80, 120] "examples": [60, 80, 120]
}, },
"maxDataLineLength": {
"$id": "#/properties/maxDataLineLength",
"type": "number",
"title": "maxDataLineLength",
"description": "Enforces a configurable maximum line length for data section. Shows a warning for lines exceeding this length.",
"default": 80,
"examples": [60, 80, 120]
},
"noNestedMacros": { "noNestedMacros": {
"$id": "#/properties/noNestedMacros", "$id": "#/properties/noNestedMacros",
"type": "boolean", "type": "boolean",

View File

@@ -1,15 +1,18 @@
import { LintConfig, Diagnostic } from '../types' import { LintConfig, Diagnostic, LineLintRuleOptions } from '../types'
import { getHeaderLinesCount, splitText } from '../utils' import { getHeaderLinesCount, splitText } from '../utils'
import { checkIsDataLine, getDataSectionsDetail } from '../utils'
export const processText = (text: string, config: LintConfig) => { export const processText = (text: string, config: LintConfig) => {
const lines = splitText(text, config) const lines = splitText(text, config)
const headerLinesCount = getHeaderLinesCount(text, config) const headerLinesCount = getHeaderLinesCount(text, config)
const dataSections = getDataSectionsDetail(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) => {
index += 1 const isHeaderLine = index + 1 <= headerLinesCount
const isDataLine = checkIsDataLine(dataSections, index)
diagnostics.push( diagnostics.push(
...processLine(config, line, index, index <= headerLinesCount) ...processLine(config, line, index + 1, { isHeaderLine, isDataLine })
) )
}) })
@@ -41,11 +44,11 @@ export const processLine = (
config: LintConfig, config: LintConfig,
line: string, line: string,
lineNumber: number, lineNumber: number,
isHeaderLine: boolean options: LineLintRuleOptions
): Diagnostic[] => { ): Diagnostic[] => {
const diagnostics: Diagnostic[] = [] const diagnostics: Diagnostic[] = []
config.lineLintRules.forEach((rule) => { config.lineLintRules.forEach((rule) => {
diagnostics.push(...rule.test(line, lineNumber, config, isHeaderLine)) diagnostics.push(...rule.test(line, lineNumber, config, options))
}) })
return diagnostics return diagnostics

View File

@@ -41,4 +41,44 @@ describe('maxLineLength', () => {
'Prow scuttle parrel provost Sail ho shrouds spirits boom mizzenmast yard' 'Prow scuttle parrel provost Sail ho shrouds spirits boom mizzenmast yard'
expect(maxLineLength.test(line, 1)).toEqual([]) expect(maxLineLength.test(line, 1)).toEqual([])
}) })
it('should return an array with a single diagnostic when the line in header section exceeds the specified length', () => {
const line = 'This line is from header section'
const config = new LintConfig({
maxLineLength: 10,
maxHeaderLineLength: 15
})
expect(maxLineLength.test(line, 1, config, { isHeaderLine: true })).toEqual(
[
{
message: `Line exceeds maximum length by ${
line.length - config.maxHeaderLineLength
} characters`,
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
}
]
)
})
it('should return an array with a single diagnostic when the line in data section exceeds the specified length', () => {
const line = 'GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8.'
const config = new LintConfig({
maxLineLength: 10,
maxDataLineLength: 15
})
expect(maxLineLength.test(line, 1, config, { isDataLine: true })).toEqual([
{
message: `Line exceeds maximum length by ${
line.length - config.maxDataLineLength
} characters`,
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
}
])
})
}) })

View File

@@ -1,5 +1,5 @@
import { LintConfig } from '../../types' import { LintConfig } from '../../types'
import { LineLintRule } from '../../types/LintRule' import { LineLintRule, LineLintRuleOptions } from '../../types/LintRule'
import { LintRuleType } from '../../types/LintRuleType' import { LintRuleType } from '../../types/LintRuleType'
import { Severity } from '../../types/Severity' import { Severity } from '../../types/Severity'
import { DefaultLintConfiguration } from '../../utils' import { DefaultLintConfiguration } from '../../utils'
@@ -12,15 +12,19 @@ const test = (
value: string, value: string,
lineNumber: number, lineNumber: number,
config?: LintConfig, config?: LintConfig,
isHeaderLine?: boolean options?: LineLintRuleOptions
) => { ) => {
const severity = config?.severityLevel[name] || Severity.Warning const severity = config?.severityLevel[name] || Severity.Warning
let maxLineLength = config let maxLineLength = DefaultLintConfiguration.maxLineLength
? config.maxLineLength
: DefaultLintConfiguration.maxLineLength
if (isHeaderLine && config) { if (config) {
maxLineLength = Math.max(config.maxLineLength, config.maxHeaderLineLength) if (options?.isHeaderLine) {
maxLineLength = Math.max(config.maxLineLength, config.maxHeaderLineLength)
} else if (options?.isDataLine) {
maxLineLength = Math.max(config.maxLineLength, config.maxDataLineLength)
} else {
maxLineLength = config.maxLineLength
}
} }
if (value.length <= maxLineLength) return [] if (value.length <= maxLineLength) return []

View File

@@ -35,6 +35,7 @@ export class LintConfig {
readonly pathLintRules: PathLintRule[] = [] readonly pathLintRules: PathLintRule[] = []
readonly maxLineLength: number = 80 readonly maxLineLength: number = 80
readonly maxHeaderLineLength: number = 80 readonly maxHeaderLineLength: number = 80
readonly maxDataLineLength: number = 80
readonly indentationMultiple: number = 2 readonly indentationMultiple: number = 2
readonly lineEndings: LineEndings = LineEndings.LF readonly lineEndings: LineEndings = LineEndings.LF
readonly defaultHeader: string = getDefaultHeader() readonly defaultHeader: string = getDefaultHeader()
@@ -75,6 +76,10 @@ export class LintConfig {
if (!isNaN(json?.maxHeaderLineLength)) { if (!isNaN(json?.maxHeaderLineLength)) {
this.maxHeaderLineLength = json.maxHeaderLineLength this.maxHeaderLineLength = json.maxHeaderLineLength
} }
if (!isNaN(json?.maxDataLineLength)) {
this.maxDataLineLength = json.maxDataLineLength
}
} }
this.fileLintRules.push(lineEndings) this.fileLintRules.push(lineEndings)

View File

@@ -13,6 +13,11 @@ export interface LintRule {
message: string message: string
} }
export interface LineLintRuleOptions {
isHeaderLine?: boolean
isDataLine?: boolean
}
/** /**
* A LineLintRule is run once per line of text. * A LineLintRule is run once per line of text.
*/ */
@@ -22,7 +27,7 @@ export interface LineLintRule extends LintRule {
value: string, value: string,
lineNumber: number, lineNumber: number,
config?: LintConfig, config?: LintConfig,
isHeaderLine?: boolean options?: LineLintRuleOptions
) => Diagnostic[] ) => Diagnostic[]
fix?: (value: string, config?: LintConfig) => string fix?: (value: string, config?: LintConfig) => string
} }

View File

@@ -0,0 +1,113 @@
import { LintConfig } from '../types'
import { getDataSectionsDetail, checkIsDataLine } from './getDataSectionsDetail'
import { DefaultLintConfiguration } from './getLintConfig'
const datalines = `GROUP_LOGIC:$3. SUBGROUP_LOGIC:$3. SUBGROUP_ID:8. VARIABLE_NM:$32. OPERATOR_NM:$10. RAW_VALUE:$4000.
AND,AND,1,LIBREF,CONTAINS,"'DC'"
AND,OR,2,DSN,=,"'MPE_LOCK_ANYTABLE'"`
const datalinesBeginPattern1 = `datalines;`
const datalinesBeginPattern2 = `datalines4;`
const datalinesBeginPattern3 = `cards;`
const datalinesBeginPattern4 = `cards4;`
const datalinesBeginPattern5 = `parmcards;`
const datalinesBeginPattern6 = `parmcards4;`
const datalinesEndPattern1 = `;`
const datalinesEndPattern2 = `;;;;`
describe('getDataSectionsDetail', () => {
const config = new LintConfig(DefaultLintConfiguration)
it(`should return the detail of data section when it begins and ends with '${datalinesBeginPattern1}' and '${datalinesEndPattern1}' markers`, () => {
const text = `%put hello\n${datalinesBeginPattern1}\n${datalines}\n${datalinesEndPattern1}\n%put world;`
expect(getDataSectionsDetail(text, config)).toEqual([
{
start: 1,
end: 5
}
])
})
it(`should return the detail of data section when it begins and ends with '${datalinesBeginPattern2}' and '${datalinesEndPattern2}' markers`, () => {
const text = `%put hello\n${datalinesBeginPattern2}\n${datalines}\n${datalinesEndPattern2}\n%put world;`
expect(getDataSectionsDetail(text, config)).toEqual([
{
start: 1,
end: 5
}
])
})
it(`should return the detail of data section when it begins and ends with '${datalinesBeginPattern3}' and '${datalinesEndPattern1}' markers`, () => {
const text = `%put hello\n${datalinesBeginPattern3}\n${datalines}\n${datalinesEndPattern1}\n%put world;`
expect(getDataSectionsDetail(text, config)).toEqual([
{
start: 1,
end: 5
}
])
})
it(`should return the detail of data section when it begins and ends with '${datalinesBeginPattern4}' and '${datalinesEndPattern1}' markers`, () => {
const text = `%put hello\n${datalinesBeginPattern4}\n${datalines}\n${datalinesEndPattern1}\n%put world;`
expect(getDataSectionsDetail(text, config)).toEqual([
{
start: 1,
end: 5
}
])
})
it(`should return the detail of data section when it begins and ends with '${datalinesBeginPattern5}' and '${datalinesEndPattern1}' markers`, () => {
const text = `%put hello\n${datalinesBeginPattern5}\n${datalines}\n${datalinesEndPattern1}\n%put world;`
expect(getDataSectionsDetail(text, config)).toEqual([
{
start: 1,
end: 5
}
])
})
it(`should return the detail of data section when it begins and ends with '${datalinesBeginPattern6}' and '${datalinesEndPattern2}' markers`, () => {
const text = `%put hello\n${datalinesBeginPattern6}\n${datalines}\n${datalinesEndPattern2}\n%put world;`
expect(getDataSectionsDetail(text, config)).toEqual([
{
start: 1,
end: 5
}
])
})
})
describe('checkIsDataLine', () => {
const config = new LintConfig(DefaultLintConfiguration)
it(`should return true if a line index is in a range of any data section`, () => {
const text = `%put hello\n${datalinesBeginPattern1}\n${datalines}\n${datalinesEndPattern1}\n%put world;`
expect(
checkIsDataLine(
[
{
start: 1,
end: 5
}
],
4
)
).toBe(true)
})
it(`should return false if a line index is not in a range of any of data sections`, () => {
const text = `%put hello\n${datalinesBeginPattern1}\n${datalines}\n${datalinesEndPattern1}\n%put world;`
expect(
checkIsDataLine(
[
{
start: 1,
end: 5
}
],
8
)
).toBe(false)
})
})

View File

@@ -0,0 +1,56 @@
import { LintConfig } from '../types'
import { splitText } from './splitText'
interface DataSectionsDetail {
start: number
end: number
}
export const getDataSectionsDetail = (text: string, config: LintConfig) => {
const dataSections: DataSectionsDetail[] = []
const lines = splitText(text, config)
const dataSectionStartRegex1 = new RegExp(
'^(datalines;)|(cards;)|(cards4;)|(parmcards;)'
)
const dataSectionEndRegex1 = new RegExp(';')
const dataSectionStartRegex2 = new RegExp('^(datalines4)|(parmcards4);')
const dataSectionEndRegex2 = new RegExp(';;;;')
let dataSectionStarted = false
let dataSectionStartIndex = -1
let dataSectionEndRegex = dataSectionEndRegex1
lines.forEach((line, index) => {
if (dataSectionStarted) {
if (dataSectionEndRegex.test(line)) {
dataSections.push({ start: dataSectionStartIndex, end: index })
dataSectionStarted = false
}
} else {
if (dataSectionStartRegex1.test(line)) {
dataSectionStarted = true
dataSectionStartIndex = index
dataSectionEndRegex = dataSectionEndRegex1
} else if (dataSectionStartRegex2.test(line)) {
dataSectionStarted = true
dataSectionStartIndex = index
dataSectionEndRegex = dataSectionEndRegex2
}
}
})
return dataSections
}
export const checkIsDataLine = (
dataSections: DataSectionsDetail[],
lineIndex: number
) => {
for (const dataSection of dataSections) {
if (lineIndex >= dataSection.start && lineIndex <= dataSection.end)
return true
}
return false
}

View File

@@ -17,6 +17,7 @@ export const DefaultLintConfiguration = {
lowerCaseFileNames: true, lowerCaseFileNames: true,
maxLineLength: 80, maxLineLength: 80,
maxHeaderLineLength: 80, maxHeaderLineLength: 80,
maxDataLineLength: 80,
noTabIndentation: true, noTabIndentation: true,
indentationMultiple: 2, indentationMultiple: 2,
hasMacroNameInMend: true, hasMacroNameInMend: true,

View File

@@ -7,3 +7,4 @@ export * from './listSasFiles'
export * from './splitText' export * from './splitText'
export * from './getIndicesOf' export * from './getIndicesOf'
export * from './getHeaderLinesCount' export * from './getHeaderLinesCount'
export * from './getDataSectionsDetail'