UNPKG

@atomist/sdm-pack-aspect

Version:

an Atomist SDM Extension Pack for visualizing drift across an organization

180 lines (162 loc) 5.73 kB
/* * Copyright © 2019 Atomist, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { logger, NoParameters, ProjectReview, ReviewComment, } from "@atomist/automation-client"; import { CodeTransform, ReviewerRegistration, } from "@atomist/sdm"; import { ApplyFingerprint, Aspect, fingerprintOf, FP, } from "@atomist/sdm-pack-fingerprint"; import { CodeInspection } from "@atomist/sdm/lib/api/registration/CodeInspectionRegistration"; import { ClassificationAspect, projectClassificationAspect, } from "../compose/classificationAspect"; import { AspectMetadata, CountAspect, CountData, } from "../compose/commonTypes"; export type EligibleReviewer = ReviewerRegistration | CodeInspection<ProjectReview, NoParameters>; export interface ReviewerAspectOptions extends AspectMetadata { /** * Reviewer that can provide the fingerprint */ readonly reviewer: EligibleReviewer; /** * Code transform that can remove usages of this problematic fingerprint */ readonly terminator?: CodeTransform<NoParameters>; /** * Do we want classification for this aspect */ readonly emitClassifier?: boolean; /** * If provided, causes classification to take place and specifies a custom tag. * Otherwise tag will default to name passed into reviewerAspects */ readonly tag?: string; } /** * Emit fingerprint aspect, count aspect and classification aspect for the given review comment. * If a terminator CodeTransform is provided, it will try to delete all instances of the fingerprint */ export function reviewerAspects(opts: ReviewerAspectOptions): Aspect[] { const aspects: Aspect[] = [ reviewCommentAspect(opts), reviewCommentCountAspect(opts), ]; if (opts.emitClassifier || opts.tag) { aspects.push(reviewCommentClassificationAspect(opts)); } return aspects; } export function isReviewCommentFingerprint(fp: FP): fp is FP<ReviewComment> { const maybe = fp.data as ReviewComment; return !!maybe && !!maybe.subcategory && !!maybe.detail; } /** * Create fingerprints from the output of this reviewer. * Every fingerprint is unique */ function reviewCommentAspect(opts: ReviewerAspectOptions): Aspect<ReviewComment> { const inspection = isReviewerRegistration(opts.reviewer) ? opts.reviewer.inspection : opts.reviewer; const type = reviewCommentAspectName(opts.name); return { ...opts, name: type, extract: async (p, pli) => { const result = await inspection(p, { ...pli, push: pli }); if (!result) { return []; } return result.comments.map(data => { return fingerprintOf({ type, data, }); }); }, }; } function reviewCommentAspectName(name: string): string { return "instance_" + name; } function reviewCommentClassificationAspect(opts: ReviewerAspectOptions): ClassificationAspect { const requiredType = reviewCommentAspectName(opts.name); return projectClassificationAspect({ name: `has_${opts.name}`, displayName: opts.displayName, }, { tags: opts.tag || `has-${opts.name}`, reason: `Has review comment ${opts.name}`, testFingerprints: async fps => fps.some(fp => isReviewCommentFingerprint(fp) && fp.type === requiredType), }); } /** * Count the problematic usage and delete it if necessary * @param {ReviewerAspectOptions} opts * @return {CountAspect} */ export function reviewCommentCountAspect(opts: ReviewerAspectOptions): CountAspect { const requiredType = reviewCommentAspectName(opts.name); const type = countFingerprintTypeFor(opts.name); return { name: type, displayName: opts.displayName, extract: async () => [], consolidate: async fps => { const count = fps.filter(fp => isReviewCommentFingerprint(fp) && fp.type === requiredType).length; return fingerprintOf({ type, data: { count }, }); }, apply: opts.terminator ? terminateWithExtremePrejudice(opts) : undefined, }; } function countFingerprintTypeFor(name: string): string { return `count_${name}`; } export function findReviewCommentCountFingerprint(name: string, fps: FP[]): FP<CountData> | undefined { const type = countFingerprintTypeFor(name); return fps.find(fp => fp.type === type); } function terminateWithExtremePrejudice(opts: ReviewerAspectOptions): ApplyFingerprint { return async (p, pi) => { const to = pi.parameters.fp; if (to.data.count !== 0) { const msg = `Doesn't make sense to keep a non-zero number of fingerprints in ${opts.name}`; logger.warn(msg); return { target: p, success: false, error: new Error(msg) }; } return opts.terminator(p, pi); }; } function isReviewerRegistration(er: EligibleReviewer): er is ReviewerRegistration { const maybe = er as ReviewerRegistration; return !!maybe.inspection; }