@ammar-ahmed/automation-app-bot
Version:
Automation made easy and fun OH Yeah!
262 lines (225 loc) • 8.1 kB
JavaScript
const minimatch = require('minimatch')
const { pick, mapKeys, isUndefined } = require('lodash')
const fetch = require('node-fetch')
const { Validator } = require('./validator')
const constructOutput = require('./options_processor/options/lib/constructOutput')
const consolidateResult = require('./options_processor/options/lib/consolidateResults')
const constructErrorOutput = require('./options_processor/options/lib/constructErrorOutput')
const SINGLE_LINE_COMMENT_REGEXES = {
'.js': /^\/\//i,
'.py': /^#/i
}
const BLOCK_COMMENT_REGEXES = {
'.js': {
beginning: /\/\*/i,
end: /\*\//i
},
'.py': null
}
class Size extends Validator {
constructor () {
super('size')
this.supportedFileExtensionForCommentIgnore = [
'.js',
'.py'
]
this.supportedEvents = [
'pull_request.*',
'pull_request_review.*'
]
this.supportedSettings = {
match: 'array',
ignore: 'array',
lines: {
max: {
count: 'number',
message: 'string'
},
total: {
count: 'number',
message: 'string'
},
additions: {
count: 'number',
message: 'string'
},
deletions: {
count: 'number',
message: 'string'
},
ignore_comments: 'boolean'
}
}
}
async validate (context, validationSettings) {
const ERROR_MESSAGE = 'Failed to validate because the \'lines\' or \'max / total\', \'additions\' or \'deletions\' option is missing. Please check the documentation.'
const ERROR_MAX_TOTAL = 'Options max and total cannot be used together. Please choose one'
const VALIDATOR_NAME = 'Size'
const validatorContext = { name: VALIDATOR_NAME }
const patternsToInclude = validationSettings.match || ['**']
const patternsToIgnore = validationSettings.ignore || []
const listFilesResult = await this.githubAPI.listFiles(context, context.repo({ pull_number: this.getPayload(context).number }))
let modifiedFiles
if (validationSettings.lines && validationSettings.lines.ignore_comments) {
modifiedFiles = await this.calculateChangesWithoutComments(this.getPayload(context).diff_url)
} else {
// Possible file statuses: added, modified, removed.
modifiedFiles = listFilesResult
.filter(file => matchesPatterns(file.filename, patternsToInclude))
.filter(file => !matchesPatterns(file.filename, patternsToIgnore))
.filter(file => file.status === 'modified' || file.status === 'added')
.map(
file => ({
filename: file.filename,
additions: file.additions,
deletions: file.deletions,
changes: file.changes
})
)
}
const prSizeChanges = {
additions: 0,
deletions: 0,
total: 0
}
modifiedFiles.forEach((file) => {
prSizeChanges.additions += file.additions
prSizeChanges.deletions += file.deletions
prSizeChanges.total += file.changes
})
const inputMessage = `${prSizeChanges.total} total, ${prSizeChanges.additions} additions, ${prSizeChanges.deletions} deletions
Files: ${modifiedFiles.map(file => file.filename).join(',')}`
if (!validationSettings.lines) {
return consolidateResult(
[
constructErrorOutput(
VALIDATOR_NAME,
inputMessage,
validationSettings,
ERROR_MESSAGE
)
],
validatorContext
)
}
// lets get available keys. must be either total, max, additions or deletions
const prSize = pick(validationSettings.lines, ['additions', 'deletions', 'total', 'max'])
if (!isUndefined(prSize.max) && !isUndefined(prSize.total)) {
return consolidateResult(
[
constructErrorOutput(
VALIDATOR_NAME,
inputMessage,
validationSettings,
ERROR_MAX_TOTAL
)
],
validatorContext
)
}
if (prSize.max) {
prSize.total = prSize.max
delete prSize.max
}
const output = []
mapKeys(prSize, (value, key) => {
const isMergeable = prSizeChanges[key] <= value.count
const message = key === 'total' ? 'total additions + deletions' : key
const defaultFailMessage = `PR is too large. Should be under ${value.count} ${message}`
const description = isMergeable
? `PR size for ${message} is OK!`
: value.message || defaultFailMessage
const result = {
status: isMergeable ? 'pass' : 'fail',
description
}
output.push(constructOutput(
validatorContext,
inputMessage,
validationSettings,
result
))
})
if (output.length === 0) {
return consolidateResult(
[
constructErrorOutput(
VALIDATOR_NAME,
inputMessage,
validationSettings,
ERROR_MESSAGE
)
],
validatorContext
)
}
return consolidateResult(output, validatorContext)
}
async calculateChangesWithoutComments (url) {
const response = await fetch(url)
const diffs = await response.text()
const files = diffs.split('diff --git')
files.shift()
const processedFiles = []
for (const file of files) {
const filename = extractFileName(file)
const fileExtension = extractFileExtension(filename)
const lines = file.split('\n')
if (file.includes('deleted')) continue
// eslint-disable-next-line prefer-regex-literals
const addOrDeleteRegex = new RegExp(/^(\+|-)/i)
// eslint-disable-next-line prefer-regex-literals
const fileModifierRegex = new RegExp(/(\+\+\+|---)/i)
const singleLineCommentRegex = new RegExp(SINGLE_LINE_COMMENT_REGEXES[fileExtension])
let additions = 0
let deletions = 0
let isBlockCommentActive = false
for (const line of lines) {
if (addOrDeleteRegex.test(line) && !fileModifierRegex.test(line)) {
if (this.supportedFileExtensionForCommentIgnore.includes(fileExtension)) {
const lineContent = line.substring(1).trim()
if (singleLineCommentRegex.test(lineContent)) continue
if (BLOCK_COMMENT_REGEXES[fileExtension] !== null) {
const blockCommentStartRegex = new RegExp(BLOCK_COMMENT_REGEXES[fileExtension].beginning)
const blockCommentEndRegex = new RegExp(BLOCK_COMMENT_REGEXES[fileExtension].end)
if (isBlockCommentActive) {
if (blockCommentEndRegex.test(lineContent)) isBlockCommentActive = false
continue
} else if (blockCommentStartRegex.test(lineContent)) {
if (!blockCommentEndRegex.test(lineContent)) {
isBlockCommentActive = true
}
continue
}
}
}
const plusOrMinus = line[0]
if (plusOrMinus === '+') additions++
else deletions++
}
}
processedFiles.push({
filename,
additions,
deletions,
changes: additions + deletions
})
}
return processedFiles
}
}
const extractFileExtension = (filename) => {
// eslint-disable-next-line prefer-regex-literals
const fileExtensionRegex = new RegExp(/\.[0-9a-z]+$/i)
const matches = filename.match(fileExtensionRegex)
return matches ? matches[0] : matches
}
const extractFileName = (file) => {
const startIndex = file.indexOf('+++ b/')
const endIndex = file.indexOf('\n', startIndex)
return file.substring(startIndex + 6, endIndex)
}
const matchesPatterns = (filename, patterns) => (
patterns.some((pattern) => minimatch(filename, pattern, { dot: true }))
)
module.exports = Size