refactor: fix hidden eslint errors (#49365)

* refactor: explicit types for validate

* refactor: explicit return types for ui-components

* refactor: use exec instead of match

* refactor: add lots more boundary types

* refactor: more eslint warnings

* refactor: more explicit exports

* refactor: more explicit types

* refactor: even more explicit types

* fix: relax type contrainsts for superblock-order

* refactor: final boundaries

* refactor: avoid using 'object' type

* fix: use named import for captureException

This uses TypeScript (which works) instead of import/namespace
(which doesn't) to check if captureException exists in sentry/gatsby
(it does)
This commit is contained in:
Oliver Eyton-Williams
2023-02-13 16:13:50 +01:00
committed by GitHub
parent 218fe6605b
commit 4ff00922da
40 changed files with 196 additions and 84 deletions

View File

@@ -215,7 +215,7 @@ const schemaValidation = (
if (
fileName === 'motivation' &&
!(fileJson.motivationalQuotes as MotivationalQuotes).every(
(object: object) =>
object =>
Object.prototype.hasOwnProperty.call(object, 'quote') &&
Object.prototype.hasOwnProperty.call(object, 'author')
)

View File

@@ -9,7 +9,7 @@ type Props = {
};
type Solution = Pick<ChallengeFile, 'ext' | 'contents' | 'fileKey'>;
function SolutionViewer({ challengeFiles, solution }: Props) {
function SolutionViewer({ challengeFiles, solution }: Props): JSX.Element {
const isLegacy = !challengeFiles || !challengeFiles.length;
const solutions = isLegacy
? [

View File

@@ -6,15 +6,16 @@ interface FullWidthRowProps {
className?: string;
}
const FullWidthRow = ({ children, className }: FullWidthRowProps) => {
return (
<Row className={className}>
<Col sm={8} smOffset={2} xs={12}>
{children}
</Col>
</Row>
);
};
const FullWidthRow = ({
children,
className
}: FullWidthRowProps): JSX.Element => (
<Row className={className}>
<Col sm={8} smOffset={2} xs={12}>
{children}
</Col>
</Row>
);
FullWidthRow.displayName = 'FullWidthRow';

View File

@@ -17,10 +17,23 @@ interface SiteData {
};
}
interface Item {
'@type': 'Course';
url: string;
name: string;
description?: string;
provider: {
'@type': 'Organization';
name: string;
sameAs: string;
nonprofitStatus: string;
};
}
interface ListItem {
'@type': 'ListItem';
position: number;
item: object;
item: Item;
}
interface StructuredData {

View File

@@ -22,7 +22,7 @@ export function SolutionDisplayWidget({
showUserCode,
showProjectPreview,
displayContext
}: Props) {
}: Props): JSX.Element | null {
const { id, solution, githubLink } = completedChallenge;
const { t } = useTranslation();
const viewText = t('buttons.view');

View File

@@ -39,17 +39,17 @@ class MobileLayout extends Component<MobileLayoutProps, MobileLayoutState> {
currentTab: this.props.hasEditableBoundaries ? Tab.Editor : Tab.Instructions
};
switchTab = (tab: Tab) => {
switchTab = (tab: Tab): void => {
this.setState({
currentTab: tab
});
};
handleKeyDown = () => this.props.updateUsingKeyboardInTablist(true);
handleKeyDown = (): void => this.props.updateUsingKeyboardInTablist(true);
handleClick = () => this.props.updateUsingKeyboardInTablist(false);
handleClick = (): void => this.props.updateUsingKeyboardInTablist(false);
render() {
render(): JSX.Element {
const { currentTab } = this.state;
const {
hasEditableBoundaries,

View File

@@ -10,7 +10,7 @@ export function concatHtml({
required = [],
template,
contents
}: ConcatHTMLOptions) {
}: ConcatHTMLOptions): string {
const embedSource = template
? _template(template)
: ({ source }: { source: ConcatHTMLOptions['contents'] }) => source;

View File

@@ -107,11 +107,13 @@ const buildFunctions = {
[challengeTypes.multifileCertProject]: buildDOMChallenge
};
export function canBuildChallenge(challengeData: BuildChallengeData) {
export function canBuildChallenge(challengeData: BuildChallengeData): boolean {
const { challengeType } = challengeData;
return Object.prototype.hasOwnProperty.call(buildFunctions, challengeType);
}
// TODO: Figure out and (hopefully) simplify the return type.
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export async function buildChallenge(
challengeData: BuildChallengeData,
options: BuildOptions
@@ -131,6 +133,8 @@ const testRunners = {
[challengeTypes.pythonProject]: getDOMTestRunner,
[challengeTypes.multifileCertProject]: getDOMTestRunner
};
// TODO: Figure out and (hopefully) simplify the return type.
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function getTestRunner(
buildData: BuildChallengeData,
runnerConfig: TestRunnerConfig,
@@ -185,10 +189,16 @@ async function getDOMTestRunner(
runTestInTestFrame(document, testString, testTimeout);
}
type BuildResult = {
challengeType: number;
build: string;
sources: Source | undefined;
};
export function buildDOMChallenge(
{ challengeFiles, required = [], template = '' }: BuildChallengeData,
{ usesTestRunner } = { usesTestRunner: false }
) {
): Promise<BuildResult> | undefined {
const finalRequires = [...required];
if (usesTestRunner) finalRequires.push(...frameRunner);
@@ -225,7 +235,7 @@ export function buildDOMChallenge(
export function buildJSChallenge(
{ challengeFiles }: { challengeFiles: ChallengeFiles },
options: BuildOptions
) {
): Promise<BuildResult> | undefined {
const pipeLine = composeFunctions(...getTransformers(options));
const finalFiles = challengeFiles?.map(pipeLine);
@@ -262,7 +272,7 @@ export function updatePreview(
buildData: BuildChallengeData,
document: Document,
proxyLogger: ProxyLogger
) {
): void {
if (
buildData.challengeType === challengeTypes.html ||
buildData.challengeType === challengeTypes.multifileCertProject
@@ -293,7 +303,7 @@ function getDocumentTitle(buildData: BuildChallengeData) {
export function updateProjectPreview(
buildData: BuildChallengeData,
document: Document
) {
): void {
if (
buildData.challengeType === challengeTypes.html ||
buildData.challengeType === challengeTypes.multifileCertProject
@@ -309,7 +319,7 @@ export function updateProjectPreview(
}
}
export function challengeHasPreview({ challengeType }: ChallengeMeta) {
export function challengeHasPreview({ challengeType }: ChallengeMeta): boolean {
return (
challengeType === challengeTypes.html ||
challengeType === challengeTypes.modern ||
@@ -317,13 +327,15 @@ export function challengeHasPreview({ challengeType }: ChallengeMeta) {
);
}
export function isJavaScriptChallenge({ challengeType }: ChallengeMeta) {
export function isJavaScriptChallenge({
challengeType
}: ChallengeMeta): boolean {
return (
challengeType === challengeTypes.js ||
challengeType === challengeTypes.jsProject
);
}
export function isLoopProtected(challengeMeta: ChallengeMeta) {
export function isLoopProtected(challengeMeta: ChallengeMeta): boolean {
return challengeMeta.superBlock !== 'coding-interview-prep';
}

View File

@@ -132,11 +132,15 @@ const createHeader = (id = mainPreviewId) => `
</script>
`;
type TestResult =
| { pass: boolean }
| { err: { message: string; stack?: string } };
export const runTestInTestFrame = async function (
document: Document,
test: string,
timeout: number
) {
): Promise<TestResult | undefined> {
const { contentDocument: frame } = document.getElementById(
testId
) as HTMLIFrameElement;
@@ -309,7 +313,7 @@ export const createMainPreviewFramer = (
document: Document,
proxyLogger: ProxyLogger,
frameTitle: string
) =>
): ((args: Context) => void) =>
createFramer(
document,
mainPreviewId,
@@ -322,7 +326,7 @@ export const createMainPreviewFramer = (
export const createProjectPreviewFramer = (
document: Document,
frameTitle: string
) =>
): ((args: Context) => void) =>
createFramer(
document,
projectPreviewId,
@@ -336,7 +340,8 @@ export const createTestFramer = (
document: Document,
proxyLogger: ProxyLogger,
frameReady: () => void
) => createFramer(document, testId, initTestFrame, proxyLogger, frameReady);
): ((args: Context) => void) =>
createFramer(document, testId, initTestFrame, proxyLogger, frameReady);
const createFramer = (
document: Document,

View File

@@ -35,7 +35,7 @@ export function transformEditorLink(url: string): string {
export function enhancePrismAccessibility(
prismEnv: Prism.hooks.ElementHighlightedEnvironment
) {
): void {
const langs: { [key: string]: string } = {
js: 'JavaScript',
javascript: 'JavaScript',

View File

@@ -136,6 +136,8 @@ function parseApiResponseToClientUser(data: ApiUser): UserResponse {
};
}
// TODO: this at least needs a few aliases so it's human readable
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function mapFilesToChallengeFiles<File, Rest>(
fileContainer: ({ files: (File & { key: string })[] } & Rest)[] = []
) {

View File

@@ -13,11 +13,25 @@ interface StandardizeRequestBodyArgs {
challengeType: number;
}
interface File {
contents: string;
ext: string;
history: string[];
key: string;
name: string;
}
interface Body {
id: string;
files?: File[];
challengeType: number;
}
export function standardizeRequestBody({
id,
challengeFiles = [],
challengeType
}: StandardizeRequestBodyArgs) {
}: StandardizeRequestBodyArgs): Body {
return {
id,
files: challengeFiles?.map(({ fileKey, contents, ext, name, history }) => {
@@ -33,12 +47,12 @@ export function standardizeRequestBody({
};
}
export function getStringSizeInBytes(str = '') {
export function getStringSizeInBytes(str = ''): number {
const stringSizeInBytes = new Blob([JSON.stringify(str)]).size;
return stringSizeInBytes;
}
export function bodySizeFits(bodySizeInBytes: number) {
export function bodySizeFits(bodySizeInBytes: number): boolean {
return bodySizeInBytes <= MAX_BODY_SIZE;
}

View File

@@ -1,4 +1,4 @@
import * as Sentry from '@sentry/gatsby';
import { captureException } from '@sentry/gatsby';
import envData from '../../../config/env.json';
const { sentryClientDSN } = envData;
@@ -11,5 +11,5 @@ export function reportClientSideError(
): string | void {
return sentryClientDSN === null
? console.error(`Client: ${message}`, e)
: Sentry.captureException(e);
: captureException(e);
}

View File

@@ -2,12 +2,20 @@ import type { CompletedChallenge } from '../redux/prop-types';
import { challengeTypes } from '../../utils/challenge-types';
import { maybeUrlRE } from '.';
// eslint-disable-next-line @typescript-eslint/naming-convention
type DisplayType =
| 'none'
| 'showMultifileProjectSolution'
| 'showUserCode'
| 'showProjectAndGithubLinks'
| 'showProjectLink';
export const getSolutionDisplayType = ({
solution,
githubLink,
challengeFiles,
challengeType
}: CompletedChallenge) => {
}: CompletedChallenge): DisplayType => {
if (challengeFiles?.length)
return challengeType === challengeTypes.multifileCertProject
? 'showMultifileProjectSolution'

View File

@@ -14,7 +14,7 @@ const superBlocksWithoutLastWord = [
SuperBlocks.TheOdinProject
];
export function getSuperBlockTitleForMap(superBlock: SuperBlocks) {
export function getSuperBlockTitleForMap(superBlock: SuperBlocks): string {
const i18nSuperBlock = i18next.t(`intro:${superBlock}.title`);
return superBlocksWithoutLastWord.includes(superBlock)

View File

@@ -80,8 +80,22 @@ export const paypalConfigTypes = {
}
};
type DonationAmount = 500 | 1000 | 2000 | 3000 | 4000 | 5000;
interface OneTimeConfig {
amount: DonationAmount;
duration: 'one-time';
planId: null;
}
interface SubscriptionConfig {
amount: DonationAmount;
duration: 'month';
planId: string;
}
export const paypalConfigurator = (
donationAmount: 500 | 1000 | 2000 | 3000 | 4000 | 5000,
donationAmount: DonationAmount,
donationDuration: 'one-time' | 'month',
paypalConfig: {
month: {
@@ -93,7 +107,7 @@ export const paypalConfigurator = (
5000: { planId: string };
};
}
) => {
): OneTimeConfig | SubscriptionConfig => {
if (donationDuration === 'one-time') {
return { amount: donationAmount, duration: donationDuration, planId: null };
}

View File

@@ -111,7 +111,7 @@ export const rtlLangs = ['arabic'];
// locale is sourced from a JSON file, so we use getLangCode to
// find the associated enum values
export function getLangCode(locale: PropertyKey) {
export function getLangCode(locale: PropertyKey): string {
if (isPropertyOf(LangCodes, locale)) return LangCodes[locale];
throw new Error(`${String(locale)} is not a valid locale`);
}

View File

@@ -576,11 +576,17 @@ function shouldShowSuperblocks({
return true;
}
type Config = {
language: string;
showNewCurriculum?: string;
showUpcomingChanges?: string;
};
export function getLearnSuperBlocks({
language = 'english',
showNewCurriculum = 'false',
showUpcomingChanges = 'false'
}) {
}: Config): SuperBlocks[] {
const learnSuperBlocks: SuperBlocks[] = [];
Object.values(TranslationStates).forEach(translationState => {
@@ -608,7 +614,7 @@ export function getAuditedSuperBlocks({
language = 'english',
showNewCurriculum = 'false',
showUpcomingChanges = 'false'
}) {
}: Config): SuperBlocks[] {
const auditedSuperBlocks: SuperBlocks[] = [];
Object.values(SuperBlockStates).forEach(superBlockState => {
@@ -634,7 +640,7 @@ export function getNotAuditedSuperBlocks({
language = 'english',
showNewCurriculum = 'false',
showUpcomingChanges = 'false'
}) {
}: Config): SuperBlocks[] {
const notAuditedSuperBlocks: SuperBlocks[] = [];
Object.values(SuperBlockStates).forEach(superBlockState => {

View File

@@ -1,7 +1,10 @@
import { Request, Response } from 'express';
import { getSteps } from '../utils/get-steps';
export const blockRoute = async (req: Request, res: Response) => {
export const blockRoute = async (
req: Request,
res: Response
): Promise<void> => {
const { superblock, block } = req.params;
const steps = await getSteps(superblock, block);

View File

@@ -1,6 +1,6 @@
import { Request, Response } from 'express';
import { superBlockList } from '../configs/super-block-list';
export const indexRoute = (req: Request, res: Response) => {
export const indexRoute = (req: Request, res: Response): void => {
res.json(superBlockList);
};

View File

@@ -1,7 +1,7 @@
import { Request, Response } from 'express';
import { saveStep } from '../utils/save-step';
export const saveRoute = async (req: Request, res: Response) => {
export const saveRoute = async (req: Request, res: Response): Promise<void> => {
const { superblock, block, step } = req.params;
const content = (req.body as { content: string }).content;

View File

@@ -1,7 +1,7 @@
import { Request, Response } from 'express';
import { getStepContent } from '../utils/get-step-contents';
export const stepRoute = async (req: Request, res: Response) => {
export const stepRoute = async (req: Request, res: Response): Promise<void> => {
const { superblock, block, step } = req.params;
const stepContents = await getStepContent(superblock, block, step);

View File

@@ -1,7 +1,10 @@
import { Request, Response } from 'express';
import { getBlocks } from '../utils/get-blocks';
export const superblockRoute = async (req: Request, res: Response) => {
export const superblockRoute = async (
req: Request,
res: Response
): Promise<void> => {
const sup = req.params.superblock;
const blocks = await getBlocks(sup);

View File

@@ -25,7 +25,10 @@ const toolsSwitch: ToolsSwitch = {
}
};
export const toolsRoute = async (req: Request, res: Response) => {
export const toolsRoute = async (
req: Request,
res: Response
): Promise<void> => {
const { superblock, block, command } = req.params;
const { num } = req.body as Record<string, number>;
const directory = join(

View File

@@ -4,7 +4,12 @@ import { CHALLENGE_DIR, META_DIR } from '../configs/paths';
import { PartialMeta } from '../interfaces/partial-meta';
export const getBlocks = async (sup: string) => {
type Block = {
name: string;
path: string;
};
export const getBlocks = async (sup: string): Promise<Block[]> => {
const filePath = join(CHALLENGE_DIR, sup);
const files = await readdir(filePath);

View File

@@ -7,7 +7,7 @@ export const getStepContent = async (
sup: string,
block: string,
step: string
) => {
): Promise<{ name: string; fileData: string }> => {
const filePath = join(CHALLENGE_DIR, sup, block, step);
const fileData = await readFile(filePath, 'utf8');

View File

@@ -10,7 +10,13 @@ const getFileOrder = (id: string, meta: PartialMeta) => {
return meta.challengeOrder.findIndex(([f]) => f === id);
};
export const getSteps = async (sup: string, block: string) => {
type Step = {
name: string;
id: string;
path: string;
};
export const getSteps = async (sup: string, block: string): Promise<Step[]> => {
const filePath = join(CHALLENGE_DIR, sup, block);
const metaPath = join(META_DIR, block, 'meta.json');

View File

@@ -7,7 +7,7 @@ export const saveStep = async (
block: string,
step: string,
content: string
) => {
): Promise<boolean> => {
try {
const filePath = join(CHALLENGE_DIR, sup, block, step);

View File

@@ -9,7 +9,7 @@ import {
updateStepTitles
} from './utils';
function deleteStep(stepNum: number) {
function deleteStep(stepNum: number): void {
if (stepNum < 1) {
throw 'Step not deleted. Step num must be a number greater than 0.';
}
@@ -28,7 +28,7 @@ function deleteStep(stepNum: number) {
console.log(`Sucessfully deleted step #${stepNum}`);
}
function insertStep(stepNum: number) {
function insertStep(stepNum: number): void {
if (stepNum < 1) {
throw 'Step not inserted. New step number must be greater than 0.';
}
@@ -56,7 +56,7 @@ function insertStep(stepNum: number) {
console.log(`Sucessfully inserted new step #${stepNum}`);
}
function createEmptySteps(num: number) {
function createEmptySteps(num: number): void {
if (num < 1 || num > 1000) {
throw `No steps created. arg 'num' must be between 1 and 1000 inclusive`;
}

View File

@@ -17,7 +17,7 @@ const createStepFile = ({
stepNum,
projectPath = getProjectPath(),
challengeSeeds = {}
}: Options) => {
}: Options): ObjectID => {
const challengeId = new ObjectID();
const template = getStepTemplate({
@@ -36,7 +36,7 @@ interface InsertOptions {
stepId: ObjectID;
}
function insertStepIntoMeta({ stepNum, stepId }: InsertOptions) {
function insertStepIntoMeta({ stepNum, stepId }: InsertOptions): void {
const existingMeta = getMetaData();
const oldOrder = [...existingMeta.challengeOrder];
oldOrder.splice(stepNum - 1, 0, [stepId.toString()]);
@@ -49,7 +49,7 @@ function insertStepIntoMeta({ stepNum, stepId }: InsertOptions) {
updateMetaData({ ...existingMeta, challengeOrder });
}
function deleteStepFromMeta({ stepNum }: { stepNum: number }) {
function deleteStepFromMeta({ stepNum }: { stepNum: number }): void {
const existingMeta = getMetaData();
const oldOrder = [...existingMeta.challengeOrder];
oldOrder.splice(stepNum - 1, 1);
@@ -62,7 +62,7 @@ function deleteStepFromMeta({ stepNum }: { stepNum: number }) {
updateMetaData({ ...existingMeta, challengeOrder });
}
const updateStepTitles = () => {
const updateStepTitles = (): void => {
const meta = getMetaData();
const fileNames: string[] = [];

View File

@@ -65,6 +65,6 @@ const schema = Joi.object().keys({
export const trendingSchemaValidator = (
trendingObj: Record<string, string>
) => {
): Joi.ValidationResult => {
return schema.validate(trendingObj);
};

View File

@@ -6,6 +6,6 @@ const story = {
component: AllPalettes
};
export const ColorSystem = () => <AllPalettes />;
export const ColorSystem = (): JSX.Element => <AllPalettes />;
export default story;

View File

@@ -53,7 +53,7 @@ const Palette = ({ colors }: PaletteProps) => {
);
};
export const AllPalettes = () => {
export const AllPalettes = (): JSX.Element => {
return (
<>
<Palette colors={getPaletteByColorName('gray')} />

View File

@@ -6,7 +6,7 @@ export const FormControlFeedback = ({
children,
className,
testId
}: FormControlVariationProps) => {
}: FormControlVariationProps): JSX.Element => {
const defaultClasses =
'absolute top-0 right-0 z-2 block w-8 h-8 leading-8 ' +
'text-center pointer-events-none text-green-700';

View File

@@ -6,7 +6,7 @@ export const FormControlStatic = ({
className,
children,
testId
}: FormControlVariationProps) => {
}: FormControlVariationProps): JSX.Element => {
const defaultClasses = 'py-1.5 mb-0 min-h-43-px text-foreground-secondary';
const classes = [defaultClasses, className].join(' ');

View File

@@ -8,7 +8,7 @@ if (!name) {
throw new Error('You must include a component name.');
}
if (!name.match(/^[A-Z]/)) {
if (!/^[A-Z]/.exec(name)) {
throw new Error('Component name must be in PascalCase.');
}

View File

@@ -1,23 +1,23 @@
// component.tsx
export const component = (name: string) => `
export const component = (name: string): string => `
import React from 'react';
import { ${name}Props } from './types';
export const ${name} = ({}: ${name}Props) => {
return <div>Hello, I am a ${name} component</div>;
};
`;
// types.ts
export const type = (name: string) => `
export const type = (name: string): string => `
export interface ${name}Props {
className?: string
}
`;
// component.test.tsx
export const test = (name: string) => `
export const test = (name: string): string => `
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
@@ -30,7 +30,7 @@ describe('<${name} />', () => {
`;
// component.stories.tsx
export const story = (name: string) => `
export const story = (name: string): string => `
import React from 'react';
import { Story } from '@storybook/react';
import { ${name}, ${name}Props } from '.';
@@ -53,7 +53,7 @@ export default story;
`;
// index.ts
export const barrel = (name: string, kebabCasedName: string) => `
export const barrel = (name: string, kebabCasedName: string): string => `
export { ${name} } from './${kebabCasedName}';
export type { ${name}Props } from './types';
`;

View File

@@ -1,4 +1,4 @@
export function getLines(contents: string, range?: number[]) {
export function getLines(contents: string, range?: number[]): string {
if (!range) {
return '';
}

View File

@@ -44,6 +44,8 @@ for (const [id, title] of idToTitle) {
}
}
export const getCertIds = () => idToPath.keys();
export const getPathFromID = (id: string) => idToPath.get(id);
export const getTitleFromId = (id: string) => idToTitle.get(id);
export const getCertIds = (): IterableIterator<string> => idToPath.keys();
export const getPathFromID = (id: string): string | undefined =>
idToPath.get(id);
export const getTitleFromId = (id: string): string | undefined =>
idToTitle.get(id);

View File

@@ -1,21 +1,36 @@
export const invalidCharError = {
type Valid = {
valid: true;
error: null;
};
type Invalid = {
valid: false;
error: string;
};
type Validated = Valid | Invalid;
export const invalidCharError: Invalid = {
valid: false,
error: 'contains invalid characters'
};
export const validationSuccess = { valid: true, error: null };
export const usernameTooShort = { valid: false, error: 'is too short' };
export const usernameIsHttpStatusCode = {
export const validationSuccess: Valid = { valid: true, error: null };
export const usernameTooShort: Invalid = {
valid: false,
error: 'is too short'
};
export const usernameIsHttpStatusCode: Invalid = {
valid: false,
error: 'is a reserved error code'
};
const validCharsRE = /^[a-zA-Z0-9\-_+]*$/;
export const isHttpStatusCode = (str: string) => {
export const isHttpStatusCode = (str: string): boolean => {
const output = parseInt(str, 10);
return !isNaN(output) && output >= 100 && output <= 599;
};
export const isValidUsername = (str: string) => {
export const isValidUsername = (str: string): Validated => {
if (!validCharsRE.test(str)) return invalidCharError;
if (str.length < 3) return usernameTooShort;
if (isHttpStatusCode(str)) return usernameIsHttpStatusCode;