Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: honour .gitignore when linting filesystems #167

Merged
merged 5 commits into from
Aug 15, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6,714 changes: 6,657 additions & 57 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"prebuild": "node checkNodeVersion",
"prepublishOnly": "cp -r ./build/* . && rm -rf ./build && rm -rf ./src && rm tsconfig.json",
"postpublish": "git clean -fd",
"package:lib": "npm run build && cp ./package.json build && cp README.md build && cd build && npm version \"5.0.0\" && npm pack",
"package:lib": "npm run build && cp ./package.json ./checkNodeVersion.js build && cp README.md build && cd build && npm version \"5.0.0\" && npm pack",
"lint:fix": "npx prettier --write \"{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
"lint": "npx prettier --check \"{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
"prepare": "git rev-parse --git-dir && git config core.hooksPath ./.git-hooks || true"
Expand Down Expand Up @@ -48,6 +48,7 @@
"typescript": "^4.3.2"
},
"dependencies": {
"@sasjs/utils": "^2.19.0"
"@sasjs/utils": "^2.19.0",
"ignore": "^5.2.0"
}
}
8 changes: 8 additions & 0 deletions sasjslint-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,14 @@
"description": "Enforces Macro Definition syntax. Shows a warning when incorrect syntax is used.",
"default": true,
"examples": [true, false]
},
"ignoreList": {
"$id": "#/properties/ignoreList",
"type": "object",
"title": "ignoreList",
"description": "An array of paths or path patterns to ignore matching resources from linting. Files or folders matching patterns in .gitignore will always be ignored.",
"default": ["sasjsbuild/", "sasjsresults/"],
"examples": ["sasjs/services", "appinit.sas"]
}
}
}
10 changes: 6 additions & 4 deletions src/lint/lintFile.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { readFile } from '@sasjs/utils/file'
import { LintConfig } from '../types/LintConfig'
import { getLintConfig } from '../utils/getLintConfig'
import { Diagnostic, LintConfig } from '../types'
import { getLintConfig, isIgnored } from '../utils'
import { processFile, processText } from './shared'

