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

feat(lint): add rules for lowercase file names, max line length and no tab indentation

This commit is contained in:
Krishna Acondy
2021-03-26 09:09:42 +00:00
parent 1be358ca51
commit 3631f5c25c
15 changed files with 265 additions and 12 deletions

View File

@@ -2,5 +2,8 @@
"noTrailingSpaces": true,
"noEncodedPasswords": true,
"hasDoxygenHeader": true,
"noSpacesInFileNames": true
"noSpacesInFileNames": true,
"maxLineLength": 80,
"lowerCaseFileNames": true,
"noTabIndentation": true
}

View File

@@ -8,14 +8,20 @@
"noTrailingSpaces": true,
"noEncodedPasswords": true,
"hasDoxygenHeader": true,
"noSpacesInFileNames": true
"noSpacesInFileNames": true,
"lowerCaseFileNames": true,
"maxLineLength": 80,
"noTabIndentation": true
},
"examples": [
{
"noTrailingSpaces": true,
"noEncodedPasswords": true,
"hasDoxygenHeader": true,
"noSpacesInFileNames": true
"noSpacesInFileNames": true,
"lowerCaseFileNames": true,
"maxLineLength": 80,
"noTabIndentation": true
}
],
"properties": {
@@ -50,6 +56,30 @@
"description": "Enforces no spaces in file names. Shows a warning when they are present.",
"default": true,
"examples": [true, false]
},
"lowerCaseFileNames": {
"$id": "#/properties/lowerCaseFileNames",
"type": "boolean",
"title": "lowerCaseFileNames",
"description": "Enforces no uppercase characters in file names. Shows a warning when they are present.",
"default": true,
"examples": [true, false]
},
"maxLineLength": {
"$id": "#/properties/maxLineLength",
"type": "number",
"title": "maxLineLength",
"description": "Enforces a configurable maximum line length. Shows a warning for lines exceeding this length.",
"default": 80,
"examples": [60, 80, 120]
},
"noTabIndentation": {
"$id": "#/properties/noTabIndentation",
"type": "boolean",
"title": "noTabIndentation",
"description": "Enforces no indentation using tabs. Shows a warning when a line starts with a tab.",
"default": true,
"examples": [true, false]
}
}
}

View File

@@ -1,4 +1,5 @@
import { lintText } from './lint'
import { lintFile, lintText } from './lint'
import path from 'path'
/**
* Example which tests a piece of text with all known violations.
@@ -30,8 +31,8 @@ const text = `/**
%macro mf_getuniquelibref(prefix=mclib,maxtries=1000);
%local x libref;
%let x={SAS002};
%local x libref;
%let x={SAS002};
%do x=0 %to &maxtries;
%if %sysfunc(libref(&prefix&x)) ne 0 %then %do;
%let libref=&prefix&x;
@@ -44,6 +45,15 @@ const text = `/**
%end;
%put unable to find available libref in range &prefix.0-&maxtries;
%mend;
`
lintText(text).then((diagnostics) => console.table(diagnostics))
lintText(text).then((diagnostics) => {
console.log('Text lint results:')
console.table(diagnostics)
})
lintFile(path.join(__dirname, 'Example File.sas')).then((diagnostics) => {
console.log('File lint results:')
console.table(diagnostics)
})

View File

@@ -65,7 +65,7 @@ const processLine = (
): Diagnostic[] => {
const diagnostics: Diagnostic[] = []
config.lineLintRules.forEach((rule) => {
diagnostics.push(...rule.test(line, lineNumber))
diagnostics.push(...rule.test(line, lineNumber, config))
})
return diagnostics

View File

@@ -0,0 +1,27 @@
import { Severity } from '../types/Severity'
import { lowerCaseFileNames } from './lowerCaseFileNames'
describe('lowerCaseFileNames', () => {
it('should return an empty array when the file name has no uppercase characters', () => {
const filePath = '/code/sas/my_sas_file.sas'
expect(lowerCaseFileNames.test(filePath)).toEqual([])
})
it('should return an empty array when the file name has no uppercase characters, even if the containing folder has uppercase characters', () => {
const filePath = '/code/SAS Projects/my_sas_file.sas'
expect(lowerCaseFileNames.test(filePath)).toEqual([])
})
it('should return an array with a single diagnostic when the file name has uppercase characters', () => {
const filePath = '/code/sas/my SAS file.sas'
expect(lowerCaseFileNames.test(filePath)).toEqual([
{
message: 'File name contains uppercase characters',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
}
])
})
})

View File

@@ -0,0 +1,32 @@
import { PathLintRule } from '../types/LintRule'
import { LintRuleType } from '../types/LintRuleType'
import { Severity } from '../types/Severity'
import path from 'path'
const name = 'lowerCaseFileNames'
const description = 'Enforce the use of lower case file names.'
const message = 'File name contains uppercase characters'
const test = (value: string) => {
const fileName = path.basename(value)
if (fileName.toLocaleLowerCase() === fileName) return []
return [
{
message,
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
}
]
}
/**
* Lint rule that checks for the absence of uppercase characters in a given file name.
*/
export const lowerCaseFileNames: PathLintRule = {
type: LintRuleType.Path,
name,
description,
message,
test
}

