1
0
Fork 0

lint: fix things

This commit is contained in:
ssube 2019-09-11 08:48:14 -05:00
parent 6aa1cb5365
commit 2bb48b0b87
Signed by: ssube
GPG Key ID: 3EED7B957D362AF1
13 changed files with 175 additions and 140 deletions

View File

@ -1,5 +1,6 @@
import { Options, showCompletionScript, usage } from 'yargs';
import { RuleSelector } from '../rule';
import { VERSION_INFO } from '../version';
export const CONFIG_ARGS_NAME = 'config-name';
@ -16,25 +17,40 @@ export interface Args {
mode: string;
}
export interface ParsedArgs extends RuleSelector {
[CONFIG_ARGS_NAME]: string;
[CONFIG_ARGS_PATH]: string;
coerce: boolean;
count: boolean;
defaults: boolean;
dest: string;
mode: string;
rules: Array<string>;
source: string;
}
export interface ParseResults {
args: ParsedArgs;
mode: string;
}
/**
* Wrap yargs to exit after completion.
*
* @TODO: fix it to use argv, not sure if yargs can do that
*/
export function parseArgs(argv: Array<string>) {
export function parseArgs(argv: Array<string>): ParseResults {
let mode = 'check';
const args = usage(`Usage: salty-dog <mode> [options]`)
const parser = usage(`Usage: salty-dog <mode> [options]`)
.command({
command: ['check', '*'],
describe: 'validate the source documents',
handler: (argv: any) => {
handler: (argi: any) => {
mode = 'check';
},
})
.command({
command: ['fix'],
describe: 'validate the source document and insert defaults',
builder: (yargs: any) => {
return yargs
.option('coerce', {
@ -46,21 +62,23 @@ export function parseArgs(argv: Array<string>) {
type: 'boolean',
});
},
handler: (argv: any) => {
command: ['fix'],
describe: 'validate the source document and insert defaults',
handler: (argi: any) => {
mode = 'fix';
},
})
.command({
command: ['list'],
describe: 'list active rules',
handler: (argv: any) => {
handler: (argi: any) => {
mode = 'list';
},
})
.command({
command: ['complete'],
describe: 'generate tab completion script for bash or zsh',
handler: (argv: any) => {
handler: (argi: any) => {
mode = 'complete';
},
})
@ -112,8 +130,11 @@ export function parseArgs(argv: Array<string>) {
})
.help()
.version(VERSION_INFO.app.version)
.alias('version', 'v')
.argv;
.alias('version', 'v');
// @TODO: this should not need a cast but argv's type only has the last option (include-tag)
// @tslint:disable-next-line:no-any
const args = parser.argv as any;
if (mode === 'complete') {
showCompletionScript();

View File

@ -1,14 +1,26 @@
import { Stream } from 'bunyan';
import { isNil, isString } from 'lodash';
import { LogLevel } from 'noicejs';
import { join } from 'path';
import { CONFIG_ENV, CONFIG_SCHEMA } from './schema';
import { includeSchema } from './type/Include';
import { NotFoundError } from '../error/NotFoundError';
import { YamlParser } from '../parser/YamlParser';
import { readFileSync } from '../source';
import { CONFIG_ENV, CONFIG_SCHEMA } from './schema';
import { includeSchema } from './type/Include';
includeSchema.schema = CONFIG_SCHEMA;
export interface ConfigData {
data: {
logger: {
level: LogLevel;
name: string;
streams: Array<Stream>;
};
};
}
/**
* With the given name, generate all potential config paths in their complete, absolute form.
*
@ -39,7 +51,7 @@ export function completePaths(name: string, extras: Array<string>): Array<string
return paths;
}
export async function loadConfig(name: string, ...extras: Array<string>): Promise<any> {
export async function loadConfig(name: string, ...extras: Array<string>): Promise<ConfigData> {
const paths = completePaths(name, extras);
for (const p of paths) {

View File

@ -20,4 +20,3 @@ export const streamType = new YamlType('!stream', {
return Reflect.get(process, name);
},
});

View File

@ -18,6 +18,7 @@ const MODES_LIST: Array<string> = [MODES.check, MODES.fix, MODES.list];
const STATUS_SUCCESS = 0;
const STATUS_ERROR = 1;
const STATUS_MAX = 255;
export async function main(argv: Array<string>): Promise<number> {
const { args, mode } = parseArgs(argv);
@ -43,7 +44,7 @@ export async function main(argv: Array<string>): Promise<number> {
});
const rules = await loadRules(args.rules, ctx);
const activeRules = await resolveRules(rules, args as any);
const activeRules = await resolveRules(rules, args);
if (mode === 'list') {
logger.info({ rules: activeRules }, 'listing active rules');
@ -61,7 +62,7 @@ export async function main(argv: Array<string>): Promise<number> {
if (ctx.errors.length > 0) {
logger.error({ count: ctx.errors.length, errors: ctx.errors }, 'some rules failed');
if (args.count) {
return Math.min(ctx.errors.length, 255);
return Math.min(ctx.errors.length, STATUS_MAX);
} else {
return STATUS_ERROR;
}

View File

@ -4,7 +4,7 @@ import { CONFIG_SCHEMA } from '../config/schema';
import { Parser } from '../parser';
export class YamlParser implements Parser {
dump(...data: Array<any>): string {
public dump(...data: Array<any>): string {
const docs: Array<any> = [];
for (const doc of data) {
const part = safeDump(doc, {
@ -15,7 +15,7 @@ export class YamlParser implements Parser {
return docs.join('\n---\n\n');
}
parse(body: string): Array<any> {
public parse(body: string): Array<any> {
const docs: Array<any> = [];
safeLoadAll(body, (doc: any) => docs.push(doc), {
schema: CONFIG_SCHEMA,

View File

@ -1,12 +1,12 @@
import { ValidateFunction } from 'ajv';
import { applyDiff, diff } from 'deep-diff';
import { JSONPath } from 'jsonpath-plus';
import { cloneDeep, Dictionary, intersection, isNil } from 'lodash';
import { cloneDeep, defaultTo, Dictionary, intersection, isNil } from 'lodash';
import { LogLevel } from 'noicejs';
import { YamlParser } from './parser/YamlParser';
import { readFileSync } from './source';
import { isNilOrEmpty } from './utils';
import { ensureArray, isNilOrEmpty } from './utils';
import { friendlyError } from './utils/ajv';
import { Visitor } from './visitor';
import { VisitorContext } from './visitor/VisitorContext';
@ -40,11 +40,77 @@ export interface RuleSource {
rules: Array<RuleData>;
}
export function ensureArray<T>(val: Array<T> | undefined): Array<T> {
if (isNil(val)) {
export interface RuleResult extends VisitorResult {
rule: Rule;
}
export class Rule implements RuleData, Visitor<RuleResult> {
public readonly check: ValidateFunction;
public readonly desc: string;
public readonly filter?: ValidateFunction;
public readonly level: LogLevel;
public readonly name: string;
public readonly select: string;
public readonly tags: Array<string>;
constructor(data: RuleData) {
this.desc = data.desc;
this.level = data.level;
this.name = data.name;
this.select = defaultTo(data.select, '$');
this.tags = Array.from(data.tags);
// copy schema objects
this.check = cloneDeep(data.check);
if (!isNil(data.filter)) {
this.filter = cloneDeep(data.filter);
}
}
public async pick(ctx: VisitorContext, root: any): Promise<Array<any>> {
const scopes = JSONPath({
json: root,
path: this.select,
});
if (isNil(scopes) || scopes.length === 0) {
ctx.logger.debug('no data selected');
return [];
}
return scopes;
}
public async visit(ctx: VisitorContext, node: any): Promise<RuleResult> {
ctx.logger.debug({ item: node, rule: this }, 'visiting node');
const check = ctx.compile(this.check);
const filter = this.compileFilter(ctx);
const errors: Array<VisitorError> = [];
const result: RuleResult = {
changes: [],
errors,
rule: this,
};
if (filter(node)) {
ctx.logger.debug({ item: node }, 'checking item');
if (!check(node) && !isNil(check.errors) && check.errors.length > 0) {
ctx.error(...Array.from(check.errors).map(friendlyError));
}
} else {
return Array.from(val);
ctx.logger.debug({ errors: filter.errors, item: node }, 'skipping item');
}
return result;
}
protected compileFilter(ctx: VisitorContext): ValidateFunction {
if (isNil(this.filter)) {
return () => true;
} else {
return ctx.compile(this.filter);
}
}
}
@ -75,7 +141,7 @@ export async function loadRules(paths: Array<string>, ctx: VisitorContext): Prom
ctx.addSchema(data.name, data.definitions);
}
rules.push(...data.rules.map((data: RuleData) => new Rule(data)));
rules.push(...data.rules.map((it: RuleData) => new Rule(it)));
}
}
@ -149,78 +215,3 @@ export async function visitRules(ctx: VisitorContext, rules: Array<Rule>, data:
return ctx;
}
export interface RuleResult extends VisitorResult {
rule: Rule;
}
export class Rule implements RuleData, Visitor<RuleResult> {
public readonly check: ValidateFunction;
public readonly desc: string;
public readonly filter?: ValidateFunction;
public readonly level: LogLevel;
public readonly name: string;
public readonly select: string;
public readonly tags: string[];
constructor(data: RuleData) {
this.desc = data.desc;
this.level = data.level;
this.name = data.name;
this.select = data.select || '$';
this.tags = Array.from(data.tags);
// copy schema objects
this.check = cloneDeep(data.check);
if (!isNil(data.filter)) {
this.filter = cloneDeep(data.filter);
}
}
public async pick(ctx: VisitorContext, root: any): Promise<Array<any>> {
const scopes = JSONPath({
json: root,
path: this.select,
});
if (isNil(scopes) || scopes.length === 0) {
ctx.logger.debug('no data selected');
return [];
}
return scopes;
}
public async visit(ctx: VisitorContext, node: any): Promise<RuleResult> {
ctx.logger.debug({ item: node, rule: this }, 'visiting node');
const check = ctx.compile(this.check);
const filter = this.compileFilter(ctx);
const errors: Array<VisitorError> = [];
const result: RuleResult = {
changes: [],
errors,
rule: this,
};
if (filter(node)) {
ctx.logger.debug({ item: node }, 'checking item');
if (!check(node) && check.errors && check.errors.length) {
const errors = Array.from(check.errors);
ctx.error(...errors.map(friendlyError));
}
} else {
ctx.logger.debug({ errors: filter.errors, item: node }, 'skipping item');
}
return result;
}
protected compileFilter(ctx: VisitorContext): ValidateFunction {
if (isNil(this.filter)) {
return () => true;
} else {
return ctx.compile(this.filter);
}
}
}

View File

@ -1,4 +1,5 @@
import { ErrorObject } from 'ajv';
import { isNil } from 'lodash';
import { VisitorError } from '../../visitor/VisitorError';
@ -13,9 +14,9 @@ export function friendlyError(err: ErrorObject): VisitorError {
}
export function friendlyErrorMessage(err: ErrorObject): string {
if (err.message) {
return `${err.dataPath} ${err.message}`;
} else {
if (isNil(err.message)) {
return `${err.dataPath} ${err.keyword}`;
} else {
return `${err.dataPath} ${err.message}`;
}
}

View File

@ -1,3 +1,13 @@
import { isNil } from 'lodash';
export function isNilOrEmpty(val: Array<unknown> | null | undefined): val is Array<unknown> {
return (Array.isArray(val) && val.length > 0);
}
export function ensureArray<T>(val: Array<T> | undefined): Array<T> {
if (isNil(val)) {
return [];
} else {
return Array.from(val);
}
}

View File

@ -20,20 +20,20 @@ export class VisitorContext implements VisitorContextOptions, VisitorResult {
public readonly innerOptions: RuleOptions;
protected readonly ajv: Ajv.Ajv;
protected readonly _changes: Array<any>;
protected readonly _errors: Array<VisitorError>;
protected readonly changeBuffer: Array<any>;
protected readonly errorBuffer: Array<VisitorError>;
public get changes(): ReadonlyArray<any> {
return this._changes;
return this.changeBuffer;
}
public get errors(): ReadonlyArray<VisitorError> {
return this._errors;
return this.errorBuffer;
}
constructor(options: VisitorContextOptions) {
this._changes = [];
this._errors = [];
this.changeBuffer = [];
this.errorBuffer = [];
this.ajv = new Ajv({
coerceTypes: options.innerOptions.coerce,
@ -49,12 +49,12 @@ export class VisitorContext implements VisitorContextOptions, VisitorResult {
logWithLevel(this.logger, err.level, err.data, err.msg);
}
this._errors.push(...errors);
this.errorBuffer.push(...errors);
}
public mergeResult(other: VisitorResult): this {
this._changes.push(...other.changes);
this._errors.push(...other.errors);
this.changeBuffer.push(...other.changes);
this.errorBuffer.push(...other.errors);
return this;
}
@ -69,7 +69,7 @@ export class VisitorContext implements VisitorContextOptions, VisitorResult {
});
this.ajv.addSchema({
'$id': name,
$id: name,
definitions: schema,
});
}

View File

@ -6,26 +6,26 @@ import { makeSelector, resolveRules, Rule, visitRules } from '../src/rule';
import { VisitorContext } from '../src/visitor/VisitorContext';
const TEST_RULES = [new Rule({
name: 'foo',
check: {},
desc: '',
level: 'info',
name: 'foo',
select: '$',
tags: ['all', 'foo'],
check: {},
select: '$',
}), new Rule({
check: {},
desc: '',
level: 'warn',
name: 'bar',
desc: '',
level: 'warn',
tags: ['all', 'test'],
check: {},
select: '$',
tags: ['all', 'test'],
}), new Rule({
name: 'bin',
check: {},
desc: '',
level: 'warn',
tags: ['all', 'test'],
check: {},
name: 'bin',
select: '$',
tags: ['all', 'test'],
})];
describe('rule resolver', () => {
@ -102,21 +102,21 @@ describe('rule resolver', () => {
describe('rule visitor', () => {
it('should only call visit for selected items', async () => {
const ctx = new VisitorContext({
logger: new ConsoleLogger(),
innerOptions: {
coerce: false,
defaults: false,
mutate: false,
}
},
logger: new ConsoleLogger(),
});
const data = {};
const rule = new Rule({
name: 'foo',
check: {},
desc: '',
level: 'info',
tags: [],
name: 'foo',
select: '$',
check: {},
tags: [],
});
const mockRule = mock(rule);
@ -143,12 +143,12 @@ describe('rule visitor', () => {
});
const data = {};
const rule = new Rule({
name: 'foo',
check: {},
desc: '',
level: 'info',
tags: [],
name: 'foo',
select: '$',
check: {},
tags: [],
});
const mockRule = mock(rule);

View File

@ -1,3 +1,3 @@
import sourceMapSupport from 'source-map-support'
import sourceMapSupport from 'source-map-support';
sourceMapSupport.install()
sourceMapSupport.install();

View File

@ -5,10 +5,10 @@ import { friendlyError } from '../../../src/utils/ajv';
describe('friendly errors', () => {
it('should have a message', () => {
const err = friendlyError({
keyword: 'test',
dataPath: 'test-path',
schemaPath: 'test-path',
keyword: 'test',
params: { /* ? */ },
schemaPath: 'test-path',
});
expect(err.msg).to.not.equal('');
});