/**
* Analyses and produces a set of diagnostics for the file at the given path.
* @param {string} filePath - the path to the file to be linted.
* @param {LintConfig} configuration - an optional configuration. When not passed in, this is read from the .sasjslint file.
* @returns {Diagnostic[]} array of diagnostic objects, each containing a warning, line number and column number.
* @returns {Promise<Diagnostic[]>} array of diagnostic objects, each containing a warning, line number and column number.
*/
export const lintFile = async (
filePath: string,
configuration?: LintConfig
) => {
): Promise<Diagnostic[]> => {
if (await isIgnored(filePath)) return []

const config = configuration || (await getLintConfig())
const text = await readFile(filePath)

Expand Down
16 changes: 7 additions & 9 deletions src/lint/lintFolder.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { listSubFoldersInFolder } from '@sasjs/utils/file'
import path from 'path'
import { Diagnostic } from '../types/Diagnostic'
import { LintConfig } from '../types/LintConfig'
import { asyncForEach } from '../utils/asyncForEach'
import { getLintConfig } from '../utils/getLintConfig'
import { listSasFiles } from '../utils/listSasFiles'
import { Diagnostic, LintConfig } from '../types'
import { asyncForEach, getLintConfig, isIgnored, listSasFiles } from '../utils'
import { lintFile } from './lintFile'

const excludeFolders = [
Expand All @@ -28,6 +25,9 @@ export const lintFolder = async (
) => {
const config = configuration || (await getLintConfig())
let diagnostics: Map<string, Diagnostic[]> = new Map<string, Diagnostic[]>()

if (await isIgnored(folderPath)) return diagnostics

const fileNames = await listSasFiles(folderPath)
await asyncForEach(fileNames, async (fileName) => {
const filePath = path.join(folderPath, fileName)
Expand All @@ -39,10 +39,8 @@ export const lintFolder = async (
)

await asyncForEach(subFolders, async (subFolder) => {
const subFolderDiagnostics = await lintFolder(
path.join(folderPath, subFolder),
config
)
const subFolderPath = path.join(folderPath, subFolder)
const subFolderDiagnostics = await lintFolder(subFolderPath, config)
diagnostics = new Map([...diagnostics, ...subFolderDiagnostics])
})

Expand Down
15 changes: 15 additions & 0 deletions src/types/LintConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { FileLintRule, LineLintRule, PathLintRule } from './LintRule'
* More types of rules, when available, will be added here.
*/
export class LintConfig {
readonly ignoreList: string[] = []
readonly lineLintRules: LineLintRule[] = []
readonly fileLintRules: FileLintRule[] = []
readonly pathLintRules: PathLintRule[] = []
Expand All @@ -33,6 +34,20 @@ export class LintConfig {
readonly lineEndings: LineEndings = LineEndings.LF

constructor(json?: any) {
if (json?.ignoreList) {
if (Array.isArray(json.ignoreList)) {
json.ignoreList.forEach((item: any) => {
if (typeof item === 'string') this.ignoreList.push(item)
else
throw new Error(
`Property "ignoreList" has invalid type of values. It can contain only strings.`
)
})
} else {
throw new Error(`Property "ignoreList" can only be an array of strings`)
}
}

if (json?.noTrailingSpaces) {
this.lineLintRules.push(noTrailingSpaces)
}
Expand Down
2 changes: 2 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export * from './asyncForEach'
export * from './getLintConfig'
export * from './getProjectRoot'
export * from './isIgnored'
export * from './listSasFiles'
export * from './splitText'
83 changes: 83 additions & 0 deletions src/utils/isIgnored.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import * as fileModule from '@sasjs/utils/file'
import * as getProjectRootModule from './getProjectRoot'
import * as getLintConfigModule from './getLintConfig'
import { DefaultLintConfiguration } from './getLintConfig'
import { LintConfig } from '../types'
import { isIgnored } from './isIgnored'

describe('isIgnored', () => {
it('should return true if provided path matches the patterns from .gitignore', async () => {
jest
.spyOn(getLintConfigModule, 'getLintConfig')
.mockImplementationOnce(
async () => new LintConfig(DefaultLintConfiguration)
)
jest
.spyOn(fileModule, 'fileExists')
.mockImplementationOnce(async () => true)

jest
.spyOn(fileModule, 'readFile')
.mockImplementationOnce(async () => 'sasjs')

jest
.spyOn(getProjectRootModule, 'getProjectRoot')
.mockImplementationOnce(async () => '')

const ignored = await isIgnored('sasjs')

expect(ignored).toBeTruthy()
})

it('should return true if provided path matches any pattern from ignoreList (.sasjslint)', async () => {
jest
.spyOn(fileModule, 'fileExists')
.mockImplementationOnce(async () => false)

jest
.spyOn(getProjectRootModule, 'getProjectRoot')
.mockImplementationOnce(async () => '')

const ignored = await isIgnored(
'sasjs',
new LintConfig({
...DefaultLintConfiguration,
ignoreList: ['sasjs']
})
)

expect(ignored).toBeTruthy()
})

it('should return false if provided path does not matches any pattern from .gitignore and ignoreList (.sasjslint)', async () => {
jest
.spyOn(fileModule, 'fileExists')
.mockImplementationOnce(async () => true)

jest.spyOn(fileModule, 'readFile').mockImplementationOnce(async () => '')

jest
.spyOn(getProjectRootModule, 'getProjectRoot')
.mockImplementationOnce(async () => '')

const ignored = await isIgnored(
'sasjs',
new LintConfig(DefaultLintConfiguration)
)

expect(ignored).toBeFalsy()
})

it('should return false if provided path is equal to projectRoot', async () => {
jest
.spyOn(getProjectRootModule, 'getProjectRoot')
.mockImplementationOnce(async () => '')

const ignored = await isIgnored(
'',
new LintConfig(DefaultLintConfiguration)
)

expect(ignored).toBeFalsy()
})
})
34 changes: 34 additions & 0 deletions src/utils/isIgnored.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { fileExists, readFile } from '@sasjs/utils'
import path from 'path'
import ignore from 'ignore'
import { getLintConfig, getProjectRoot } from '.'
import { LintConfig } from '../types'

/**
* A function to check if file/folder path matches any pattern from .gitignore or ignoreList (.sasjsLint)
*
* @param fPath absolute path of file or folder
* @returns {Promise<boolean>} true if path matches the patterns from .gitignore file otherwise false
*/
export const isIgnored = async (
fPath: string,
configuration?: LintConfig
): Promise<boolean> => {
const config = configuration || (await getLintConfig())
const projectRoot = await getProjectRoot()
const gitIgnoreFilePath = path.join(projectRoot, '.gitignore')
const rootPath = projectRoot + path.sep
const relativePath = fPath.replace(rootPath, '')

if (fPath === projectRoot) return false

let gitIgnoreFileContent = ''

if (await fileExists(gitIgnoreFilePath))
gitIgnoreFileContent = await readFile(gitIgnoreFilePath)

return ignore()
.add(gitIgnoreFileContent)
.add(config.ignoreList)
.ignores(relativePath)
}