chore: upgrade eslint (#58575)

This commit is contained in:
Oliver Eyton-Williams
2025-02-07 21:48:43 +01:00
committed by GitHub
parent 54b028b10d
commit 6e9513a933
75 changed files with 2272 additions and 885 deletions

View File

@@ -1,12 +0,0 @@
client/static/**
client/.cache/**
client/public/**
api-server/src/public/**
api-server/lib/**
shared/config/i18n.js
shared/config/certification-settings.js
shared/config/donation-settings.js
shared/config/superblocks.js
docs/**/*.md
playwright*.config.ts
playwright/**

View File

@@ -1,134 +0,0 @@
{
"env": {
"es6": true,
"browser": true,
"mocha": true,
"node": true,
"jest": true
},
"parser": "@babel/eslint-parser",
"parserOptions": {
"babelOptions": {
"presets": ["@babel/preset-react"]
}
},
"root": true,
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:import/recommended",
"plugin:jsx-a11y/recommended",
"prettier"
],
"plugins": ["no-only-tests", "filenames-simple"],
"globals": {
"Promise": true,
"window": true,
"$": true,
"ga": true,
"jQuery": true,
"router": true
},
"settings": {
"react": {
"version": "16.4.2"
},
"import/resolver": {
"typescript": true,
"node": true
}
},
"rules": {
"import/no-unresolved": [2, { "commonjs": true }],
"import/named": "error",
"import/no-named-as-default": "off",
"import/no-named-as-default-member": "off",
"import/order": "error",
"import/no-cycle": [2, { "maxDepth": 2 }],
"react/prop-types": "off",
"no-only-tests/no-only-tests": "error",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
],
"filenames-simple/naming-convention": ["warn"]
},
"overrides": [
{
"files": ["**/*.ts?(x)"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": [
"./client/tsconfig.json",
"./tsconfig.json",
"./api/tsconfig.json",
"./shared/tsconfig.json",
"./tools/client-plugins/browser-scripts/tsconfig.json",
"./e2e/tsconfig.json"
]
},
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:import/typescript"
],
"plugins": ["@typescript-eslint"],
"rules": {
"import/no-unresolved": "off",
"import/named": 0,
"@typescript-eslint/naming-convention": "off"
}
},
{
"files": ["./client/**/*.test.[jt]s?(x)"],
"extends": [
"plugin:testing-library/react",
"plugin:jest-dom/recommended"
],
"rules": { "import/named": 2 }
},
{
"files": ["e2e/*.ts"],
"rules": {
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-assignment": "off"
}
},
{
"files": ["**/api-server/**/*", "**/404.*"],
"rules": {
"filenames-simple/naming-convention": "off"
}
},
{
"files": ["**/api/src/**/*.ts"],
"plugins": ["jsdoc"],
"extends": ["plugin:jsdoc/recommended-typescript-error"],
"rules": {
"jsdoc/require-jsdoc": [
"error",
{
"require": {
"ArrowFunctionExpression": true,
"ClassDeclaration": true,
"ClassExpression": true,
"FunctionDeclaration": true,
"FunctionExpression": true,
"MethodDefinition": true
},
"publicOnly": true
}
],
"jsdoc/require-description-complete-sentence": "warn",
"jsdoc/tag-lines": "off"
}
}
]
}

View File

