@azure/avocado
Advanced tools
Comparing version 0.8.7 to 0.8.8
@@ -13,4 +13,9 @@ #!/usr/bin/env node | ||
type: 'array', | ||
desc: 'array contains path patterns to be ignored' | ||
desc: 'array contains path patterns to be ignored', | ||
}) | ||
.option('includePaths', { | ||
type: 'array', | ||
desc: | ||
'array contains path patterns to be included. If this option is not set, all files will be included. If this option is set, only files that match at least one pattern will be included', | ||
}) | ||
.help('h') | ||
@@ -21,2 +26,6 @@ .alias('h', 'help').argv | ||
cli.run(index.avocado, index.UnifiedPipelineReport(argv.f), {cwd: process.cwd(), env: process.env, args: {dir: argv.d, excludePaths: argv.excludePaths}}) | ||
cli.run(index.avocado, index.UnifiedPipelineReport(argv.f), { | ||
cwd: process.cwd(), | ||
env: process.env, | ||
args: { dir: argv.d, excludePaths: argv.excludePaths, includePaths: argv.includePaths }, | ||
}) |
import { JsonParseError } from './errors'; | ||
import * as jsonParser from '@ts-common/json-parser'; | ||
import * as format from '@azure/swagger-validation-common'; | ||
declare type ErrorMessage = 'The example JSON file is not referenced from the swagger file.' | 'The swagger JSON file is not referenced from the readme file.' | 'The `readme.md` is not an AutoRest markdown file.' | 'The JSON file is not found but it is referenced from the readme file.' | 'The JSON file has a circular reference.' | 'The file is not a valid JSON file.' | 'Can not find readme.md in the folder. If no readme.md file, it will block SDK generation.' | 'The API version of the swagger is inconsistent with its file path.' | 'The default tag contains multiple API versions swaggers.' | 'The management plane swagger JSON file does not match its folder path. Make sure management plane swagger located in resource-manager folder' | 'The default tag does not contain all APIs in this RP. Please make sure the missing API swaggers are in the default tag.' | 'The default tag does not contains the latest API version. Please make sure the latest api version swaggers are in the default tag.'; | ||
declare type ErrorMessage = 'The example JSON file is not referenced from the swagger file.' | 'The swagger JSON file is not referenced from the readme file.' | 'The `readme.md` is not an AutoRest markdown file.' | 'The JSON file is not found but it is referenced from the readme file.' | 'The JSON file has a circular reference.' | 'The file is not a valid JSON file.' | 'Can not find readme.md in the folder. If no readme.md file, it will block SDK generation.' | 'The API version of the swagger is inconsistent with its file path.' | 'The default tag contains multiple API versions swaggers.' | 'The management plane swagger JSON file does not match its folder path. Make sure management plane swagger located in resource-manager folder' | 'The default tag does not contain all APIs in this RP. Please make sure the missing API swaggers are in the default tag.' | 'The default tag does not contains the latest API version. Please make sure the latest api version swaggers are in the default tag.' | 'The readme file has more than one default tag.'; | ||
export interface IErrorBase { | ||
@@ -26,2 +26,8 @@ readonly level: 'Warning' | 'Error' | 'Info'; | ||
} & IErrorBase; | ||
export declare type MultipleDefaultTags = { | ||
readonly code: 'MULTIPLE_DEFAULT_TAGS'; | ||
readonly message: ErrorMessage; | ||
readonly readMeUrl: string; | ||
readonly tags: string[]; | ||
} & IErrorBase; | ||
export declare type MissingLatestApiInDefaultTag = { | ||
@@ -46,4 +52,4 @@ readonly code: 'MISSING_APIS_IN_DEFAULT_TAG' | 'NOT_LATEST_API_VERSION_IN_DEFAULT_TAG'; | ||
export declare const getPathInfoFromError: (error: Error) => format.JsonPath[]; | ||
export declare type Error = JsonParseError | FileError | NotAutoRestMarkDown | MissingReadmeError | MultipleApiVersion | MissingLatestApiInDefaultTag; | ||
export declare type Error = JsonParseError | FileError | NotAutoRestMarkDown | MissingReadmeError | MultipleApiVersion | MissingLatestApiInDefaultTag | MultipleDefaultTags; | ||
export {}; | ||
//# sourceMappingURL=errors.d.ts.map |
@@ -36,2 +36,3 @@ "use strict"; | ||
{ tag: 'json', path: format.blobHref(format.getRelativeSwaggerPathToRepo(error.jsonUrl)) }, | ||
{ tag: 'path', path: error.path }, | ||
]; | ||
@@ -43,2 +44,4 @@ case 'NOT_LATEST_API_VERSION_IN_DEFAULT_TAG': | ||
]; | ||
case 'MULTIPLE_DEFAULT_TAGS': | ||
return [{ tag: 'readme', path: format.blobHref(format.getRelativeSwaggerPathToRepo(error.readMeUrl)) }]; | ||
default: | ||
@@ -45,0 +48,0 @@ return []; |
@@ -12,2 +12,6 @@ import * as md from '@ts-common/commonmark-to-markdown'; | ||
/** | ||
* @return return tag string array. | ||
*/ | ||
export declare const getAllDefaultTags: (markDown: commonmark.Node) => string[]; | ||
/** | ||
* @return return undefined indicates not found, otherwise return non-empty string. | ||
@@ -14,0 +18,0 @@ */ |
@@ -82,2 +82,3 @@ "use strict"; | ||
readMeUrl: error.readMeUrl, | ||
path: error.path, | ||
}; | ||
@@ -92,2 +93,9 @@ } | ||
} | ||
case 'MULTIPLE_DEFAULT_TAGS': { | ||
return { | ||
code: error.code, | ||
url: error.readMeUrl, | ||
tags: error.tags, | ||
}; | ||
} | ||
} | ||
@@ -127,3 +135,53 @@ }; | ||
}; | ||
const nodeHeading = (startNode) => { | ||
let resultNode = startNode; | ||
while (resultNode !== null && resultNode.type !== 'heading') { | ||
resultNode = resultNode.prev || resultNode.parent; | ||
} | ||
return resultNode; | ||
}; | ||
const getHeadingLiteral = (heading) => { | ||
const headingNode = walkToNode(heading.walker(), n => n.type === 'text'); | ||
return headingNode && headingNode.literal ? headingNode.literal : ''; | ||
}; | ||
/** | ||
* walks a markdown tree until the callback provided returns true for a node | ||
*/ | ||
const walkToNode = (walker, cb) => { | ||
let event = walker.next(); | ||
while (event) { | ||
const curNode = event.node; | ||
if (cb(curNode)) { | ||
return curNode; | ||
} | ||
event = walker.next(); | ||
} | ||
return undefined; | ||
}; | ||
/** | ||
* @return return tag string array. | ||
*/ | ||
exports.getAllDefaultTags = (markDown) => { | ||
const startNode = markDown; | ||
const walker = startNode.walker(); | ||
const tags = []; | ||
while (true) { | ||
const node = walkToNode(walker, n => n.type === 'code_block'); | ||
if (!node) { | ||
break; | ||
} | ||
const heading = nodeHeading(node); | ||
if (!heading) { | ||
continue; | ||
} | ||
if (getHeadingLiteral(heading) === 'Basic Information' && node.literal) { | ||
const latestDefinition = safeLoad(node.literal); | ||
if (latestDefinition && latestDefinition.tag) { | ||
tags.push(latestDefinition.tag); | ||
} | ||
} | ||
} | ||
return tags; | ||
}; | ||
/** | ||
* @return return undefined indicates not found, otherwise return non-empty string. | ||
@@ -294,2 +352,14 @@ */ | ||
const m = md.parse(readmeContent); | ||
const defaultTags = exports.getAllDefaultTags(m.markDown); | ||
if (defaultTags.length > 1) { | ||
yield { | ||
code: 'MULTIPLE_DEFAULT_TAGS', | ||
level: 'Warning', | ||
message: 'The readme file has more than one default tag.', | ||
path: readme, | ||
readMeUrl: readme, | ||
tags: defaultTags, | ||
}; | ||
continue; | ||
} | ||
const inputFiles = exports.getSwaggerFileUnderDefaultTag(m); | ||
@@ -648,3 +718,3 @@ let defaultTagPathTable = new Map(); | ||
*/ | ||
const avocadoForDir = async (dir, exclude) => { | ||
const avocadoForDir = async (dir, exclude, include) => { | ||
const map = new Map(); | ||
@@ -658,3 +728,4 @@ if (fs.existsSync(dir)) { | ||
for (const [k, v] of map) { | ||
if (exclude.some(item => v.path.search(item) !== -1)) { | ||
if ((include.length > 0 && include.every(item => v.path.search(item) === -1)) || | ||
exclude.some(item => v.path.search(item) !== -1)) { | ||
map.delete(k); | ||
@@ -671,3 +742,3 @@ } | ||
*/ | ||
const avocadoForDevOps = (pr, exclude) => asyncIt.iterable(async function* () { | ||
const avocadoForDevOps = (pr, exclude, include) => asyncIt.iterable(async function* () { | ||
// collect all errors from the 'targetBranch' | ||
@@ -699,6 +770,6 @@ const diffFiles = await pr.diff(); | ||
await pr.checkout(pr.targetBranch); | ||
const targetMap = await avocadoForDir(path.resolve(pr.workingDir, dir), exclude); | ||
const targetMap = await avocadoForDir(path.resolve(pr.workingDir, dir), exclude, include); | ||
// collect all errors from the 'sourceBranch' | ||
await pr.checkout(pr.sourceBranch); | ||
const sourceMap = await avocadoForDir(path.resolve(pr.workingDir, dir), exclude); | ||
const sourceMap = await avocadoForDir(path.resolve(pr.workingDir, dir), exclude, include); | ||
const fileChanges = await pr.diff(); | ||
@@ -727,4 +798,8 @@ // remove existing errors. | ||
} | ||
let include = []; | ||
if (config.args && config.args.includePaths) { | ||
include = config.args.includePaths; | ||
} | ||
if (pr !== undefined) { | ||
yield* avocadoForDevOps(pr, exclude); | ||
yield* avocadoForDevOps(pr, exclude, include); | ||
} | ||
@@ -737,3 +812,3 @@ else { | ||
} | ||
yield* (await avocadoForDir(path.resolve(config.cwd, dir), exclude)).values(); | ||
yield* (await avocadoForDir(path.resolve(config.cwd, dir), exclude, include)).values(); | ||
} | ||
@@ -740,0 +815,0 @@ }); |
{ | ||
"name": "@azure/avocado", | ||
"version": "0.8.7", | ||
"version": "0.8.8", | ||
"description": "A validator of OpenAPI configurations", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
@@ -24,2 +24,3 @@ import { JsonParseError } from './errors' | ||
| 'The default tag does not contains the latest API version. Please make sure the latest api version swaggers are in the default tag.' | ||
| 'The readme file has more than one default tag.' | ||
@@ -51,2 +52,9 @@ export interface IErrorBase { | ||
export type MultipleDefaultTags = { | ||
readonly code: 'MULTIPLE_DEFAULT_TAGS' | ||
readonly message: ErrorMessage | ||
readonly readMeUrl: string | ||
readonly tags: string[] | ||
} & IErrorBase | ||
export type MissingLatestApiInDefaultTag = { | ||
@@ -110,2 +118,3 @@ readonly code: 'MISSING_APIS_IN_DEFAULT_TAG' | 'NOT_LATEST_API_VERSION_IN_DEFAULT_TAG' | ||
{ tag: 'json', path: format.blobHref(format.getRelativeSwaggerPathToRepo(error.jsonUrl)) }, | ||
{ tag: 'path', path: error.path }, | ||
] | ||
@@ -117,2 +126,4 @@ case 'NOT_LATEST_API_VERSION_IN_DEFAULT_TAG': | ||
] | ||
case 'MULTIPLE_DEFAULT_TAGS': | ||
return [{ tag: 'readme', path: format.blobHref(format.getRelativeSwaggerPathToRepo(error.readMeUrl)) }] | ||
default: | ||
@@ -130,1 +141,2 @@ return [] | ||
| MissingLatestApiInDefaultTag | ||
| MultipleDefaultTags |
107
src/index.ts
@@ -82,2 +82,3 @@ // Copyright (c) Microsoft Corporation. All rights reserved. | ||
readMeUrl: error.readMeUrl, | ||
path: error.path, | ||
} | ||
@@ -92,2 +93,9 @@ } | ||
} | ||
case 'MULTIPLE_DEFAULT_TAGS': { | ||
return { | ||
code: error.code, | ||
url: error.readMeUrl, | ||
tags: error.tags, | ||
} | ||
} | ||
} | ||
@@ -133,3 +141,64 @@ } | ||
const nodeHeading = (startNode: commonmark.Node): commonmark.Node | null => { | ||
let resultNode: commonmark.Node | null = startNode | ||
while (resultNode !== null && resultNode.type !== 'heading') { | ||
resultNode = resultNode.prev || resultNode.parent | ||
} | ||
return resultNode | ||
} | ||
const getHeadingLiteral = (heading: commonmark.Node): string => { | ||
const headingNode = walkToNode(heading.walker(), n => n.type === 'text') | ||
return headingNode && headingNode.literal ? headingNode.literal : '' | ||
} | ||
/** | ||
* walks a markdown tree until the callback provided returns true for a node | ||
*/ | ||
const walkToNode = ( | ||
walker: commonmark.NodeWalker, | ||
cb: (node: commonmark.Node) => boolean, | ||
): commonmark.Node | undefined => { | ||
let event = walker.next() | ||
while (event) { | ||
const curNode = event.node | ||
if (cb(curNode)) { | ||
return curNode | ||
} | ||
event = walker.next() | ||
} | ||
return undefined | ||
} | ||
/** | ||
* @return return tag string array. | ||
*/ | ||
export const getAllDefaultTags = (markDown: commonmark.Node): string[] => { | ||
const startNode = markDown | ||
const walker = startNode.walker() | ||
const tags = [] | ||
while (true) { | ||
const node = walkToNode(walker, n => n.type === 'code_block') | ||
if (!node) { | ||
break | ||
} | ||
const heading = nodeHeading(node) | ||
if (!heading) { | ||
continue | ||
} | ||
if (getHeadingLiteral(heading) === 'Basic Information' && node.literal) { | ||
const latestDefinition = safeLoad(node.literal) | ||
if (latestDefinition && latestDefinition.tag) { | ||
tags.push(latestDefinition.tag) | ||
} | ||
} | ||
} | ||
return tags | ||
} | ||
/** | ||
* @return return undefined indicates not found, otherwise return non-empty string. | ||
@@ -347,2 +416,15 @@ */ | ||
const m = md.parse(readmeContent) | ||
const defaultTags = getAllDefaultTags(m.markDown) | ||
if (defaultTags.length > 1) { | ||
yield { | ||
code: 'MULTIPLE_DEFAULT_TAGS', | ||
level: 'Warning', | ||
message: 'The readme file has more than one default tag.', | ||
path: readme, | ||
readMeUrl: readme, | ||
tags: defaultTags, | ||
} | ||
continue | ||
} | ||
const inputFiles = getSwaggerFileUnderDefaultTag(m) | ||
@@ -752,3 +834,3 @@ let defaultTagPathTable = new Map<string, { apiVersion: string; swaggerFile: string }>() | ||
*/ | ||
const avocadoForDir = async (dir: string, exclude: string[]) => { | ||
const avocadoForDir = async (dir: string, exclude: string[], include: string[]) => { | ||
const map = new Map<string, err.Error>() | ||
@@ -762,3 +844,6 @@ if (fs.existsSync(dir)) { | ||
for (const [k, v] of map) { | ||
if (exclude.some(item => v.path.search(item) !== -1)) { | ||
if ( | ||
(include.length > 0 && include.every(item => v.path.search(item) === -1)) || | ||
exclude.some(item => v.path.search(item) !== -1) | ||
) { | ||
map.delete(k) | ||
@@ -776,3 +861,7 @@ } | ||
*/ | ||
const avocadoForDevOps = (pr: devOps.PullRequestProperties, exclude: string[]): asyncIt.AsyncIterableEx<err.Error> => | ||
const avocadoForDevOps = ( | ||
pr: devOps.PullRequestProperties, | ||
exclude: string[], | ||
include: string[], | ||
): asyncIt.AsyncIterableEx<err.Error> => | ||
asyncIt.iterable<err.Error>(async function*() { | ||
@@ -806,7 +895,7 @@ // collect all errors from the 'targetBranch' | ||
await pr.checkout(pr.targetBranch) | ||
const targetMap = await avocadoForDir(path.resolve(pr.workingDir, dir), exclude) | ||
const targetMap = await avocadoForDir(path.resolve(pr.workingDir, dir), exclude, include) | ||
// collect all errors from the 'sourceBranch' | ||
await pr.checkout(pr.sourceBranch) | ||
const sourceMap = await avocadoForDir(path.resolve(pr.workingDir, dir), exclude) | ||
const sourceMap = await avocadoForDir(path.resolve(pr.workingDir, dir), exclude, include) | ||
@@ -840,4 +929,8 @@ const fileChanges = await pr.diff() | ||
} | ||
let include = [] | ||
if (config.args && config.args.includePaths) { | ||
include = config.args.includePaths | ||
} | ||
if (pr !== undefined) { | ||
yield* avocadoForDevOps(pr, exclude) | ||
yield* avocadoForDevOps(pr, exclude, include) | ||
} else { | ||
@@ -849,3 +942,3 @@ // tslint:disable-next-line: no-let | ||
} | ||
yield* (await avocadoForDir(path.resolve(config.cwd, dir), exclude)).values() | ||
yield* (await avocadoForDir(path.resolve(config.cwd, dir), exclude, include)).values() | ||
} | ||
@@ -852,0 +945,0 @@ }) |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
149512
2639