View File

@@ -0,0 +1,44 @@
import { LintConfig, Severity } from '../types'
import { maxLineLength } from './maxLineLength'
describe('maxLineLength', () => {
it('should return an empty array when the line is within the specified length', () => {
const line = "%put 'hello';"
const config = new LintConfig({ maxLineLength: 60 })
expect(maxLineLength.test(line, 1, config)).toEqual([])
})
it('should return an array with a single diagnostic when the line exceeds the specified length', () => {
const line = "%put 'hello';"
const config = new LintConfig({ maxLineLength: 10 })
expect(maxLineLength.test(line, 1, config)).toEqual([
{
message: `Line exceeds maximum length by 3 characters`,
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
}
])
})
it('should fall back to a default of 80 characters', () => {
const line =
'Prow scuttle parrel provost Sail ho shrouds spirits boom mizzenmast yardarm. Pinnace holystone.'
expect(maxLineLength.test(line, 1)).toEqual([
{
message: `Line exceeds maximum length by 15 characters`,
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
}
])
})
it('should return an empty array for lines within the default length', () => {
const line =
'Prow scuttle parrel provost Sail ho shrouds spirits boom mizzenmast yard'
expect(maxLineLength.test(line, 1)).toEqual([])
})
})

View File

@@ -0,0 +1,32 @@
import { LintConfig } from '../types'
import { LineLintRule } from '../types/LintRule'
import { LintRuleType } from '../types/LintRuleType'
import { Severity } from '../types/Severity'
const name = 'maxLineLength'
const description = 'Restrict lines to the specified length.'
const message = 'Line exceeds maximum length'
const test = (value: string, lineNumber: number, config?: LintConfig) => {
const maxLineLength = config?.maxLineLength || 80
if (value.length <= maxLineLength) return []
return [
{
message: `${message} by ${value.length - maxLineLength} characters`,
lineNumber,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
}
]
}
/**
* Lint rule that checks if a line has exceeded the configured maximum length.
*/
export const maxLineLength: LineLintRule = {
type: LintRuleType.Line,
name,
description,
message,
test
}

View File

@@ -1,7 +1,7 @@
import { PathLintRule } from '../types/LintRule'
import { LintRuleType } from '../types/LintRuleType'
import path from 'path'
import { Severity } from '../types/Severity'
import path from 'path'
const name = 'noSpacesInFileNames'
const description = 'Enforce the absence of spaces within file names.'

View File

@@ -0,0 +1,22 @@
import { Severity } from '../types/Severity'
import { noTabIndentation } from './noTabIndentation'
describe('noTabs', () => {
it('should return an empty array when the line is not indented with a tab', () => {
const line = "%put 'hello';"
expect(noTabIndentation.test(line, 1)).toEqual([])
})
it('should return an array with a single diagnostic when the line is indented with a tab', () => {
const line = "\t%put 'hello';"
expect(noTabIndentation.test(line, 1)).toEqual([
{
message: 'Line is indented with a tab',
lineNumber: 1,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
}
])
})
})

