<commit_before>/* eslint-disable @typescript-eslint/no-unsafe-return */

import * as types from '../../gen/items/items'
import * as recast from 'recast'
import { ASTNode as AST, namedTypes as n, builders as b } from 'ast-types'

import api from '../../gen/api/key-formatter.json'
// move this upgrade to setup/extract-api after migration
const object_or_null = { oneOf: [ { type: 'object' }, { type: 'null' } ] }
const basics = {
  loc: object_or_null,
  comments: object_or_null,
  regex: object_or_null,
}
function upgrade(type) {
  switch (type.type) {
    case 'string':
    case 'number':
    case 'boolean':
      return {
        type: 'object',
        properties: {
          type: { const: 'Literal' },
          value: { type: type.type },
          raw: { type: 'string' },
          ...basics,
        },
        required: [ 'type', 'value' ],
        additionalProperties: false,
      }

    case 'array':
      return {
        type: 'object',
        properties: {
          type: { const: 'ArrayExpression' },
          elements: { type: 'array', items: upgrade(type.items) },
          ...basics,
        },
        required: [ 'type', 'elements' ],
        additionalProperties: false,
      }
  }

  if (typeof type.const !== 'undefined') {
    return {
      type: 'object',
      properties: {
        type: { const: 'Literal' },
        value: { const: type.const },
        raw: { type: 'string' },
        ...basics,
      },
      required: [ 'type', 'value' ],
      additionalProperties: false,
    }
  }

  if (type.instanceof === 'RegExp') {
    return {
      type: 'object',
      properties: {
        type: { const: 'Literal' },
        value: { type: 'object' },
        raw: { type: 'string' },
        ...basics,
        regex: {
          type: 'object',
          properties: {
            pattern: { type: 'string' },
            flags: { type: 'string' },
            ...basics,
          },
          required: [ 'pattern', 'flags' ],
          additionalProperties: false,
        },
      },
      required: [ 'type', 'regex' ],
      additionalProperties: false,
    }
  }

  if (type.oneOf) {
    return { oneOf: type.oneOf.map(t => upgrade(t)) }
  }

  if (type.anyOf) {
    return { anyOf: type.anyOf.map(t => upgrade(t)) }
  }

  throw { notUpgradable: type } // eslint-disable-line no-throw-literal
}
const astapi: typeof api = JSON.parse(JSON.stringify(api))
for (const meta of Object.values(astapi)) {
  for (const property of Object.keys(meta.schema.properties)) {
    meta.schema.properties[property] = upgrade(meta.schema.properties[property])
  }
}

const methodnames: Record<string, string> = Object.keys(astapi).reduce((acc, name) => { acc[name.toLowerCase()]=name; return acc }, {} as Record<string, string>)
function findMethod(fname: string): string {
  const uscore = fname.replace(/[a-z][A-Z]/g, chr => `${chr[0]}_${chr[1]}`).toLowerCase()
  const duscore = fname.replace(/[a-z][A-Z]/g, chr => `${chr[0]}__${chr[1]}`).toLowerCase()
  for (const prefix of ['', '$', '_']) {
    for (let name of [uscore, duscore]) {
      if (name = methodnames[prefix + name]) return name
    }
  }
  return ''
}

import { validator } from '../ajv'
for (const method of Object.values(astapi)) {
  (method  as any).validate = validator((method  as any).schema)
}

for (const fname in astapi) {
  if (fname[0] !== '_' && fname[0] !== '$') throw new Error(`Unexpected fname ${fname}`)
}

type Context = { arguments?: boolean, coerce?: boolean }
export class PatternParser {
  public code: string
  private finder: AST
  private ftype: string

  constructor(source: string) {
    this.finder = recast.parse('[].find(pattern => { try { return pattern() } catch (err) { if (err.next) return ""; throw err } })')
    this.addpattern(recast.parse(source).program.body[0].expression)
    this.insert(recast.parse('"zotero-" + this.item.id').program.body[0].expression, false)
    this.code = recast.prettyPrint(this.finder, {tabWidth: 2}).code
  }

  private error(expr): void {
    throw new Error(`Unexpected ${expr.type} at ${expr.loc.start.column}`)
  }

  protected Literal(expr: AST, _context: Context): AST {
    return expr
  }

