Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lint): created local rule to use isNullish and nonNullish #2553

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ module.exports = {
curly: 'error',
'local-rules/prefer-object-params': 'warn',
'local-rules/no-svelte-store-in-api': 'error',
'local-rules/use-nullish-checks': 'warn',
'local-rules/use-option-type-wrapper': 'warn',
'import/no-duplicates': ['error', { 'prefer-inline': true }],
'no-console': ['error', { allow: ['error', 'warn'] }],
Expand Down
49 changes: 49 additions & 0 deletions eslint-local-rules.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -172,5 +172,54 @@ module.exports = {
}
};
}
},
'use-nullish-checks': {
meta: {
type: 'suggestion',
docs: {
description: 'Enforce the use of isNullish functions for nullish checks',
category: 'Best Practices',
recommended: true
},
fixable: 'code',
schema: []
},
create(context) {
const isNullishMessage =
'Use isNullish() instead of direct variable check for nullish checks.';
const nonNullishMessage =
'Use nonNullish() instead of direct variable check for nullish checks.';

const binaryCheck = (node) => {
if (node.type === 'BinaryExpression') {
return (
(node.operator === '===' || node.operator === '!==') &&
((node.right.type === 'Identifier' && node.right.name === 'undefined') ||
(node.right.type === 'Literal' && node.right.value === null))
);
}
};

const binaryReportCheck = (node) => {
context.report({
node,
message: node.operator === '===' ? isNullishMessage : nonNullishMessage,
fix(fixer) {
return fixer.replaceText(
node,
`${node.operator === '===' ? 'isNullish' : 'nonNullish'}(${context.getSourceCode().getText(node.left)})`
);
}
});
};

return {
BinaryExpression(node) {
if (binaryCheck(node)) {
binaryReportCheck(node);
}
}
};
}
}
};
2 changes: 2 additions & 0 deletions scripts/build.copy-workers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ await cp(
filter: (source) => extname(source) !== '.map'
},
(err) => {
// TODO: Remove ESLint exception and use nullish checks
// eslint-disable-next-line local-rules/use-nullish-checks
if (err === null) {
return;
}
Expand Down
2 changes: 2 additions & 0 deletions scripts/build.tokens.ckerc20.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ const buildOrchestratorInfo = async (orchestratorId) => {

const assertUniqueTokenSymbol = Object.values(tokens).find((value) => value.length > 1);

// TODO: Remove ESLint exception and use nullish checks
// eslint-disable-next-line local-rules/use-nullish-checks
if (assertUniqueTokenSymbol !== undefined) {
throw new Error(
`More than one pair of ledger and index canisters were used for the token symbol ${assertUniqueTokenSymbol}.`
Expand Down
22 changes: 13 additions & 9 deletions src/frontend/src/eth/components/tokens/AddTokenReview.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { isNullish } from '@dfinity/utils';
import { isNullish, nonNullish } from '@dfinity/utils';
import { createEventDispatcher, onMount } from 'svelte';
import { fade } from 'svelte/transition';
import { erc20Tokens } from '$eth/derived/erc20.derived';
Expand Down Expand Up @@ -41,9 +41,11 @@
}

if (
$erc20Tokens?.find(
({ address }) => address.toLowerCase() === contractAddress?.toLowerCase()
) !== undefined
nonNullish(
$erc20Tokens?.find(
({ address }) => address.toLowerCase() === contractAddress?.toLowerCase()
)
)
) {
toastsError({
msg: { text: $i18n.tokens.error.already_available }
Expand All @@ -67,11 +69,13 @@
}

if (
$erc20Tokens?.find(
({ symbol, name }) =>
symbol.toLowerCase() === (metadata?.symbol.toLowerCase() ?? '') ||
name.toLowerCase() === (metadata?.name.toLowerCase() ?? '')
) !== undefined
nonNullish(
$erc20Tokens?.find(
({ symbol, name }) =>
symbol.toLowerCase() === (metadata?.symbol.toLowerCase() ?? '') ||
name.toLowerCase() === (metadata?.name.toLowerCase() ?? '')
)
)
) {
toastsError({
msg: { text: $i18n.tokens.error.duplicate_metadata }
Expand Down
1 change: 1 addition & 0 deletions src/frontend/src/eth/derived/erc20.derived.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export const enabledErc20TokensAddresses: Readable<ContractAddressText[]> = deri

export const erc20UserTokensInitialized: Readable<boolean> = derived(
[erc20UserTokensStore],
// eslint-disable-next-line local-rules/use-nullish-checks -- We want to check only for non-initiated state
([$erc20UserTokensStore]) => $erc20UserTokensStore !== undefined
);

Expand Down
1 change: 1 addition & 0 deletions src/frontend/src/icp-eth/services/cketh.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const loadCkEthMinterInfo = async ({
const minterInfoInStore = get(ckEthMinterInfoStore);

// We try to load only once per session the helpers (ckETH and ckErc20) contract addresses
// eslint-disable-next-line local-rules/use-nullish-checks -- We want to check for not-undefined values but null is allowed
if (minterInfoInStore?.[tokenId] !== undefined) {
return;
}
Expand Down
16 changes: 9 additions & 7 deletions src/frontend/src/icp/services/ic-add-custom-tokens.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { i18n } from '$lib/stores/i18n.store';
import { toastsError } from '$lib/stores/toasts.store';
import type { OptionIdentity } from '$lib/types/identity';
import type { Identity } from '@dfinity/agent';
import { assertNonNullish, isNullish } from '@dfinity/utils';
import { assertNonNullish, isNullish, nonNullish } from '@dfinity/utils';
import { get } from 'svelte/store';

export interface ValidateTokenData {
Expand Down Expand Up @@ -99,11 +99,13 @@ const assertExistingTokens = ({
token: IcTokenWithoutId;
}): { valid: boolean } => {
if (
icrcTokens.find(
({ symbol, name }) =>
symbol.toLowerCase() === token.symbol.toLowerCase() ||
name.toLowerCase() === token.name.toLowerCase()
) !== undefined
nonNullish(
icrcTokens.find(
({ symbol, name }) =>
symbol.toLowerCase() === token.symbol.toLowerCase() ||
name.toLowerCase() === token.name.toLowerCase()
)
)
) {
toastsError({
msg: { text: get(i18n).tokens.error.duplicate_metadata }
Expand All @@ -121,7 +123,7 @@ const assertAlreadyAvailable = ({
}: {
icrcTokens: IcToken[];
} & IcCanisters): { alreadyAvailable: boolean } => {
if (icrcTokens?.find(({ ledgerCanisterId: id }) => id === ledgerCanisterId) !== undefined) {
if (nonNullish(icrcTokens?.find(({ ledgerCanisterId: id }) => id === ledgerCanisterId))) {
toastsError({
msg: { text: get(i18n).tokens.error.already_available }
});
Expand Down
4 changes: 4 additions & 0 deletions src/frontend/src/icp/utils/cketh-transactions.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ export const mapCkEthereumTransaction = ({
const memoInfo = nonNullish(memo) ? mintMemoInfo(memo) : undefined;

const from =
// TODO: Remove ESLint exception and use nullish checks
// eslint-disable-next-line local-rules/use-nullish-checks
memoInfo?.fromAddress !== undefined
? mapAddressStartsWith0x(memoInfo.fromAddress)
: undefined;
Expand All @@ -102,6 +104,8 @@ export const mapCkEthereumTransaction = ({
const burnMemo = burnMemoInfo(memo);

const to =
// TODO: Remove ESLint exception and use nullish checks
// eslint-disable-next-line local-rules/use-nullish-checks
burnMemo?.toAddress !== undefined ? mapAddressStartsWith0x(burnMemo.toAddress) : undefined;

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
export let token: TokenUi;
</script>

<!-- eslint-disable-next-line local-rules/use-nullish-checks -- We want to check for undefined and not null, since they mean different situations -->
{#if token.balance === undefined}
<span class="w-full max-w-[100px] mt-2"><SkeletonText /></span>
{:else}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
export let token: TokenUi;
</script>

<!-- eslint-disable-next-line local-rules/use-nullish-checks -- We want to check for undefined and not null, since they mean different situations -->
{#if token.balance === undefined || !$exchangeInitialized}
<span class="w-full max-w-[50px]"><SkeletonText /></span>
{:else}
Expand Down
1 change: 1 addition & 0 deletions src/frontend/src/lib/components/ui/Json.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
| 'undefined';

const getValueType = (value: unknown): ValueType => {
// eslint-disable-next-line local-rules/use-nullish-checks -- We want to check for null and not undefined
if (value === null) {
return 'null';
}
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/lib/derived/auth.derived.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { derived, type Readable } from 'svelte/store';

export const authSignedIn: Readable<boolean> = derived(
authStore,
// TODO: Remove ESLint exception and use nullish checks
// eslint-disable-next-line local-rules/use-nullish-checks
({ identity }) => identity !== null && identity !== undefined
);

Expand Down
1 change: 1 addition & 0 deletions src/frontend/src/lib/utils/certified-store.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ import type { CertifiedData } from '$lib/types/store';
import type { Option } from '$lib/types/utils';

export const mapCertifiedData = <T>(certifiedData: Option<CertifiedData<T>>): Option<T> =>
// eslint-disable-next-line local-rules/use-nullish-checks -- We need to check for null explicitly, since it is the scope of this function.
certifiedData === null ? null : certifiedData?.data;
5 changes: 4 additions & 1 deletion src/frontend/src/lib/utils/json.utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { Principal } from '@dfinity/principal';
import { isNullish } from '@dfinity/utils';

// e.g. payloads.did/state_hash
export const isHash = (bytes: number[]): boolean =>
bytes.length === 32 &&
bytes.find((value) => !Number.isInteger(value) || value < 0 || value > 255) === undefined;
isNullish(bytes.find((value) => !Number.isInteger(value) || value < 0 || value > 255));

// Convert a byte array to a hex string
const bytesToHexString = (bytes: number[]): string =>
Expand Down Expand Up @@ -58,6 +59,8 @@ export const stringifyJson = ({
break;
}
case 'bigint': {
// TODO: Remove ESLint exception and use nullish checks
// eslint-disable-next-line local-rules/use-nullish-checks
if (options?.devMode !== undefined && options.devMode) {
return `BigInt('${value.toString()}')`;
}
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/lib/utils/nav.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ export const loadRouteParams = ($event: LoadEvent): RouteParams => {
const token = searchParams?.get('token');

const replaceEmoji = (input: string | null): string | null => {
// TODO: Remove ESLint exception and use nullish checks
// eslint-disable-next-line local-rules/use-nullish-checks
if (input === null) {
return null;
}
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/lib/utils/route.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export const replaceHistory = (url: URL) => {
* Source: https://stackoverflow.com/a/6825002/5404186
*/
const supportsHistory = (): boolean =>
// TODO: Remove ESLint exception and use nullish checks
// eslint-disable-next-line local-rules/use-nullish-checks
window.history !== undefined &&
'pushState' in window.history &&
typeof window.history.pushState !== 'undefined';
8 changes: 8 additions & 0 deletions src/frontend/src/lib/workers/auth.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const onIdleSignOut = async () => {
const [auth, chain] = await Promise.all([checkAuthentication(), checkDelegationChain()]);

// Both identity and delegation are alright, so all good
// TODO: Remove ESLint exception and use nullish checks
// eslint-disable-next-line local-rules/use-nullish-checks
if (auth && chain.valid && chain.delegation !== null) {
emitExpirationTime(chain.delegation);
return;
Expand Down Expand Up @@ -68,9 +70,13 @@ const checkDelegationChain = async (): Promise<{
const idbStorage: IdbStorage = new IdbStorage();
const delegationChain: string | null = await idbStorage.get(KEY_STORAGE_DELEGATION);

// TODO: Remove ESLint exception and use nullish checks
// eslint-disable-next-line local-rules/use-nullish-checks
const delegation = delegationChain !== null ? DelegationChain.fromJSON(delegationChain) : null;

return {
// TODO: Remove ESLint exception and use nullish checks
// eslint-disable-next-line local-rules/use-nullish-checks
valid: delegation !== null && isDelegationValid(delegation),
delegation
};
Expand All @@ -88,6 +94,8 @@ const emitExpirationTime = (delegation: DelegationChain) => {
const expirationTime: bigint | undefined = delegation.delegations[0]?.delegation.expiration;

// That would be unexpected here because the delegation has just been tested and is valid
// TODO: Remove ESLint exception and use nullish checks
// eslint-disable-next-line local-rules/use-nullish-checks
if (expirationTime === undefined) {
return;
}
Expand Down
3 changes: 1 addition & 2 deletions src/frontend/src/routes/(app)/transactions/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
await goto('/');
}

const unknownNetwork =
$networks.find(({ id }) => id.description === $routeNetwork) === undefined;
const unknownNetwork = isNullish($networks.find(({ id }) => id.description === $routeNetwork));

if (unknownNetwork) {
await goto('/');
Expand Down
4 changes: 2 additions & 2 deletions src/frontend/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { Spinner, Toasts } from '@dfinity/gix-components';
import { nonNullish } from '@dfinity/utils';
import { isNullish, nonNullish } from '@dfinity/utils';
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
import { browser } from '$app/environment';
Expand Down Expand Up @@ -75,7 +75,7 @@
}

// We want to display a spinner until the authentication is loaded. This to avoid a glitch when either the landing page or effective content (sign-in / sign-out) is presented.
if ($authStore === undefined) {
if (isNullish($authStore)) {
return;
}

Expand Down
1 change: 1 addition & 0 deletions src/frontend/src/tests/mocks/qr-generator.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const generateUrn = ({

const queryString = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
// eslint-disable-next-line local-rules/use-nullish-checks -- We want to check for not-undefined values but null is allowed
if (value !== undefined) {
queryString.append(key, value.toString());
}
Expand Down
11 changes: 7 additions & 4 deletions vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isNullish, nonNullish } from '@dfinity/utils';
import inject from '@rollup/plugin-inject';
import { sveltekit } from '@sveltejs/kit/vite';
import { basename, dirname, resolve } from 'node:path';
Expand Down Expand Up @@ -36,16 +37,18 @@ const config: UserConfig = {
const lazy = ['@dfinity/nns', '@dfinity/nns-proto', 'html5-qrcode', 'qr-creator'];

if (
['@sveltejs', 'svelte', '@dfinity/gix-components', ...lazy].find((lib) =>
folder.includes(lib)
) === undefined &&
isNullish(
['@sveltejs', 'svelte', '@dfinity/gix-components', ...lazy].find((lib) =>
folder.includes(lib)
)
) &&
folder.includes('node_modules')
) {
return 'vendor';
}

if (
lazy.find((lib) => folder.includes(lib)) !== undefined &&
nonNullish(lazy.find((lib) => folder.includes(lib))) &&
folder.includes('node_modules')
) {
return 'lazy';
Expand Down
2 changes: 2 additions & 0 deletions vite.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ const readRemoteCanisterIds = ({ prefix }: { prefix?: string }): Record<string,
return Object.entries(canisters).reduce((acc, current: [string, Details]) => {
const [canisterName, canisterDetails] = current;

// TODO: Remove ESLint exception and use nullish checks
// eslint-disable-next-line local-rules/use-nullish-checks
if (canisterDetails.remote !== undefined) {
const ids = Object.entries(canisterDetails.remote.id).reduce(
(acc, [network, id]) => ({
Expand Down