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 { Options, showCompletionScript, usage } from 'yargs';
import { RuleSelector } from '../rule';
import { VERSION_INFO } from '../version'; import { VERSION_INFO } from '../version';
export const CONFIG_ARGS_NAME = 'config-name'; export const CONFIG_ARGS_NAME = 'config-name';
@ -16,25 +17,40 @@ export interface Args {
mode: string; 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. * Wrap yargs to exit after completion.
* *
* @TODO: fix it to use argv, not sure if yargs can do that * @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'; let mode = 'check';
const args = usage(`Usage: salty-dog <mode> [options]`) const parser = usage(`Usage: salty-dog <mode> [options]`)
.command({ .command({
command: ['check', '*'], command: ['check', '*'],
describe: 'validate the source documents', describe: 'validate the source documents',
handler: (argv: any) => { handler: (argi: any) => {
mode = 'check'; mode = 'check';
}, },
}) })
.command({ .command({
command: ['fix'],
describe: 'validate the source document and insert defaults',
builder: (yargs: any) => { builder: (yargs: any) => {
return yargs return yargs
.option('coerce', { .option('coerce', {
@ -46,21 +62,23 @@ export function parseArgs(argv: Array<string>) {
type: 'boolean', type: 'boolean',
}); });
}, },
handler: (argv: any) => { command: ['fix'],
describe: 'validate the source document and insert defaults',
handler: (argi: any) => {
mode = 'fix'; mode = 'fix';
}, },
}) })
.command({ .command({
command: ['list'], command: ['list'],
describe: 'list active rules', describe: 'list active rules',
handler: (argv: any) => { handler: (argi: any) => {
mode = 'list'; mode = 'list';
}, },
}) })
.command({ .command({
command: ['complete'], command: ['complete'],
describe: 'generate tab completion script for bash or zsh', describe: 'generate tab completion script for bash or zsh',
handler: (argv: any) => { handler: (argi: any) => {
mode = 'complete'; mode = 'complete';
}, },
}) })
@ -112,8 +130,11 @@ export function parseArgs(argv: Array<string>) {
}) })
.help() .help()
.version(VERSION_INFO.app.version) .version(VERSION_INFO.app.version)
.alias('version', 'v') .alias('version', 'v');
.argv;
// @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') { if (mode === 'complete') {
showCompletionScript(); showCompletionScript();

View File