  private resolveArguments(fname: string, args: AST[]): AST[] {
    const method = astapi[findMethod(fname)] // transitional before rename in formatter.ts
    const kind = {$: 'function', _: 'filter'}[fname[0]]
    fname = fname.slice(1)
    const me = `${kind} ${JSON.stringify(fname)}`
    if (!method) throw new Error(`No such ${me}`)

    let named = false
    args.forEach((arg, i) => {
      if (!arg.named_argument) {
        if (named) throw new Error(`${me}: positional argument ${i+1} after named argument`)
        arg.named_argument = method.parameters[i]
      }
      else {
        if (method.rest) throw new Error(`${me}: named argument not supported in rest function`)
        named = true
      }
    })

    let parameters = {}
    args = args.map(arg => {
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      const { named_argument, loc, ...argc } = arg
      if (typeof parameters[named_argument] === 'undefined') {
        parameters[named_argument] = argc
      }
      else {
        throw new Error(`${me}: duplicate argument ${JSON.stringify(named_argument)}`)
      }
      return argc
    })

    if (method.rest) {
      if (method.parameters.length !== 1) throw new Error(`${me}: ...rest method may have only one parameter, got ${method.parameters.join(', ')}`)
      parameters = { [method.rest]: b.arrayExpression(args) }
    }
    else {
      if (method.parameters.length < args.length) throw new Error(`${me}: expected ${method.parameters.length} arguments, got ${args.length}`)

      args = method.parameters.map((param: string) => parameters[param] as AST || b.identifier('undefined') as AST)
      let arg
      while (args.length && (arg = args[args.length - 1]).type === 'Identifier' && arg.name === 'undefined') args.pop()
    }

    let err: string
    if (err = method.validate(parameters)) {
      throw new Error(`${me}: ${err}`)
    }

    return args
  }