@@ -57,7 +57,6 @@ describe('boot/challenge', () => {
expect(result).toHaveProperty('updateData.$push.completedChallenges');
});
// eslint-disable-next-line max-len
it('preserves file contents if the completed challenge is a JS Project', () => {
const jsChallengeId = 'aa2e6f85cab2ab736c9a9b24';
const completedChallenge = {
@@ -102,7 +101,6 @@ describe('boot/challenge', () => {
);
});
// eslint-disable-next-line max-len
it('does not attempt to update progressTimestamps for a previously completed challenge', () => {
const completedChallengeId = 'aaa48de84e1ecc7c742e1124';
const completedChallenge = {
@@ -122,7 +120,6 @@ describe('boot/challenge', () => {
expect(hasProgressTimestamps).toBe(false);
});
// eslint-disable-next-line max-len
it('provides a progressTimestamps update for new challenge completion', () => {
expect.assertions(2);
const { updateData } = buildUserUpdate(
@@ -238,7 +235,6 @@ describe('boot/challenge', () => {
});
}, 10000);
// eslint-disable-next-line max-len
it('returns the first challenge url if the provided id does not relate to a challenge', async () => {
const challengeUrlResolver = await createChallengeUrlResolver(
mockAllChallenges,
@@ -403,7 +399,6 @@ describe('boot/challenge', () => {
expect(res.redirect).toHaveBeenCalledWith(mockLearnUrl);
});
// eslint-disable-next-line max-len
it('redirects to the url provided by the challengeUrlResolver', async () => {
const challengeUrlResolver = await createChallengeUrlResolver(
mockAllChallenges,
@@ -427,7 +422,6 @@ describe('boot/challenge', () => {
expect(res.redirect).toHaveBeenCalledWith(expectedUrl);
});
// eslint-disable-next-line max-len
it('redirects to the first challenge for users without a currentChallengeId', async () => {
const challengeUrlResolver = await createChallengeUrlResolver(
mockAllChallenges,

View File

@@ -113,9 +113,7 @@ export const mockCompletedChallenges = [
completedDate: 1541678430790.0,
files: [
{
contents:
// eslint-disable-next-line max-len
"function palindrome(str) {\n const clean = str.replace(/[\\W_]/g, '').toLowerCase()\n const revStr = clean.split('').reverse().join('');\n return clean === revStr;\n}\n\n\n\npalindrome(\"eye\");\n",
contents: "function palindrome(str) {\n const clean = str.replace(/[\\W_]/g, '').toLowerCase()\n const revStr = clean.split('').reverse().join('');\n return clean === revStr;\n}\n\n\n\npalindrome(\"eye\");\n",
ext: 'js',
path: 'index.js',
name: 'index',

View File

@@ -77,7 +77,6 @@ app.start = _.once(function () {
log('DB connection closed');
// exit process
// this may close kept alive sockets
// eslint-disable-next-line no-process-exit
process.exit(0);
});
});

View File

@@ -13,7 +13,6 @@ export default function getCsurf() {
return function csrf(req, res, next) {
const { path } = req;
if (
// eslint-disable-next-line max-len
/^\/donate\/charge-stripe$|^\/donate\/create-stripe-payment-intent$|^\/coderoad-challenge-completed$/.test(
path
)

View File

@@ -43,7 +43,7 @@ describe('getSetAccessToken', () => {
describe('cookies', () => {
it('returns `invalid token` error for malformed tokens', () => {
const invalidJWT = jwt.sign({ accessToken }, invalidJWTSecret);
// eslint-disable-next-line camelcase
const req = mockReq({ cookie: { jwt_access_token: invalidJWT } });
const result = getAccessTokenFromRequest(req, validJWTSecret);
@@ -55,7 +55,7 @@ describe('getSetAccessToken', () => {
{ accessToken: { ...accessToken, created: theBeginningOfTime } },
validJWTSecret
);
// eslint-disable-next-line camelcase
const req = mockReq({ cookie: { jwt_access_token: invalidJWT } });
const result = getAccessTokenFromRequest(req, validJWTSecret);
@@ -65,7 +65,7 @@ describe('getSetAccessToken', () => {
it('returns a valid access token with no errors ', () => {
expect.assertions(2);
const validJWT = jwt.sign({ accessToken }, validJWTSecret);
// eslint-disable-next-line camelcase
const req = mockReq({ cookie: { jwt_access_token: validJWT } });
const result = getAccessTokenFromRequest(req, validJWTSecret);
@@ -101,7 +101,6 @@ describe('getSetAccessToken', () => {
});
describe('removeCookies', () => {
// eslint-disable-next-line max-len
it('removes four cookies set in the lifetime of an authenticated session', () => {
// expect.assertions(4);
const req = mockReq();

View File

@@ -5,7 +5,7 @@ const args = process.argv.slice(2);
const ENV_EXAM_ID = args[0];
if (!ENV_EXAM_ID) {
throw 'First argument must be the EnvExam _id';
throw Error('First argument must be the EnvExam _id');
}
const prisma = new PrismaClient({

View File

@@ -7,10 +7,10 @@ const ENV_EXAM_ID = args[0];
const NUMBER_OF_EXAMS_TO_GENERATE = Number(args[1]);
if (!ENV_EXAM_ID) {
throw 'First argument must be the EnvExam _id';
throw Error('First argument must be the EnvExam _id');
}
if (!NUMBER_OF_EXAMS_TO_GENERATE) {
throw 'Second argument must be an unsigned integer';
throw Error('Second argument must be an unsigned integer');
}
const prisma = new PrismaClient({

View File

@@ -19,7 +19,7 @@ async function main() {
console.info('Connected.');
if (!EXAM_JSON_PATH) {
throw 'First argument must be the file path to the exam';
throw Error('First argument must be the file path to the exam');
}
const exam_str = await readFile(EXAM_JSON_PATH, 'utf-8');

View File

@@ -1,3 +1,5 @@
// TODO: enable this, since strings don't make good errors.
/* eslint-disable @typescript-eslint/only-throw-error */
/* eslint-disable jsdoc/require-returns, jsdoc/require-param */
import {
EnvAnswer,
@@ -292,6 +294,7 @@ export function generateExam(exam: EnvExam): Omit<EnvGeneratedExam, 'id'> {
(acc, curr) => {
// If type is already in accumulator, add to it.
const typeIndex = acc.findIndex(a => a[0]?.type === curr.type);
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
acc[typeIndex]?.push(curr) ?? acc.push([curr]);
return acc;
},

View File

@@ -1,5 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import Fastify, { FastifyInstance } from 'fastify';
import { defaultUserEmail } from '../../jest.utils';

View File

@@ -180,7 +180,7 @@ describe('auth0 plugin', () => {
getAccessTokenFromAuthorizationCodeFlowSpy.mockResolvedValueOnce({
token: 'any token'
});
userinfoSpy.mockResolvedValueOnce(Promise.reject('any error'));
userinfoSpy.mockResolvedValueOnce(Promise.reject(Error('any error')));
const res = await fastify.inject({
method: 'GET',

View File

@@ -57,7 +57,7 @@ const shadowCapture: FastifyPluginCallback = (fastify, _options, done) => {
done();
};
/* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
function captureRequest(req: FastifyRequest) {
const savedRequest = {
// @ts-expect-error Exists

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import {
devLogin,

View File

@@ -127,7 +127,6 @@ describe('auth0 routes', () => {
const res = await superRequest('/mobile-login', {
method: 'GET',
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
setCookies: firstRes.get('Set-Cookie')
})
.set('Authorization', 'Bearer does-not-matter')

View File

@@ -102,6 +102,7 @@ export const chargeStripeRoute: FastifyPluginCallbackTypebox = (
const subscription =
await stripe.subscriptions.retrieve(subscriptionId);
const isSubscriptionActive = subscription.status === 'active';
// eslint-disable-next-line @typescript-eslint/no-base-to-string
const productId = subscription.items.data[0]?.plan.product?.toString();
const isStartedRecently = inLastFiveMinutes(
subscription.current_period_start

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox';
import * as schemas from '../../schemas';
import { getRedirectParams } from '../../utils/redirection';

View File

@@ -1,4 +1,3 @@
/* eslint-disable jsdoc/require-returns, jsdoc/require-param */
import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox';
import { type FastifyInstance, type FastifyReply } from 'fastify';
@@ -29,9 +28,6 @@ export const sentryRoutes: FastifyPluginCallbackTypebox = (
done();
};
/**
* Creates a new event in Sentry.
*/
function postSentryEventHandler(
this: FastifyInstance,
req: UpdateReqType<typeof schemas.sentryPostEvent>,

View File

@@ -1,6 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import type { Prisma } from '@prisma/client';
import { ObjectId } from 'mongodb';
import _ from 'lodash';

View File

@@ -105,7 +105,7 @@ function getParamsFromUrl(
let returnUrl;
try {
returnUrl = new URL(url ? url : HOME_LOCATION);
} catch (e) {
} catch (_e) {
returnUrl = new URL(HOME_LOCATION);
}

View File

@@ -1,4 +1,3 @@
/* eslint-disable camelcase */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
@@ -112,7 +111,6 @@ export default class PayPalButtonScriptLoader extends Component<
prevProps.style.height !== this.props.style.height ||
prevProps.isMinimalForm !== this.props.isMinimalForm
) {
// eslint-disable-next-line react/no-did-update-set-state
this.setState({ isSdkLoaded: false });
this.loadScript(this.state.isSubscription, true);
}

View File

@@ -25,7 +25,7 @@ function useIsInViewport(ref: React.RefObject<HTMLDivElement>) {
);
useEffect(() => {
ref.current && observer.observe(ref.current);
if (ref.current) observer.observe(ref.current);
return () => {
observer.disconnect();
};

View File

@@ -72,7 +72,7 @@ const GrowthBookWrapper = ({
});
}
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);

View File

@@ -1,4 +1,3 @@
// eslint-disable-next-line filenames-simple/naming-convention
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Spacer } from '@freecodecamp/ui';

View File

@@ -1,9 +1,17 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import CalendarHeatMap from '@freecodecamp/react-calendar-heatmap';
// TODO: Check if we can import { addDays, addMonths ... } from 'date-fns'
// without bundling all of the package then we can remove the disable-next-line
// comments.
// eslint-disable-next-line import/no-duplicates
import addDays from 'date-fns/addDays';
// eslint-disable-next-line import/no-duplicates
import addMonths from 'date-fns/addMonths';
// eslint-disable-next-line import/no-duplicates
import isEqual from 'date-fns/isEqual';
// eslint-disable-next-line import/no-duplicates
import startOfDay from 'date-fns/startOfDay';
import React, { Component } from 'react';
import type { TFunction } from 'i18next';
@@ -21,7 +29,6 @@ import { getLangCode } from '../../../../../shared/config/i18n';
import { User } from '../../../redux/prop-types';
import FullWidthRow from '../../helpers/full-width-row';
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { clientLocale } = envData;
const localeCode = getLangCode(clientLocale);

View File

@@ -1,9 +1,17 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
// TODO: Check if we can import { addDays, addMonths ... } from 'date-fns'
// without bundling all of the package then we can remove the disable-next-line
// comments.
// eslint-disable-next-line import/no-duplicates
import addDays from 'date-fns/addDays';
// eslint-disable-next-line import/no-duplicates
import addMonths from 'date-fns/addMonths';
// eslint-disable-next-line import/no-duplicates
import isEqual from 'date-fns/isEqual';
import { Spacer } from '@freecodecamp/ui';
// eslint-disable-next-line import/no-duplicates
import startOfDay from 'date-fns/startOfDay';
import { User } from '../../../redux/prop-types';
import { FullWidthRow } from '../../helpers';

View File

@@ -240,7 +240,7 @@ function TimelineInner({
);
}
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call*/
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call*/
function useIdToNameMap(t: TFunction): Map<string, NameMap> {
const {
allChallengeNode: { edges }

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import React from 'react';
import renderer from 'react-test-renderer';
import { Provider } from 'react-redux';

View File

@@ -72,7 +72,6 @@ const userProps = {
isCollegeAlgebraPyCertV8: true,
isFoundationalCSharpVertV8: true
},
// eslint-disable-next-line @typescript-eslint/no-empty-function
navigate: () => {}
};

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import { render, screen } from '@testing-library/react';
import React from 'react';
import { Provider } from 'react-redux';

View File

@@ -24,7 +24,7 @@ function ExamToken(): JSX.Element {
} = response;
setExamToken(examEnvironmentAuthorizationToken);
setExamTokenError('');
} catch (e) {
} catch (_e) {
setExamTokenError(t('exam-token.error'));
}

View File

@@ -2,10 +2,8 @@ import { Router } from '@reach/router';
import { withPrefix } from 'gatsby';
import React from 'react';
/* eslint-disable max-len */
import ShowProfileOrFourOhFour from '../client-only-routes/show-profile-or-four-oh-four';
import FourOhFour from '../components/FourOhFour';
/* eslint-enable max-len */
function FourOhFourPage(): JSX.Element {
return (

View File

@@ -58,7 +58,6 @@ function DonatePage({
event: 'donation_view',
action: `Displayed Donate Page`
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return showLoading ? (

View File

@@ -233,7 +233,6 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
} else if (!isAdvancing && !showPreviewPane && !showPreviewPortal) {
togglePane('showPreviewPane');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const challengeFile = getChallengeFile();

View File

@@ -635,9 +635,11 @@ const Editor = (props: EditorProps): JSX.Element => {
const setAriaRoledescription = (value: boolean) => {
const textareas = document.querySelectorAll('.monaco-editor textarea');
textareas.forEach(textarea => {
value
? textarea.setAttribute('aria-roledescription', 'editor')
: textarea.removeAttribute('aria-roledescription');
if (value) {
textarea.setAttribute('aria-roledescription', 'editor');
} else {
textarea.removeAttribute('aria-roledescription');
}
});
store.set('ariaRoledescription', value);
};
@@ -699,7 +701,7 @@ const Editor = (props: EditorProps): JSX.Element => {
dataRef.current.descriptionZoneTop =
editor.getTopForLineNumber(getLineBeforeEditableRegion() + 1) -
domNode.offsetHeight;
dataRef.current.descriptionWidget &&
if (dataRef.current.descriptionWidget)
editor.layoutContentWidget(dataRef.current.descriptionWidget);
}
};
@@ -1247,7 +1249,6 @@ const Editor = (props: EditorProps): JSX.Element => {
}
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.tests]);
useEffect(() => {
@@ -1258,7 +1259,6 @@ const Editor = (props: EditorProps): JSX.Element => {
if (!isTabTrapped()) {
setMonacoTabTrapped(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.dimensions]);
function updateDescriptionZone() {

View File

@@ -1,5 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import React, { Component } from 'react';
import type { TFunction } from 'i18next';
import { withTranslation } from 'react-i18next';

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { parseBlanks } from './parse-blanks';
describe('parseBlanks', () => {

View File

@@ -13,8 +13,7 @@ interface ExitQuizModalProps {
}
const mapStateToProps = (state: unknown) => ({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
isExitQuizModalOpen: isExitQuizModalOpenSelector(state)
isExitQuizModalOpen: isExitQuizModalOpenSelector(state) as boolean
});
const mapDispatchToProps = {

View File

@@ -13,8 +13,7 @@ interface FinishQuizModalProps {
}
const mapStateToProps = (state: unknown) => ({
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
isFinishQuizModalOpen: isFinishQuizModalOpenSelector(state)
isFinishQuizModalOpen: isFinishQuizModalOpenSelector(state) as boolean
});
const mapDispatchToProps = {

View File

@@ -178,7 +178,8 @@ const ShowQuiz = ({
},
passingGrade: 90,
onSuccess: () => {
openCompletionModal(), setIsPassed(true);
openCompletionModal();
setIsPassed(true);
},
onFailure: () => setIsPassed(false)
});

View File

@@ -50,11 +50,9 @@ let presetsJS, presetsJSX;
async function loadBabel() {
if (Babel) return;
/* eslint-disable no-inline-comments */
Babel = await import(
/* webpackChunkName: "@babel/standalone" */ '@babel/standalone'
);
/* eslint-enable no-inline-comments */
Babel.registerPlugin(
'loopProtection',
protect(protectTimeout, loopProtectCB, loopsPerTimeoutCheck)
@@ -66,12 +64,10 @@ async function loadBabel() {
}
async function loadPresetEnv() {
/* eslint-disable no-inline-comments */
if (!presetEnv)
presetEnv = await import(
/* webpackChunkName: "@babel/preset-env" */ '@babel/preset-env'
);
/* eslint-enable no-inline-comments */
presetsJS = {
presets: [presetEnv]
@@ -79,7 +75,6 @@ async function loadPresetEnv() {
}
async function loadPresetReact() {
/* eslint-disable no-inline-comments */
if (!presetReact)
presetReact = await import(
/* webpackChunkName: "@babel/preset-react" */ '@babel/preset-react'
@@ -88,7 +83,7 @@ async function loadPresetReact() {
presetEnv = await import(
/* webpackChunkName: "@babel/preset-env" */ '@babel/preset-env'
);
/* eslint-enable no-inline-comments */
presetsJSX = {
presets: [presetEnv, presetReact]
};

View File

@@ -306,7 +306,6 @@ export function* previewChallengeSaga(action) {
} catch (err) {
if (err[0] === 'timeout') {
// TODO: translate the error
// eslint-disable-next-line no-ex-assign
err[0] = `The code you have written is taking longer than the ${previewTimeout}ms our challenges allow. You may have created an infinite loop or need to write a more efficient algorithm`;
}
// If the preview fails, the most useful thing to do is to show the learner

View File

@@ -115,8 +115,6 @@ export function canBuildChallenge(challengeData: BuildChallengeData): boolean {
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
@@ -139,8 +137,7 @@ const testRunners = {
[challengeTypes.multifilePythonCertProject]: getPyTestRunner,
[challengeTypes.lab]: 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,

View File

@@ -161,6 +161,7 @@ export const runTestInTestFrame = async function (
return await Promise.race([
new Promise<
{ pass: boolean } | { err: { message: string; stack?: string } }
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
>((_, reject) => setTimeout(() => reject('timeout'), timeout)),
contentDocument.__runTest(test)
]);
@@ -201,7 +202,6 @@ const mountFrame =
const oldFrame = document.getElementById(element.id) as HTMLIFrameElement;
if (oldFrame) {
element.className = oldFrame.className || hiddenFrameClassName;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
oldFrame.parentNode!.replaceChild(element, oldFrame);
// only test frames can be added (and hidden) here, other frames must be
// added by react
@@ -358,6 +358,7 @@ const initPreviewFrame = () => (frameContext: Context) => frameContext;
const waitForFrame = (frameContext: Context) => {
return new Promise((resolve, reject) => {
if (!frameContext.document) {
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
reject(DOCUMENT_NOT_FOUND_ERROR);
} else if (frameContext.document.readyState === 'loading') {
frameContext.document.addEventListener('DOMContentLoaded', resolve);

View File

@@ -143,7 +143,6 @@ 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

@@ -32,15 +32,7 @@ describe('format', () => {
);
});
it('handles all primitive values', () => {
const primitives = [
'str',
57,
true,
false,
null,
// eslint-disable-next-line no-undefined
undefined
];
const primitives = ['str', 57, true, false, null, undefined];
expect(format(primitives)).toBe(
`[ 'str', 57, true, false, null, undefined ]`
);

View File

@@ -112,11 +112,11 @@ if (FREECODECAMP_NODE_ENV !== 'development') {
checkClientLocale();
checkCurriculumLocale();
if (fs.existsSync(`${configPath}/env.json`)) {
/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-assignment */
const { showUpcomingChanges } = require(`${configPath}/env.json`);
/* eslint-enable @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-assignment */
const { showUpcomingChanges } = JSON.parse(
fs.readFileSync(`${configPath}/env.json`, 'utf-8')
) as { showUpcomingChanges: boolean };
if (env['showUpcomingChanges'] !== showUpcomingChanges) {
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
console.log('Feature flags have been changed, cleaning client cache.');
const child = spawn('pnpm', ['run', '-w', 'clean:client']);
child.stdout.setEncoding('utf8');

View File

@@ -81,7 +81,7 @@ export const generateSearchPlaceholder = async (
)
});
}
} catch (err) {
} catch (_err) {
if (environment === 'production') {
console.warn(`
----------------------------------------------------------

View File

@@ -8,11 +8,10 @@ const readDirP = require('readdirp');
const { curriculum: curriculumLangs } =
require('../shared/config/i18n').availableLangs;
const { parseMD } = require('../tools/challenge-parser/parser');
/* eslint-disable max-len */
const {
translateCommentsInChallenge
} = require('../tools/challenge-parser/translation-parser');
/* eslint-enable max-len*/
const { isAuditedSuperBlock } = require('../shared/utils/is-audited');
const { createPoly } = require('../shared/utils/polyvinyl');
@@ -267,7 +266,6 @@ async function buildChallenges({ path: filePath }, curriculum, lang) {
}
} catch (e) {
console.log(`failed to create superBlock from ${superBlockDir}`);
// eslint-disable-next-line no-process-exit
process.exit(1);
}
const { meta } = challengeBlock;

View File

@@ -1,6 +1,3 @@
// utils are not typed (yet), so we have to disable some checks
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import fs from 'fs';
import path from 'path';
import { config } from 'dotenv';

View File

@@ -177,7 +177,6 @@ test.describe('Profile component', () => {
test('should not show portfolio when empty', async ({ page }) => {
// @certifieduser doesn't have portfolio information
await expect(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
page.getByText(translations.profile.projects)
).not.toBeVisible();
});

220
eslint.config.mjs Normal file
View File

@@ -0,0 +1,220 @@
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import { fixupConfigRules, fixupPluginRules } from '@eslint/compat';
import noOnlyTests from 'eslint-plugin-no-only-tests';
import filenamesSimple from 'eslint-plugin-filenames-simple';
import globals from 'globals';
import babelParser from '@babel/eslint-parser';
import importPlugin from 'eslint-plugin-import';
import jsxAllyPlugin from 'eslint-plugin-jsx-a11y';
import prettierConfig from 'eslint-config-prettier';
import reactPlugin from 'eslint-plugin-react';
import testingLibraryPlugin from 'eslint-plugin-testing-library';
import jestDomPlugin from 'eslint-plugin-jest-dom';
import tsParser from '@typescript-eslint/parser';
import tseslint from 'typescript-eslint';
import jsdoc from 'eslint-plugin-jsdoc';
import js from '@eslint/js';
import { FlatCompat } from '@eslint/eslintrc';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
});
export default tseslint.config(
{
ignores: [
'client/static/**/*',
'client/.cache/**/*',
'client/public/**/*',
'api-server/**/*',
'shared/**/*.js',
'docs/**/*.md',
'**/playwright*.config.ts',
'playwright/**/*'
]
},
js.configs.recommended,
reactPlugin.configs['flat'].recommended,
jsxAllyPlugin.flatConfigs.recommended,
...fixupConfigRules(
compat.extends(
'plugin:react-hooks/recommended' // Note: at time of testing, upgrading to v5 creates false positives
)
),
importPlugin.flatConfigs.recommended,
// TODO: consider adding eslint-plugin-n ():
// ...nodePlugin.configs["flat/mixed-esm-and-cjs"],
prettierConfig,
{
plugins: {
'no-only-tests': noOnlyTests,
'filenames-simple': fixupPluginRules(filenamesSimple)
},
languageOptions: {
globals: {
...globals.browser,
...globals.mocha,
...globals.node,
...globals.jest,
Promise: true,
window: true,
$: true,
ga: true,
jQuery: true,
router: true
},
parser: babelParser,
parserOptions: {
babelOptions: {
presets: ['@babel/preset-react']
}
}
},
settings: {
react: {
version: '16.4.2'
},
'import/resolver': {
typescript: true,
node: true
}
},
// TODO: audit these rules and remove as many as possible, ideally we want
// to rely on recommended configs.
rules: {
'import/no-unresolved': [
2,
{
commonjs: true
}
],
'import/named': 'error',
'import/no-named-as-default': 'off',
'import/no-named-as-default-member': 'off',
'import/order': 'error',
'import/no-cycle': [
2,
{
maxDepth: 2
}
],
'react/prop-types': 'off',
'no-only-tests/no-only-tests': 'error',
'no-unused-vars': 'off',
'no-unused-expressions': 'error', // This is so the js rules are more in line with the ts rules
'filenames-simple/naming-convention': ['warn']
}
},
{
files: ['**/*.ts?(x)'],
extends: [
tseslint.configs.recommended,
// TODO: turn on type-aware rules
// tseslint.configs.recommendedTypeChecked,
importPlugin.flatConfigs['typescript']
],
languageOptions: {
parser: tsParser,
parserOptions: {
projectService: true
}
},
rules: {
'import/no-unresolved': 'off',
'@typescript-eslint/naming-convention': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_'
}
]
}
},
{
files: [
'client/**/*.ts?(x)',
'api/**/*.ts',
'shared/**/*.ts',
'tools/client-plugins/**/*.ts',
'tools/scripts/**/*.ts',
'tools/challenge-helper-scripts/**/*.ts',
'tools/challenge-auditor/**/*.ts',
'e2e/**/*.ts'
],
extends: [tseslint.configs.recommendedTypeChecked]
},
{
files: ['client/**/*.test.[jt]s?(x)'],
extends: [
testingLibraryPlugin.configs['flat/react'],
jestDomPlugin.configs['flat/recommended']
],
rules: {
'import/named': 2
}
},
{
files: ['e2e/*.ts'],
rules: {
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off'
}
},
{
files: ['.lintstagedrc.js', '**/.babelrc.js', '**/404.tsx'],
rules: {
'filenames-simple/naming-convention': 'off'
}
},
{
extends: [
jsdoc.configs['flat/recommended-typescript-error'],
tseslint.configs.recommendedTypeChecked
],
files: ['**/api/src/**/*.ts'],
rules: {
'jsdoc/require-jsdoc': [
'error',
{
require: {
ArrowFunctionExpression: true,
ClassDeclaration: true,
ClassExpression: true,
FunctionDeclaration: true,
FunctionExpression: true,
MethodDefinition: true
},
publicOnly: true
}
],
'jsdoc/require-description-complete-sentence': 'error',
'jsdoc/tag-lines': 'off'
}
}
);

View File

@@ -91,8 +91,11 @@
"dotenv": "16.4.5"
},
"devDependencies": {
"@babel/eslint-parser": "7.23.3",
"@babel/preset-react": "7.23.3",
"@babel/eslint-parser": "7.26.5",
"@babel/preset-react": "7.26.3",
"@eslint/compat": "^1.2.6",
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.19.0",
"@playwright/test": "^1.47.1",
"@testing-library/dom": "9.3.4",
"@testing-library/jest-dom": "5.17.0",
@@ -101,20 +104,21 @@
"@types/node": "20.12.8",
"@types/testing-library__jest-dom": "^5.14.5",
"@typescript-eslint/eslint-plugin": "7.1.1",
"@typescript-eslint/parser": "7.1.1",
"@typescript-eslint/parser": "8.23.0",
"babel-jest": "29.7.0",
"eslint": "8.57.0",
"eslint-config-prettier": "9.1.0",
"eslint": "9.19.0",
"eslint-config-prettier": "10.0.1",
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-filenames-simple": "0.9.0",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-jest-dom": "5.1.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-jest-dom": "5.5.0",
"eslint-plugin-jsdoc": "48.2.1",
"eslint-plugin-jsx-a11y": "6.7.1",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-no-only-tests": "3.1.0",
"eslint-plugin-react": "7.33.2",
"eslint-plugin-react": "7.37.4",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-testing-library": "6.2.0",
"eslint-plugin-testing-library": "7.1.1",
"globals": "^15.14.0",
"husky": "9.0.11",
"identity-obj-proxy": "^3.0.0",
"jest": "29.7.0",
@@ -129,6 +133,7 @@
"stylelint": "16.14.1",
"ts-node": "10.9.2",
"typescript": "5.4.5",
"typescript-eslint": "^8.23.0",
"webpack-bundle-analyzer": "4.10.1",
"yargs": "17.7.2"
},

2533
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -142,7 +142,7 @@ void (async () => {
actionShouldFail = true;
}
}
actionShouldFail ? process.exit(1) : process.exit(0);
process.exit(actionShouldFail ? 1 : 0);
})();
async function auditChallengeFiles(

View File

@@ -2,8 +2,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './app';
// TODO: Re-enable this when we upgrade React to version 18
// eslint-disable-next-line react/no-deprecated
ReactDOM.render(
<React.StrictMode>
<App />

View File

@@ -16,6 +16,5 @@
"noEmit": true,
"jsx": "react-jsx",
"types": ["node"]
},
"include": ["src"]
}
}

View File

@@ -14,13 +14,15 @@ import {
function deleteStep(stepNum: number): void {
if (stepNum < 1) {
throw 'Step not deleted. Step num must be a number greater than 0.';
throw Error('Step not deleted. Step num must be a number greater than 0.');
}
const challengeOrder = getMetaData().challengeOrder;
if (stepNum > challengeOrder.length)
throw `Step # ${stepNum} not deleted. Largest step number is ${challengeOrder.length}.`;
throw Error(
`Step # ${stepNum} not deleted. Largest step number is ${challengeOrder.length}.`
);
const stepId = challengeOrder[stepNum - 1].id;
@@ -33,14 +35,16 @@ function deleteStep(stepNum: number): void {
function insertStep(stepNum: number): void {
if (stepNum < 1) {
throw 'Step not inserted. New step number must be greater than 0.';
throw Error('Step not inserted. New step number must be greater than 0.');
}
const challengeOrder = getMetaData().challengeOrder;
if (stepNum > challengeOrder.length + 1)
throw `Step not inserted. New step number must be less than ${
challengeOrder.length + 2
}.`;
throw Error(
`Step not inserted. New step number must be less than ${
challengeOrder.length + 2
}.`
);
const challengeType = [SuperBlocks.SciCompPy].includes(
getMetaData().superBlock
)
@@ -67,7 +71,9 @@ function insertStep(stepNum: number): void {
function createEmptySteps(num: number): void {
if (num < 1 || num > 1000) {
throw `No steps created. arg 'num' must be between 1 and 1000 inclusive`;
throw Error(
`No steps created. arg 'num' must be between 1 and 1000 inclusive`
);
}
const nextStepNum = getMetaData().challengeOrder.length + 1;

View File

@@ -1,10 +1,10 @@
const isIntRE = /^\d+$/;
function getArgValue(argv: string[] = []): number {
if (argv.length !== 3) throw `only one argument allowed`;
if (argv.length !== 3) throw Error('only one argument allowed');
const value = argv[2];
const intValue = parseInt(value, 10);
if (!isIntRE.test(value) || !Number.isInteger(intValue))
throw `argument must be an integer`;
throw Error('argument must be an integer');
return intValue;
}

View File

@@ -12,7 +12,7 @@ export const getFileName = async (id: string): Promise<string | null> => {
let frontMatter = null;
try {
frontMatter = matter.read(`${path}${file}`);
} catch (err) {
} catch (_err) {
frontMatter = null;
}
if (String(frontMatter?.data.id) === id) {

View File

@@ -1,11 +1,11 @@
// Update given value with markers (labels)
const insertErms = (seedCode: string, erms: number[]): string => {
if (!erms || erms.length <= 1) {
throw `erms should be provided`;
throw Error('erms should be provided');
}
if (erms.length <= 1) {
throw `erms should contain 2 elements`;
throw Error('erms should contain 2 elements');
}
const separator = '\n';

View File

@@ -45,7 +45,7 @@ function validateMetaData(): void {
const filePath = `${getProjectPath()}${id}.md`;
try {
fs.accessSync(filePath);
} catch (e) {
} catch (_e) {
throw new Error(
`The file
${filePath}

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import fs from 'fs';
import { join } from 'path';
import ObjectID from 'bson-objectid';

View File

@@ -84,7 +84,6 @@ describe('add-text', () => {
expect(file.data[instructionsId]).toBe(instructionsSectionText);
});
// eslint-disable-next-line max-len
it('should add nothing if a section id does not appear in the md', () => {
const plugin = addText([missingId]);
plugin(mockAST, file);
@@ -110,7 +109,6 @@ describe('add-text', () => {
expect(file.data[descriptionId]).toEqual(expect.stringContaining(expected));
});
// eslint-disable-next-line max-len
it('should not add paragraphs when html elements are separated by whitespace', () => {
const plugin = addText([instructionsId]);
plugin(realisticAST, file);

View File

@@ -41,8 +41,7 @@ async function initTestFrame(e: InitTestFrameArg = { code: {} }) {
JSON.stringify(a) === JSON.stringify(b);
// Hardcode Deep Freeze dependency
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const DeepFreeze = (o: Record<string, any>) => {
const DeepFreeze = (o: Record<string, unknown>) => {
Object.freeze(o);
Object.getOwnPropertyNames(o).forEach(function (prop) {
if (
@@ -51,8 +50,7 @@ async function initTestFrame(e: InitTestFrameArg = { code: {} }) {
(typeof o[prop] === 'object' || typeof o[prop] === 'function') &&
!Object.isFrozen(o[prop])
) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
DeepFreeze(o[prop]);
DeepFreeze(o[prop] as Record<string, unknown>);
}
});
return o;
@@ -73,7 +71,6 @@ async function initTestFrame(e: InitTestFrameArg = { code: {} }) {
import(/* webpackChunkName: "enzyme" */ 'enzyme'),
import(/* webpackChunkName: "enzyme-adapter" */ 'enzyme-adapter-react-16')
]);
/* eslint-enable no-inline-comments */
Enzyme.configure({ adapter: new Adapter16() });
/* eslint-enable prefer-const */
@@ -96,12 +93,13 @@ async function initTestFrame(e: InitTestFrameArg = { code: {} }) {
const test: unknown = eval(testString);
resolve(test);
} catch (err) {
reject(err);
reject(err as Error);
}
})
);
const test = await testPromise;
if (typeof test === 'function') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
await test(e.getUserInput);
}
return { pass: true };

View File

@@ -90,7 +90,7 @@ ctx.onmessage = async (e: PythonRunEvent) => {
eval(testString);
resolve(test);
} catch (err) {
reject(err);
reject(err as Error);
}
}
);

View File

@@ -125,7 +125,6 @@ ctx.onmessage = async (e: TestEvaluatorEvent) => {
// This can be reassigned by the eval inside the try block, so it should be declared as a let
// eslint-disable-next-line prefer-const
let __userCodeWasExecuted = false;
/* eslint-disable no-eval */
try {
// Logging is proxyed after the build to catch console.log messages
// generated during testing.
@@ -152,8 +151,8 @@ ${e.data.testString}`)) as unknown;
// the user code does not get executed.
testResult = eval(e.data.testString) as unknown;
}
/* eslint-enable no-eval */
if (typeof testResult === 'function') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
await testResult((fileName: string) =>
__toString(e.data.sources[fileName])
);

View File

@@ -2,8 +2,7 @@ import path from 'path';
import fs from 'fs';
import readdirp from 'readdirp';
// TODO: remove chai and use jest's assertion errors
import { AssertionError } from 'chai';
import { SuperBlocks } from '../../../shared/config/curriculum';
import {
superblockSchemaValidator,
@@ -46,10 +45,9 @@ describe('external curriculum data build', () => {
const result = validateAvailableSuperBlocks(availableSuperblocks);
if (result.error) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
throw new AssertionError(
result.error.message,
`file: available-superblocks.json`
throw Error(
`file: available-superblocks.json
${result.error.message}`
);
}
});
@@ -72,11 +70,8 @@ describe('external curriculum data build', () => {
const result = validateSuperBlock(JSON.parse(fileContent));
if (result.error) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
throw new AssertionError(
result.error.message,
`file: ${fileInArray}`
);
throw Error(`file: ${fileInArray}
${result.error.message}`);
}
});
});

View File

@@ -43,7 +43,6 @@ describe('markdown linter', () => {
it('should write to the console describing the problem', done => {
function callback() {
const expected =
// eslint-disable-next-line max-len
'badYML.md: 19: yaml-linter YAML code blocks should be valid [bad indentation of a mapping entry at line 3, column 17:\n testString: testString\n ^] [Context: "```yml"]';
expect(console.log.mock.calls.length).toBe(1);
expect(console.log.mock.calls[0][0]).toEqual(

View File

@@ -33,7 +33,6 @@ function handleError(err, client) {
} catch (e) {
// no-op
} finally {
/* eslint-disable-next-line no-process-exit */
process.exit(1);
}
}

View File

@@ -37,7 +37,6 @@ function handleError(err, client) {
} catch (e) {
// no-op
} finally {
/* eslint-disable-next-line no-process-exit */
process.exit(1);
}
}

View File

@@ -27,7 +27,6 @@ function handleError(err, client) {
} catch (e) {
// no-op
} finally {
/* eslint-disable-next-line no-process-exit */
process.exit(1);
}
}

View File

@@ -29,7 +29,6 @@ function handleError(err, client) {
} catch (e) {
// no-op
} finally {
/* eslint-disable-next-line no-process-exit */
process.exit(1);
}
}

View File

@@ -1,4 +1,3 @@
/* eslint-disable max-len */
const { ObjectId } = require('mongodb');
const blankUserId = new ObjectId('5bd30e0f1caf6ac3ddddddb9');