effect icon indicating copy to clipboard operation
effect copied to clipboard

@effect/schema/Arbitrary: Schema.pattern wipes out earlier constraints on Schema.string

Open caubut-charter opened this issue 1 year ago • 4 comments

What version of Effect is running?

2.4.5

What steps can reproduce the bug?

package.json
{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@effect/schema": "^0.64.1",
    "effect": "^2.4.5",
    "esbuild": "^0.20.1",
    "fast-check": "^3.16.0",
    "typescript": "^5.4.2"
  }
}
tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "module": "nodenext",
    "target": "ESNext",
    "lib": [
      "ESNext"
    ],
    "moduleResolution": "nodenext",
    "removeComments": true,
    "preserveConstEnums": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "exactOptionalPropertyTypes": true,
    "typeRoots": [
      "node_modules/@types"
    ],
    "sourceMap": true,
    "incremental": true,
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "experimentalDecorators": true,
    "noErrorTruncation": true,
    "downlevelIteration": true,
    "baseUrl": ".",
    "skipLibCheck": true
  }
}
index.ts
import * as Arbitrary from '@effect/schema/Arbitrary';
import * as S from '@effect/schema/Schema';
import { Effect } from 'effect';
import * as fc from 'fast-check';

export const GoodSsid = S.string.pipe(S.pattern(/^[- !#%&'()*,./0123456789:;<=>@A-Z^_`a-z{|}~]*$/), S.nonEmpty());
export const BadSsid = S.string.pipe(S.nonEmpty(), S.pattern(/^[- !#%&'()*,./0123456789:;<=>@A-Z^_`a-z{|}~]*$/));

const goodSsids = Arbitrary.make(GoodSsid)(fc);
const badSsids = Arbitrary.make(BadSsid)(fc);

console.log('GoodSsid');
Effect.runSync(S.decode(S.array(GoodSsid))(fc.sample(goodSsids, 1000)));
console.log('BadSsid');
Effect.runSync(S.decode(S.array(BadSsid))(fc.sample(badSsids, 1000)));
$ pnpm i
$ pnpm esbuild --bundle index.ts > index.js
$ node --version
v20.11.1
$ node index.js

What is the expected behavior?

Both schemas produce valid examples using fast-check and the ordering of constraints not mattering when using @effect/schema/Arbitrary.

What do you see instead?

Note that BadSsid prints showing that GoodSsid did not throw.

The key part of the error is Expected a non empty string, actual "" which shows that the constraint applied by S.nonEmpty was lost when it comes before the S.pattern.

GoodSsid
BadSsid
<REDACTED>/test/index.js:13377
      throw fiberFailure(result.i0);
      ^

ReadonlyArray<a string matching the pattern ^[- !#%&'()*,./0123456789:;<=>@A-Z^_`a-z{|}~]*$>
└─ [4]
   └─ a string matching the pattern ^[- !#%&'()*,./0123456789:;<=>@A-Z^_`a-z{|}~]*$
      └─ From side refinement failure
         └─ a non empty string
            └─ Predicate refinement failure
               └─ Expected a non empty string, actual ""
    at parseError (<REDACTED>/test/index.js:15726:31)
    at <REDACTED>/test/index.js:14896:20
    at <REDACTED>/test/index.js:27:20
    at <REDACTED>/test/index.js:15734:36
    at <REDACTED>/test/index.js:24611:50
    at Object.<anonymous> (<REDACTED>/test/index.js:24612:3)
    at Module._compile (node:internal/modules/cjs/loader:1376:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1435:10)
    at Module.load (node:internal/modules/cjs/loader:1207:32)
    at Module._load (node:internal/modules/cjs/loader:1023:12) {
  toJSON: [Function (anonymous)],
  toString: [Function (anonymous)],
  [Symbol(effect/Runtime/FiberFailure)]: Symbol(effect/Runtime/FiberFailure),
  [Symbol(effect/Runtime/FiberFailure/Cause)]: {
    _tag: 'Fail',
    error: ParseError: ReadonlyArray<a string matching the pattern ^[- !#%&'()*,./0123456789:;<=>@A-Z^_`a-z{|}~]*$>
    └─ [4]
       └─ a string matching the pattern ^[- !#%&'()*,./0123456789:;<=>@A-Z^_`a-z{|}~]*$
          └─ From side refinement failure
             └─ a non empty string
                └─ Predicate refinement failure
                   └─ Expected a non empty string, actual ""
        at parseError (<REDACTED>/test/index.js:15726:31)
        at <REDACTED>/test/index.js:14896:20
        at <REDACTED>/test/index.js:27:20
        at <REDACTED>/test/index.js:15734:36
        at <REDACTED>/test/index.js:24611:50
        at Object.<anonymous> (<REDACTED>/test/index.js:24612:3)
        at Module._compile (node:internal/modules/cjs/loader:1376:14)
        at Module._extensions..js (node:internal/modules/cjs/loader:1435:10)
        at Module.load (node:internal/modules/cjs/loader:1207:32)
        at Module._load (node:internal/modules/cjs/loader:1023:12) {
      error: Tuple {
        ast: TupleType {
          elements: [],
          rest: [ [Refinement] ],
          isReadonly: true,
          annotations: {},
          _tag: 'TupleType'
        },
        actual: [
          '06/(1;|',    '>) 5* &|=',  ';#)})3~7',   '{l3((3&@2',  '',
          '|8%:',       '__proto_',   'arguments',  ')',          '@&V{>',
          '-9%(`/18',   ',#',         '#QlC|9',     '3',          'G(3_~_}',
          '',           '',           '',           '9:224.A(',   ">&'",
          '=>(%',       ')efi(',      '/&=<{9-P*~', '~',          '#;21/',
          ')~name&dup', ';~*%5f',     ' } x#(0#/|', '7!',         '*2',
          "'%z:>%|8}6", '#~`>~87{-/', '#%',         '@%.0 #*.|',  '5l',
          '{',          ':&*>>',      '3',          'hasOwnProp', "&^`-_&w'=*",
          "%@/66)'",    '->2/',       '2} ',        ';#35~&%(`7', '>_m',
          '<(@({2_#',   '#7-< /',     "::< -6'",    ';(!-.',      '~{&~4r,}^',
          ")178=6'(3:", '>.48@_<',    '}/4/.Z29:6', '%!~`T%8*:',  '0_',
          '9=%03>8#|@', '(*`,4)',     '#',          "'5",         "'(U%",
          '(/=`99.(',   ' 4_;()8}@',  '2;`#~2-&',   '',           '',
          '*.-1',       '4~{4}391=',  '.1',         '',           '|:.b*}5',
          '-|v;<4}',    '!~^>{)',     '>',          '0',          "'|",
          '=25) ',      '8',          '*;)9*|6*^',  '~',          '7-4Y',
          ')',          '88_',        '3@}',        '9>%)%_|',    'G8;J~= ',
          '',           '= -',        '^,1',        'v}^ )=_',    '=>=',
          'isPrototy',  "6>~'_*",     '!',          '%0,13}@2#3', '',
          "9'o",        '',           '!{)@,86',    '^=5@/',      '!',
          ... 900 more items
        ],
        errors: [ Index { index: 4, error: [Refinement2], _tag: 'Index' } ],
        _tag: 'TupleType'
      },
      _tag: 'ParseError'
    }
  },
  [Symbol(nodejs.util.inspect.custom)]: [Function (anonymous)]
}

Additional information

No response

caubut-charter avatar Mar 13 '24 21:03 caubut-charter

@gcanti I think this is user error in retrospect. The regex /^[...]*$/ is effectively zero or more, so in that case it looks to be working as intended. It works fine using /^[...]+$/ for one or more.

If you agree, this can be closed.

caubut-charter avatar Mar 13 '24 22:03 caubut-charter

I think there might be an issue actually, checking the code if you use pattern any filters preceding it are ignored.

This is because pattern sets a custom hook for the Arbitrary compiler: https://github.com/Effect-TS/effect/blob/0c0b98d0c39513322385219de0aaa23d39077e0a/packages/schema/src/Schema.ts#L3084

gcanti avatar Mar 14 '24 18:03 gcanti

@gcanti I think this is user error in retrospect. The regex /^[...]*$/ is effectively zero or more, so in that case it looks to be working as intended. It works fine using /^[...]+$/ for one or more.

Encountered the same issue with stringMatching

const SchemaTest = Schema.string.pipe(Schema.pattern(/^(?=.*[A-Za-z]).*$/i));
const x = Arbitrary.make(SchemaTest)(fc);
console.log(x);
/*
node_modules/fast-check/lib/arbitrary/stringMatching.js:149
            throw new globals_2.Error(`Unable to use "stringMatching" against a regex using the flag ${flag}`);
                  ^

Error: Unable to use "stringMatching" against a regex using the flag i
    at Object.stringMatching
*/

steffanek avatar Apr 12 '24 18:04 steffanek

@steffanek it's a separate issue, it appears to be a limitation upstream in stringMatching https://github.com/dubzzz/fast-check/pull/3925

gcanti avatar Apr 13 '24 10:04 gcanti