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

chore: add regexp related eslint plugins #202

Merged
merged 11 commits into from
Oct 20, 2023
36 changes: 33 additions & 3 deletions library/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -4,15 +4,45 @@ module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:regexp/recommended',
'plugin:security/recommended',
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'import'],
plugins: ['@typescript-eslint', 'import', 'redos-detector'],
rules: {
// Enable rules -----------------------------------------------------------

// Import
'import/extensions': ['error', 'always'], // Require file extensions

// Regexp
'regexp/no-super-linear-move': 'error', // Prevent DoS regexps
'regexp/no-control-character': 'error', // Avoid unneeded regexps characters
'regexp/no-octal': 'error', // Avoid unneeded regexps characters
'regexp/no-standalone-backslash': 'error', // Avoid unneeded regexps characters
'regexp/prefer-escape-replacement-dollar-char': 'error', // Avoid unneeded regexps characters
'regexp/prefer-quantifier': 'error', // Avoid unneeded regexps characters
'regexp/hexadecimal-escape': ['error', 'always'], // Avoid unneeded regexps characters
'regexp/sort-alternatives': 'error', // Avoid unneeded regexps characters
'regexp/require-unicode-regexp': 'error', // /u flag is faster and enables regexp strict mode
'regexp/prefer-regexp-exec': 'error', // Enforce that RegExp#exec is used instead of String#match if no global flag is provided, as exec is faster

// Redos detector
'redos-detector/no-unsafe-regex': ['error', { ignoreError: true }], // Prevent DoS regexps

// Disable rules ----------------------------------------------------------

// Default
'no-duplicate-imports': 'off',

// TypeScript
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/consistent-type-imports': 'warn',
'@typescript-eslint/no-non-null-assertion': 'off',
'no-duplicate-imports': 'off',
'import/extensions': ['error', 'always'],

// Security
'security/detect-object-injection': 'off', // Too many false positives
'security/detect-unsafe-regex': 'off', // Too many false positives, see https://github.com/eslint-community/eslint-plugin-security/issues/28 - we use the redos-detector plugin instead
},
};
1 change: 1 addition & 0 deletions library/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ All notable changes to the library will be documented in this file.
- Add `getRestAndDefaultArgs` utility function
- Add new `rest` argument to `object` and `objectAsync` schema
- Fix type check in `date` and `dateAsync` for invalid dates (pull request #214)
- Improve security of regular expressions (pull request #202)
- Change `ObjectSchema` and `ObjectSchemaAsync` type
- Change type check in `tuple` and `tupleAsync` to be less strict
- Rename `ObjectShape` and `ObjectShapeAsync` types to `ObjectEntries` and `ObjectEntriesAsync`
3 changes: 3 additions & 0 deletions library/package.json
Original file line number Diff line number Diff line change
@@ -58,6 +58,9 @@
"@vitest/coverage-v8": "^0.33.0",
"eslint": "^8.43.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-redos-detector": "^2.1.1",
"eslint-plugin-regexp": "^1.15.0",
"eslint-plugin-security": "^1.7.1",
"jsdom": "^22.1.0",
"tsup": "^7.1.0",
"typescript": "^5.1.3",
2 changes: 1 addition & 1 deletion library/src/schemas/special/special.test.ts
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import { special } from './special.ts';

type PixelString = `${number}px`;
const isPixelString = (input: unknown) =>
typeof input === 'string' && /^\d+px$/.test(input);
typeof input === 'string' && /^\d+px$/u.test(input);

describe('special', () => {
test('should pass only pixel strings', () => {
2 changes: 1 addition & 1 deletion library/src/schemas/special/specialAsync.test.ts
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import { specialAsync } from './specialAsync.ts';

type PixelString = `${number}px`;
const isPixelString = (input: unknown) =>
typeof input === 'string' && /^\d+px$/.test(input);
typeof input === 'string' && /^\d+px$/u.test(input);

describe('specialAsync', () => {
test('should pass only pixel strings', async () => {
2 changes: 1 addition & 1 deletion library/src/utils/isLuhnAlgo/isLuhnAlgo.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@
*/
export function isLuhnAlgo(input: string) {
// Remove any non-digit chars
const number = input.replace(/\D/g, '');
const number = input.replace(/\D/gu, '');

// Create necessary variables
let length = number.length;
2 changes: 1 addition & 1 deletion library/src/validations/cuid2/cuid2.ts
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function cuid2<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^[a-z][a-z0-9]*$/.test(input)
!/^[a-z][\da-z]*$/u.test(input)
? getPipeIssues('cuid2', error || 'Invalid cuid2', input)
: getOutput(input);
}
6 changes: 4 additions & 2 deletions library/src/validations/email/email.ts
Original file line number Diff line number Diff line change
@@ -2,15 +2,17 @@ import type { ErrorMessage, PipeResult } from '../../types.ts';
import { getOutput, getPipeIssues } from '../../utils/index.ts';

/**
* Creates a validation function that validates an email.
* Creates a validation function that validates a email.
*
* @param error The error message.
*
* @returns A validation function.
*/
export function email<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^[\w+-]+(?:\.[\w+-]+)*@[\da-z]+(?:[.-][\da-z]+)*\.[a-z]{2,}$/i.test(input)
!/^[\w+-]+(?:\.[\w+-]+)*@[\da-z]+(?:[.-][\da-z]+)*\.[a-z]{2,}$/iu.test(
input
)
? getPipeIssues('email', error || 'Invalid email', input)
: getOutput(input);
}
2 changes: 1 addition & 1 deletion library/src/validations/emoji/emoji.ts
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function emoji<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^(\p{Extended_Pictographic}|\p{Emoji_Component})+$/u.test(input)
!/^[\p{Extended_Pictographic}\p{Emoji_Component}]+$/u.test(input)
? getPipeIssues('emoji', error || 'Invalid emoji', input)
: getOutput(input);
}
3 changes: 1 addition & 2 deletions library/src/validations/imei/imei.ts
Original file line number Diff line number Diff line change
@@ -12,8 +12,7 @@ import { getOutput, getPipeIssues, isLuhnAlgo } from '../../utils/index.ts';
*/
export function imei<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^\d{2}[ |/|-]?\d{6}[ |/|-]?\d{6}[ |/|-]?\d$/.test(input) ||
!isLuhnAlgo(input)
!/^\d{2}(?:[ /|-]?\d{6}){2}[ /|-]?\d$/u.test(input) || !isLuhnAlgo(input)
? getPipeIssues('imei', error || 'Invalid IMEI', input)
: getOutput(input);
}
5 changes: 3 additions & 2 deletions library/src/validations/ip/ip.ts
Original file line number Diff line number Diff line change
@@ -10,8 +10,9 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function ip<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/.test(input) &&
!/^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/.test(
// eslint-disable-next-line redos-detector/no-unsafe-regex -- false positive
!/^(?:(?:25[0-5]|(?:2[0-4]|1\d|[1-9])?\d)\.?\b){4}$/u.test(input) &&
!/^(?:(?:[\da-f]{1,4}:){7}[\da-f]{1,4}|(?:[\da-f]{1,4}:){1,7}:|(?:[\da-f]{1,4}:){1,6}:[\da-f]{1,4}|(?:[\da-f]{1,4}:){1,5}(?::[\da-f]{1,4}){1,2}|(?:[\da-f]{1,4}:){1,4}(?::[\da-f]{1,4}){1,3}|(?:[\da-f]{1,4}:){1,3}(?::[\da-f]{1,4}){1,4}|(?:[\da-f]{1,4}:){1,2}(?::[\da-f]{1,4}){1,5}|[\da-f]{1,4}:(?::[\da-f]{1,4}){1,6}|:(?:(?::[\da-f]{1,4}){1,7}|:)|fe80:(?::[\da-f]{0,4}){0,4}%[\da-z]+|::(?:f{4}(?::0{1,4})?:)?(?:(?:25[0-5]|(?:2[0-4]|1?\d)?\d)\.){3}(?:25[0-5]|(?:2[0-4]|1?\d)?\d)|(?:[\da-f]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1?\d)?\d)\.){3}(?:25[0-5]|(?:2[0-4]|1?\d)?\d))$/iu.test(
input
)
? getPipeIssues('ip', error || 'Invalid IP', input)
3 changes: 2 additions & 1 deletion library/src/validations/ipv4/ipv4.ts
Original file line number Diff line number Diff line change
@@ -10,7 +10,8 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function ipv4<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/.test(input)
// eslint-disable-next-line redos-detector/no-unsafe-regex -- false positive
!/^(?:(?:25[0-5]|(?:2[0-4]|1\d|[1-9])?\d)\.?\b){4}$/u.test(input)
? getPipeIssues('ipv4', error || 'Invalid IP v4', input)
: getOutput(input);
}
2 changes: 1 addition & 1 deletion library/src/validations/ipv6/ipv6.ts
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function ipv6<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/.test(
!/^(?:(?:[\da-f]{1,4}:){7}[\da-f]{1,4}|(?:[\da-f]{1,4}:){1,7}:|(?:[\da-f]{1,4}:){1,6}:[\da-f]{1,4}|(?:[\da-f]{1,4}:){1,5}(?::[\da-f]{1,4}){1,2}|(?:[\da-f]{1,4}:){1,4}(?::[\da-f]{1,4}){1,3}|(?:[\da-f]{1,4}:){1,3}(?::[\da-f]{1,4}){1,4}|(?:[\da-f]{1,4}:){1,2}(?::[\da-f]{1,4}){1,5}|[\da-f]{1,4}:(?::[\da-f]{1,4}){1,6}|:(?:(?::[\da-f]{1,4}){1,7}|:)|fe80:(?::[\da-f]{0,4}){0,4}%[\da-z]+|::(?:f{4}(?::0{1,4})?:)?(?:(?:25[0-5]|(?:2[0-4]|1?\d)?\d)\.){3}(?:25[0-5]|(?:2[0-4]|1?\d)?\d)|(?:[\da-f]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1?\d)?\d)\.){3}(?:25[0-5]|(?:2[0-4]|1?\d)?\d))$/iu.test(
input
)
? getPipeIssues('ipv6', error || 'Invalid IP v6', input)
4 changes: 2 additions & 2 deletions library/src/validations/isoDate/isoDate.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import type { ErrorMessage, PipeResult } from '../../types.ts';
import { getOutput, getPipeIssues } from '../../utils/index.ts';

/**
* Creates a validation function that validates an date.
* Creates a validation function that validates a date.
*
* Format: yyyy-mm-dd
*
@@ -16,7 +16,7 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function isoDate<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^\d{4}-(0[1-9]|1[0-2])-([12]\d|0[1-9]|3[01])$/.test(input)
!/^\d{4}-(?:0[1-9]|1[0-2])-(?:[12]\d|0[1-9]|3[01])$/u.test(input)
? getPipeIssues('iso_date', error || 'Invalid date', input)
: getOutput(input);
}
4 changes: 2 additions & 2 deletions library/src/validations/isoDateTime/isoDateTime.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import type { ErrorMessage, PipeResult } from '../../types.ts';
import { getOutput, getPipeIssues } from '../../utils/index.ts';

/**
* Creates a validation function that validates an datetime.
* Creates a validation function that validates a datetime.
*
* Format: yyyy-mm-ddThh:mm
*
@@ -16,7 +16,7 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function isoDateTime<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^\d{4}-(0[1-9]|1[0-2])-([12]\d|0[1-9]|3[01])T(0[0-9]|1\d|2[0-3]):[0-5]\d$/.test(
!/^\d{4}-(?:0[1-9]|1[0-2])-(?:[12]\d|0[1-9]|3[01])T(?:0\d|1\d|2[0-3]):[0-5]\d$/u.test(
input
)
? getPipeIssues('iso_date_time', error || 'Invalid datetime', input)
2 changes: 1 addition & 1 deletion library/src/validations/isoTime/isoTime.ts
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function isoTime<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^(0[0-9]|1\d|2[0-3]):[0-5]\d$/.test(input)
!/^(?:0\d|1\d|2[0-3]):[0-5]\d$/u.test(input)
? getPipeIssues('iso_time', error || 'Invalid time', input)
: getOutput(input);
}
2 changes: 1 addition & 1 deletion library/src/validations/isoTimeSecond/isoTimeSecond.ts
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function isoTimeSecond<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^(0[0-9]|1\d|2[0-3]):[0-5]\d:[0-5]\d$/.test(input)
!/^(?:0\d|1\d|2[0-3])(?::[0-5]\d){2}$/u.test(input)
? getPipeIssues('iso_time_second', error || 'Invalid time', input)
: getOutput(input);
}
2 changes: 1 addition & 1 deletion library/src/validations/isoTimestamp/isoTimestamp.ts
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function isoTimestamp<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^\d{4}-(0[1-9]|1[0-2])-([12]\d|0[1-9]|3[01])T(0[0-9]|1\d|2[0-3]):[0-5]\d:[0-5]\d\.\d{3}Z$/.test(
!/^\d{4}-(?:0[1-9]|1[0-2])-(?:[12]\d|0[1-9]|3[01])T(?:0\d|1\d|2[0-3])(?::[0-5]\d){2}\.\d{3}Z$/u.test(
input
)
? getPipeIssues('iso_timestamp', error || 'Invalid timestamp', input)
2 changes: 1 addition & 1 deletion library/src/validations/isoWeek/isoWeek.ts
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function isoWeek<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^\d{4}-W(0[1-9]|[1-4]\d|5[0-3])$/.test(input)
!/^\d{4}-W(?:0[1-9]|[1-4]\d|5[0-3])$/u.test(input)
? getPipeIssues('iso_week', error || 'Invalid week', input)
: getOutput(input);
}
4 changes: 2 additions & 2 deletions library/src/validations/regex/regex.test.ts
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ import { regex } from './regex.ts';

describe('regex', () => {
test('should pass only valid strings', () => {
const validate = regex(/^ID-\d{3}$/);
const validate = regex(/^ID-\d{3}$/u);
expect(validate('ID-000').output).toBe('ID-000');
expect(validate('ID-123').output).toBe('ID-123');
expect(validate('123').issues).toBeTruthy();
@@ -13,7 +13,7 @@ describe('regex', () => {

test('should return custom error message', () => {
const error = 'Value does not match the regex!';
const validate = regex(/^ID-\d{3}$/, error);
const validate = regex(/^ID-\d{3}$/u, error);
expect(validate('test').issues?.[0].message).toBe(error);
});
});
2 changes: 1 addition & 1 deletion library/src/validations/ulid/ulid.ts
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function ulid<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^[0-9A-HJKMNPQ-TV-Z]{26}$/i.test(input)
!/^[\da-hjkmnp-tv-z]{26}$/iu.test(input)
? getPipeIssues('ulid', error || 'Invalid ULID', input)
: getOutput(input);
}
4 changes: 1 addition & 3 deletions library/src/validations/uuid/uuid.ts
Original file line number Diff line number Diff line change
@@ -10,9 +10,7 @@ import { getOutput, getPipeIssues } from '../../utils/index.ts';
*/
export function uuid<TInput extends string>(error?: ErrorMessage) {
return (input: TInput): PipeResult<TInput> =>
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
input
)
!/^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/iu.test(input)
? getPipeIssues('uuid', error || 'Invalid UUID', input)
: getOutput(input);
}
157 changes: 142 additions & 15 deletions pnpm-lock.yaml