@ -1,14 +1,26 @@
import { Stream } from 'bunyan';
import { isNil, isString } from 'lodash'; import { isNil, isString } from 'lodash';
import { LogLevel } from 'noicejs';
import { join } from 'path'; import { join } from 'path';
import { CONFIG_ENV, CONFIG_SCHEMA } from './schema';
import { includeSchema } from './type/Include';
import { NotFoundError } from '../error/NotFoundError'; import { NotFoundError } from '../error/NotFoundError';
import { YamlParser } from '../parser/YamlParser'; import { YamlParser } from '../parser/YamlParser';
import { readFileSync } from '../source'; import { readFileSync } from '../source';
import { CONFIG_ENV, CONFIG_SCHEMA } from './schema';
import { includeSchema } from './type/Include';
includeSchema.schema = CONFIG_SCHEMA; 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. * 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; 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); const paths = completePaths(name, extras);
for (const p of paths) { for (const p of paths) {

View File

@ -20,4 +20,3 @@ export const streamType = new YamlType('!stream', {
return Reflect.get(process, name); 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_SUCCESS = 0;
const STATUS_ERROR = 1; const STATUS_ERROR = 1;
const STATUS_MAX = 255;
export async function main(argv: Array<string>): Promise<number> { export async function main(argv: Array<string>): Promise<number> {
const { args, mode } = parseArgs(argv); 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 rules = await loadRules(args.rules, ctx);
const activeRules = await resolveRules(rules, args as any); const activeRules = await resolveRules(rules, args);
if (mode === 'list') { if (mode === 'list') {
logger.info({ rules: activeRules }, 'listing active rules'); 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) { if (ctx.errors.length > 0) {
logger.error({ count: ctx.errors.length, errors: ctx.errors }, 'some rules failed'); logger.error({ count: ctx.errors.length, errors: ctx.errors }, 'some rules failed');
if (args.count) { if (args.count) {
return Math.min(ctx.errors.length, 255); return Math.min(ctx.errors.length, STATUS_MAX);
} else { } else {
return STATUS_ERROR; return STATUS_ERROR;
} }

View File

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

View File

@ -1,12 +1,12 @@
import { ValidateFunction } from 'ajv'; import { ValidateFunction } from 'ajv';
import { applyDiff, diff } from 'deep-diff'; import { applyDiff, diff } from 'deep-diff';
import { JSONPath } from 'jsonpath-plus'; 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 { LogLevel } from 'noicejs';
import { YamlParser } from './parser/YamlParser'; import { YamlParser } from './parser/YamlParser';
import { readFileSync } from './source'; import { readFileSync } from './source';
import { isNilOrEmpty } from './utils'; import { ensureArray, isNilOrEmpty } from './utils';
import { friendlyError } from './utils/ajv'; import { friendlyError } from './utils/ajv';
import { Visitor } from './visitor'; import { Visitor } from './visitor';
import { VisitorContext } from './visitor/VisitorContext'; import { VisitorContext } from './visitor/VisitorContext';
@ -40,11 +40,77 @@ export interface RuleSource {
rules: Array<RuleData>; rules: Array<RuleData>;
} }
export function ensureArray<T>(val: Array<T> | undefined): Array<T> { export interface RuleResult extends VisitorResult {
if (isNil(val)) { rule: Rule;
return []; }
} else {
return Array.from(val); 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 {
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); 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; 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 { ErrorObject } from 'ajv';
import { isNil } from 'lodash';
import { VisitorError } from '../../visitor/VisitorError'; import { VisitorError } from '../../visitor/VisitorError';
@ -13,9 +14,9 @@ export function friendlyError(err: ErrorObject): VisitorError {
} }
export function friendlyErrorMessage(err: ErrorObject): string { export function friendlyErrorMessage(err: ErrorObject): string {
if (err.message) { if (isNil(err.message)) {
return `${err.dataPath} ${err.message}`;
} else {
return `${err.dataPath} ${err.keyword}`; 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> { export function isNilOrEmpty(val: Array<unknown> | null | undefined): val is Array<unknown> {
return (Array.isArray(val) && val.length > 0); 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; public readonly innerOptions: RuleOptions;
protected readonly ajv: Ajv.Ajv; protected readonly ajv: Ajv.Ajv;
protected readonly _changes: Array<any>; protected readonly changeBuffer: Array<any>;
protected readonly _errors: Array<VisitorError>; protected readonly errorBuffer: Array<VisitorError>;
public get changes(): ReadonlyArray<any> { public get changes(): ReadonlyArray<any> {
return this._changes; return this.changeBuffer;
} }
public get errors(): ReadonlyArray<VisitorError> { public get errors(): ReadonlyArray<VisitorError> {
return this._errors; return this.errorBuffer;
} }
constructor(options: VisitorContextOptions) { constructor(options: VisitorContextOptions) {
this._changes = []; this.changeBuffer = [];
this._errors = []; this.errorBuffer = [];
this.ajv = new Ajv({ this.ajv = new Ajv({
coerceTypes: options.innerOptions.coerce, coerceTypes: options.innerOptions.coerce,
@ -49,12 +49,12 @@ export class VisitorContext implements VisitorContextOptions, VisitorResult {
logWithLevel(this.logger, err.level, err.data, err.msg); logWithLevel(this.logger, err.level, err.data, err.msg);
} }
this._errors.push(...errors); this.errorBuffer.push(...errors);
} }
public mergeResult(other: VisitorResult): this { public mergeResult(other: VisitorResult): this {
this._changes.push(...other.changes); this.changeBuffer.push(...other.changes);
this._errors.push(...other.errors); this.errorBuffer.push(...other.errors);
return this; return this;
} }
@ -69,7 +69,7 @@ export class VisitorContext implements VisitorContextOptions, VisitorResult {
}); });
this.ajv.addSchema({ this.ajv.addSchema({
'$id': name, $id: name,
definitions: schema, definitions: schema,
}); });
} }

View File

@ -6,26 +6,26 @@ import { makeSelector, resolveRules, Rule, visitRules } from '../src/rule';
import { VisitorContext } from '../src/visitor/VisitorContext'; import { VisitorContext } from '../src/visitor/VisitorContext';
const TEST_RULES = [new Rule({ const TEST_RULES = [new Rule({
name: 'foo', check: {},
desc: '', desc: '',
level: 'info', level: 'info',
name: 'foo',
select: '$',
tags: ['all', 'foo'], tags: ['all', 'foo'],
check: {},
select: '$',
}), new Rule({ }), new Rule({
check: {},
desc: '',
level: 'warn',
name: 'bar', name: 'bar',
desc: '',
level: 'warn',
tags: ['all', 'test'],
check: {},
select: '$', select: '$',
tags: ['all', 'test'],
}), new Rule({ }), new Rule({
name: 'bin', check: {},
desc: '', desc: '',
level: 'warn', level: 'warn',
tags: ['all', 'test'], name: 'bin',
check: {},
select: '$', select: '$',
tags: ['all', 'test'],
})]; })];
describe('rule resolver', () => { describe('rule resolver', () => {
@ -102,21 +102,21 @@ describe('rule resolver', () => {
describe('rule visitor', () => { describe('rule visitor', () => {
it('should only call visit for selected items', async () => { it('should only call visit for selected items', async () => {
const ctx = new VisitorContext({ const ctx = new VisitorContext({
logger: new ConsoleLogger(),
innerOptions: { innerOptions: {
coerce: false, coerce: false,
defaults: false, defaults: false,
mutate: false, mutate: false,
} },
logger: new ConsoleLogger(),
}); });
const data = {}; const data = {};
const rule = new Rule({ const rule = new Rule({
name: 'foo', check: {},
desc: '', desc: '',
level: 'info', level: 'info',
tags: [], name: 'foo',
select: '$', select: '$',
check: {}, tags: [],
}); });
const mockRule = mock(rule); const mockRule = mock(rule);
@ -143,12 +143,12 @@ describe('rule visitor', () => {
}); });
const data = {}; const data = {};
const rule = new Rule({ const rule = new Rule({
name: 'foo', check: {},
desc: '', desc: '',
level: 'info', level: 'info',
tags: [], name: 'foo',
select: '$', select: '$',
check: {}, tags: [],
}); });
const mockRule = mock(rule); 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 { YamlParser } from '../../src/parser/YamlParser';
describe('yaml parser', () => { describe('yaml parser', () => {
describe('dump documents', () => { describe('dump documents', () => {
it('should dump multiple documents', () => { it('should dump multiple documents', () => {
const parser = new YamlParser(); const parser = new YamlParser();
const data = parser.dump({}, {}); const data = parser.dump({}, {});
expect(data).to.contain('---'); expect(data).to.contain('---');
}); });
}); });
@ -20,7 +20,7 @@ foo: {}
--- ---
bar: {} bar: {}
`); `);
expect(Array.isArray(data)).to.equal(true); expect(Array.isArray(data)).to.equal(true);
expect(data.length).to.equal(2); expect(data.length).to.equal(2);
}); });

View File

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