  protected Identifier(expr: AST, context: Context): AST {
    if (context.arguments) {
      return b.literal(expr.name)
    }
    else if (expr.type !== 'Identifier') {
      return expr
    }
    else if (expr.name.match(/^(auth|edtr|editors)[a-zA-Z]*$/)) {
      return b.callExpression(b.identifier(expr.name), [])
    }<commit_msg>jsformatter<commit_after>/* eslint-disable @typescript-eslint/no-unsafe-return */

import * as types from '../../gen/items/items'
import * as recast from 'recast'
import { builders as b } from 'ast-types'

type AST = any

import api from '../../gen/api/key-formatter.json'
// move this upgrade to setup/extract-api after migration
const object_or_null = { oneOf: [ { type: 'object' }, { type: 'null' } ] }
const basics = {
  loc: object_or_null,
  comments: object_or_null,
  regex: object_or_null,
}
function upgrade(type) {
  switch (type.type) {
    case 'string':
    case 'number':
    case 'boolean':
      return {
        type: 'object',
        properties: {
          type: { const: 'Literal' },
          value: { type: type.type },
          raw: { type: 'string' },
          ...basics,
        },
        required: [ 'type', 'value' ],
        additionalProperties: false,
      }

    case 'array':
      return {
        type: 'object',
        properties: {
          type: { const: 'ArrayExpression' },
          elements: { type: 'array', items: upgrade(type.items) },
          ...basics,
        },
        required: [ 'type', 'elements' ],
        additionalProperties: false,
      }
  }

  if (typeof type.const !== 'undefined') {
    return {
      type: 'object',
      properties: {
        type: { const: 'Literal' },
        value: { const: type.const },
        raw: { type: 'string' },
        ...basics,
      },
      required: [ 'type', 'value' ],
      additionalProperties: false,
    }
  }

  if (type.instanceof === 'RegExp') {
    return {
      type: 'object',
      properties: {
        type: { const: 'Literal' },
        value: { type: 'object' },
        raw: { type: 'string' },
        ...basics,
        regex: {
          type: 'object',
          properties: {
            pattern: { type: 'string' },
            flags: { type: 'string' },
            ...basics,
          },
          required: [ 'pattern', 'flags' ],
          additionalProperties: false,
        },
      },
      required: [ 'type', 'regex' ],
      additionalProperties: false,
    }
  }

  if (type.oneOf) {
    return { oneOf: type.oneOf.map(t => upgrade(t)) }
  }

  if (type.anyOf) {
    return { anyOf: type.anyOf.map(t => upgrade(t)) }
  }

  throw { notUpgradable: type } // eslint-disable-line no-throw-literal
}
const astapi: typeof api = JSON.parse(JSON.stringify(api))
for (const meta of Object.values(astapi)) {
  for (const property of Object.keys(meta.schema.properties)) {
    meta.schema.properties[property] = upgrade(meta.schema.properties[property])
  }
}

const methodnames: Record<string, string> = Object.keys(astapi).reduce((acc, name) => { acc[name.toLowerCase()]=name; return acc }, {} as Record<string, string>)
function findMethod(fname: string): string {
  const uscore = fname.replace(/[a-z][A-Z]/g, chr => `${chr[0]}_${chr[1]}`).toLowerCase()
  const duscore = fname.replace(/[a-z][A-Z]/g, chr => `${chr[0]}__${chr[1]}`).toLowerCase()
  for (const prefix of ['', '$', '_']) {
    for (let name of [uscore, duscore]) {
      if (name = methodnames[prefix + name]) return name
    }
  }
  return ''
}

import { validator } from '../ajv'
for (const method of Object.values(astapi)) {
  (method  as any).validate = validator((method  as any).schema)
}

for (const fname in astapi) {
  if (fname[0] !== '_' && fname[0] !== '$') throw new Error(`Unexpected fname ${fname}`)
}

type Context = { arguments?: boolean, coerce?: boolean }
export class PatternParser {
  public code: string
  private finder: AST
  private ftype: string

  constructor(source: string) {
    this.finder = recast.parse('[].find(pattern => { try { return pattern() } catch (err) { if (err.next) return ""; throw err } })')
    this.addpattern(recast.parse(source).program.body[0].expression)
    this.insert(recast.parse('"zotero-" + this.item.id').program.body[0].expression, false)
    this.code = recast.prettyPrint(this.finder, {tabWidth: 2}).code
  }

  private error(expr): void {
    throw new Error(`Unexpected ${expr.type} at ${expr.loc.start.column}`)
  }

  protected Literal(expr: AST, _context: Context): AST {
    return expr
  }

  private resolveArguments(fname: string, args: AST[]): AST[] {
    const method = astapi[findMethod(fname)] // transitional before rename in formatter.ts
    const kind = {$: 'function', _: 'filter'}[fname[0]]
    fname = fname.slice(1)
    const me = `${kind} ${JSON.stringify(fname)}`
    if (!method) throw new Error(`No such ${me}`)

    let named = false
    args.forEach((arg, i) => {
      if (!arg.named_argument) {
        if (named) throw new Error(`${me}: positional argument ${i+1} after named argument`)
        arg.named_argument = method.parameters[i]
      }
      else {
        if (method.rest) throw new Error(`${me}: named argument not supported in rest function`)
        named = true
      }
    })

    let parameters: Record<string, AST> = {}
    args = args.map(arg => {
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      const { named_argument, loc, ...argc } = arg
      if (typeof parameters[named_argument] === 'undefined') {
        parameters[named_argument] = argc
      }
      else {
        throw new Error(`${me}: duplicate argument ${JSON.stringify(named_argument)}`)
      }
      return argc
    })

    if (method.rest) {
      if (method.parameters.length !== 1) throw new Error(`${me}: ...rest method may have only one parameter, got ${method.parameters.join(', ')}`)
      parameters = { [method.rest]: b.arrayExpression(args) }
    }
    else {
      if (method.parameters.length < args.length) throw new Error(`${me}: expected ${method.parameters.length} arguments, got ${args.length}`)

      args = method.parameters.map((param: string) => parameters[param] || b.identifier('undefined'))
      let arg
      while (args.length && (arg = args[args.length - 1]).type === 'Identifier' && arg.name === 'undefined') args.pop()
    }

    let err: string
    if (err = method.validate(parameters)) {
      throw new Error(`${me}: ${err}`)
    }

    return args
  }

  protected Identifier(expr: AST, context: Context): AST {
    if (context.arguments) {
      return b.literal(expr.name)
    }
    else if (expr.type !== 'Identifier') {
      return expr
    }
    else if (expr.name.match(/^(auth|edtr|editors)[a-zA-Z]*$/)) {
      return b.callExpression(b.identifier(expr.name), [])
    }
{'fraction_non_alphanumeric': 0.16739231845747163, 'fraction_numerical': 0.0021769553724148654, 'mean_word_length': 2.666761687571266, 'pattern_counts': {'":': 0, '<': 10, '<?xml version=': 0, '>': 26, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1192040', 'n_tokens_mistral': 4081, 'n_tokens_neox': 4016, 'n_words': 995}