View File

@@ -0,0 +1,30 @@
import { LineLintRule } from '../types/LintRule'
import { LintRuleType } from '../types/LintRuleType'
import { Severity } from '../types/Severity'
const name = 'noTabs'
const description = 'Disallow indenting with tabs.'
const message = 'Line is indented with a tab'
const test = (value: string, lineNumber: number) => {
if (!value.startsWith('\t')) return []
return [
{
message,
lineNumber,
startColumnNumber: 1,
endColumnNumber: 1,
severity: Severity.Warning
}
]
}
/**
* Lint rule that checks if a given line of text is indented with a tab.
*/
export const noTabIndentation: LineLintRule = {
type: LintRuleType.Line,
name,
description,
message,
test
}

View File

@@ -1,6 +1,9 @@
import { hasDoxygenHeader } from '../rules/hasDoxygenHeader'
import { lowerCaseFileNames } from '../rules/lowerCaseFileNames'
import { maxLineLength } from '../rules/maxLineLength'
import { noEncodedPasswords } from '../rules/noEncodedPasswords'
import { noSpacesInFileNames } from '../rules/noSpacesInFileNames'
import { noTabIndentation } from '../rules/noTabIndentation'
import { noTrailingSpaces } from '../rules/noTrailingSpaces'
import { FileLintRule, LineLintRule, PathLintRule } from './LintRule'
@@ -15,6 +18,7 @@ export class LintConfig {
readonly lineLintRules: LineLintRule[] = []
readonly fileLintRules: FileLintRule[] = []
readonly pathLintRules: PathLintRule[] = []
readonly maxLineLength = 80
constructor(json?: any) {
if (json?.noTrailingSpaces) {
@@ -25,6 +29,15 @@ export class LintConfig {
this.lineLintRules.push(noEncodedPasswords)
}
if (json?.noTabIndentation) {
this.lineLintRules.push(noTabIndentation)
}
if (json?.maxLineLength) {
this.maxLineLength = json.maxLineLength
this.lineLintRules.push(maxLineLength)
}
if (json?.hasDoxygenHeader) {
this.fileLintRules.push(hasDoxygenHeader)
}
@@ -32,5 +45,9 @@ export class LintConfig {
if (json?.noSpacesInFileNames) {
this.pathLintRules.push(noSpacesInFileNames)
}
if (json?.lowerCaseFileNames) {
this.pathLintRules.push(lowerCaseFileNames)
}
}
}

View File

@@ -1,4 +1,5 @@
import { Diagnostic } from './Diagnostic'
import { LintConfig } from './LintConfig'
import { LintRuleType } from './LintRuleType'
/**
@@ -17,7 +18,7 @@ export interface LintRule {
*/
export interface LineLintRule extends LintRule {
type: LintRuleType.Line
test: (value: string, lineNumber: number) => Diagnostic[]
test: (value: string, lineNumber: number, config?: LintConfig) => Diagnostic[]
}
/**

View File

@@ -18,6 +18,7 @@ describe('getLintConfig', () => {
expect(config).toBeInstanceOf(LintConfig)
expect(config.fileLintRules.length).toEqual(1)
expect(config.lineLintRules.length).toEqual(2)
expect(config.lineLintRules.length).toEqual(4)
expect(config.pathLintRules.length).toEqual(2)
})
})

View File

@@ -6,7 +6,11 @@ import { getProjectRoot } from './getProjectRoot'
const defaultConfiguration = {
noTrailingSpaces: true,
noEncodedPasswords: true,
hasDoxygenHeader: true
hasDoxygenHeader: true,
noSpacesInFileNames: true,
lowerCaseFileNames: true,
maxLineLength: 80,
noTabIndentation: true
}
/**
* Fetches the config from the .sasjslint file and creates a LintConfig object.