UNPKG

@hero-design/snowflake-guard

Version:

A hero-design bot detecting snowflake usage

176 lines (175 loc) 9.95 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; const parseMobileSource_1 = __importDefault(require("./parseMobileSource")); const parseSource_1 = __importDefault(require("./parseSource")); const constants_1 = require("./reports/constants"); const fetchGraphql_1 = __importDefault(require("./graphql/fetchGraphql")); const queryGenerators_1 = require("./graphql/queryGenerators"); const getDiffLocs_1 = require("./utils/getDiffLocs"); const WEB_REGEX = /\.tsx$/; const MOBILE_REGEX = /\.(tsx|jsx|js)$/; const TEST_REGEX = /__tests__/; const SNOWFLAKE_COMMENTS_WEB = { style: 'Snowflake detected! A component is customized using inline styles. Make sure to not use [prohibited CSS properties](https://docs.google.com/spreadsheets/d/1Dj8vqLdFaf-CSaSVoYqyYZIkGqF6OoyP7K4G1_9L62U/edit?usp=sharing).', sx: 'Snowflake detected! A component is customized via sx prop. Make sure to not use [prohibited CSS properties](https://docs.google.com/spreadsheets/d/1Dj8vqLdFaf-CSaSVoYqyYZIkGqF6OoyP7K4G1_9L62U/edit?usp=sharing).', 'styled-component': 'Please do not use styled-component to customize this component, use sx prop or inline style instead.', className: `Please make sure that this className is not used as a CSS classname for component customization purposes, use sx prop or inline style instead. In case this is none-css classname, please flag it with this comment \`${constants_1.APPROVED_CLASSNAME_COMMENT}\`.`, }; const SNOWFLAKE_COMMENTS_MOBILE = { style: 'Snowflake detected! A component is customized using inline styles. Make sure to not use [prohibited CSS properties](https://docs.google.com/spreadsheets/d/1Dj8vqLdFaf-CSaSVoYqyYZIkGqF6OoyP7K4G1_9L62U/edit?gid=1761479144#gid=1761479144).', 'styled-component': 'Please do not use styled-component to customize this component.', }; const MOBILE_REPO_NAMES = JSON.parse(process.env.MOBILE_REPO_NAMES || '[]'); const APPROVED_USER_LIST = JSON.parse(process.env.APPROVED_USER_LIST || '[]'); const checkIfDetectedSnowflakesInDiff = (diffLocs, locToComment) => { const locIdx = diffLocs.findIndex(([start, end]) => { return locToComment >= start && locToComment <= end; }); return locIdx !== -1; }; module.exports = (app) => { app.on(['pull_request.opened', 'pull_request.synchronize'], (context) => __awaiter(void 0, void 0, void 0, function* () { var _a; // Get PR info const prNumber = context.payload.number; const repoInfo = { repo: context.payload.repository.name, owner: context.payload.repository.owner.login, }; const prBranch = context.payload.pull_request.head.ref; // List all changed files const prFiles = yield context.octokit.pulls.listFiles(Object.assign(Object.assign({}, repoInfo), { pull_number: prNumber })); const isMobile = MOBILE_REPO_NAMES.includes(repoInfo.repo); const parseSource = isMobile ? parseMobileSource_1.default : parseSource_1.default; const SOURCE_REGEX = isMobile ? MOBILE_REGEX : WEB_REGEX; const sourceFiles = prFiles.data.filter((file) => SOURCE_REGEX.test(file.filename) && !TEST_REGEX.test(file.filename) && file.status !== 'removed'); // Saving file patches to get diff locations const prFilePatches = sourceFiles.reduce((acc, file) => { acc[file.filename] = file.patch || ''; return acc; }, {}); // Get file contents const prFileContentPromises = sourceFiles.map((file) => context.octokit.repos.getContent(Object.assign(Object.assign({}, repoInfo), { path: file.filename, ref: prBranch }))); const prFileContents = yield Promise.all(prFileContentPromises); const snowflakeComments = []; const approvedSnowflakeLocs = []; prFileContents.forEach((file) => __awaiter(void 0, void 0, void 0, function* () { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const filePath = file.data.path; const diffLocs = (0, getDiffLocs_1.getDiffLocs)(prFilePatches[filePath]); const stringContent = Buffer.from( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore file.data.content, 'base64').toString(); // Parse file content to check for snowflakes const snowflakeReport = parseSource(stringContent); snowflakeReport.styleLocs.forEach((loc) => { if (checkIfDetectedSnowflakesInDiff(diffLocs, loc)) { snowflakeComments.push({ path: filePath, body: isMobile ? SNOWFLAKE_COMMENTS_MOBILE['style'] : SNOWFLAKE_COMMENTS_WEB['style'], line: loc, }); } }); snowflakeReport.styledComponentLocs.forEach((loc) => { if (checkIfDetectedSnowflakesInDiff(diffLocs, loc)) { snowflakeComments.push({ path: filePath, body: isMobile ? SNOWFLAKE_COMMENTS_MOBILE['styled-component'] : SNOWFLAKE_COMMENTS_WEB['styled-component'], line: loc, }); } }); snowflakeReport.approvedLocs.forEach((loc) => { if (checkIfDetectedSnowflakesInDiff(diffLocs, loc)) { approvedSnowflakeLocs.push(loc); } }); if (!isMobile) { snowflakeReport.sxLocs.forEach((loc) => { if (checkIfDetectedSnowflakesInDiff(diffLocs, loc)) { snowflakeComments.push({ path: filePath, body: SNOWFLAKE_COMMENTS_WEB['sx'], line: loc, }); } }); snowflakeReport.classNameLocs.forEach((loc) => { if (checkIfDetectedSnowflakesInDiff(diffLocs, loc)) { snowflakeComments.push({ path: filePath, body: SNOWFLAKE_COMMENTS_WEB['className'], line: loc, }); } }); } })); // Saving report const snowflakeCount = snowflakeComments.length; const report = (yield (0, fetchGraphql_1.default)((0, queryGenerators_1.generateFetchReportQuery)({ repoName: repoInfo.repo, prNumber }))); const reportData = (_a = report.data) === null || _a === void 0 ? void 0 : _a.fetchHdSnowflakeGuardReport; if (reportData) { yield (0, fetchGraphql_1.default)((0, queryGenerators_1.generateUpdateReportQuery)({ id: reportData.id, latestCount: snowflakeCount, approvedCount: approvedSnowflakeLocs.length, })); } else { yield (0, fetchGraphql_1.default)((0, queryGenerators_1.generateCreateReportQuery)({ repoName: repoInfo.repo, prNumber, owner: repoInfo.owner, originalCount: snowflakeCount, latestCount: snowflakeCount, approvedCount: approvedSnowflakeLocs.length, })); } // No snowflakes detected // Create success check-run if (snowflakeCount === 0) { return context.octokit.checks.create(Object.assign(Object.assign({}, repoInfo), { name: 'SnowflakeGuard/Check', head_sha: context.payload.pull_request.head.sha, status: 'completed', conclusion: 'success' })); } // Snowflakes detected // Create failed check-run & comment yield context.octokit.checks.create(Object.assign(Object.assign({}, repoInfo), { name: 'SnowflakeGuard/Check', head_sha: context.payload.pull_request.head.sha, status: 'completed', conclusion: 'failure' })); return context.octokit.pulls.createReview(Object.assign(Object.assign({}, repoInfo), { pull_number: prNumber, commit_id: context.payload.pull_request.head.sha, event: 'COMMENT', body: 'Snowflake Guard Bot has detected some snowflakes in this PR. Please review the following comments.', comments: snowflakeComments })); })); app.on('pull_request_review.submitted', (context) => { const submittedType = context.payload.review.state; const submittedUser = context.payload.review.user.login; if (submittedType !== 'approved' || !APPROVED_USER_LIST.includes(submittedUser)) { return; } return context.octokit.checks.create({ repo: context.payload.repository.name, owner: context.payload.repository.owner.login, name: 'SnowflakeGuard/Check', head_sha: context.payload.pull_request.head.sha, status: 'completed', conclusion: 'success', }); }); };