From 121785bc311ef74075e15fe902c8026d47dce2ca Mon Sep 17 00:00:00 2001 From: Erik Pukinskis Date: Mon, 13 Feb 2023 22:49:53 -0700 Subject: [PATCH] Fix second half of type errors. Only macro errors remaining --- .../fixtures/config/configurable.macro.ts | 2 +- src/__tests__/fixtures/emotion.macro.ts | 23 ++-- src/__tests__/fixtures/error-thrower.macro.ts | 8 +- src/__tests__/fixtures/eval.macro.ts | 112 +++++++++++++----- src/__tests__/fixtures/jsx-id-prefix.macro.ts | 32 +++-- .../fixtures/jsx-id-prefix.plugin.ts | 16 ++- src/__tests__/fixtures/keep-imports.macro.ts | 8 +- .../fixtures/macro-error-thrower.macro.ts | 8 +- .../path-replace-issue/variable-assignment.ts | 6 +- .../primitive-config/configurable.macro.ts | 15 +-- src/__tests__/index.ts | 45 +++++-- src/index.ts | 98 +++++++++------ 12 files changed, 247 insertions(+), 126 deletions(-) diff --git a/src/__tests__/fixtures/config/configurable.macro.ts b/src/__tests__/fixtures/config/configurable.macro.ts index 5fefefc..5cd2d0c 100644 --- a/src/__tests__/fixtures/config/configurable.macro.ts +++ b/src/__tests__/fixtures/config/configurable.macro.ts @@ -4,4 +4,4 @@ import {createMacro} from '../../..' export const configName = 'configurableMacro' export const realMacro = jest.fn() -export default createMacro(realMacro, ({configName} = {})) +export default createMacro(realMacro, {configName}) diff --git a/src/__tests__/fixtures/emotion.macro.ts b/src/__tests__/fixtures/emotion.macro.ts index 472fdd8..7d666ac 100644 --- a/src/__tests__/fixtures/emotion.macro.ts +++ b/src/__tests__/fixtures/emotion.macro.ts @@ -2,20 +2,25 @@ // const printAST = require('ast-pretty-print') import {createMacro} from '../../' -module.exports = createMacro(emotionMacro) - -function emotionMacro({references, babel}) { +module.exports = createMacro(function emotionMacro({references, babel}) { const {types: t} = babel references.css.forEach(cssRef => { - if (cssRef.parentPath.type === 'TaggedTemplateExpression') { - cssRef.parentPath.replaceWith( - t.stringLiteral(cssRef.parentPath.get('quasi').evaluate().value.trim()), - ) + if (cssRef.parentPath?.type === 'TaggedTemplateExpression') { + const path = cssRef.parentPath.get('quasi') + if (Array.isArray(path)) { + throw new Error("Don't know how to handle this situation") + } + const str = path.evaluate().value.trim() + + cssRef.parentPath.replaceWith(t.stringLiteral(str)) } }) references.styled.forEach(styledRef => { - if (styledRef.parentPath.parentPath.type === 'TaggedTemplateExpression') { + if (styledRef.parentPath?.parentPath?.type === 'TaggedTemplateExpression') { const quasi = styledRef.parentPath.parentPath.get('quasi') + if (Array.isArray(quasi)) { + throw new Error('Not expecting array') + } const val = quasi.evaluate().value.trim() const replacement = t.templateLiteral( [t.templateElement({raw: val, cooked: val})], @@ -24,4 +29,4 @@ function emotionMacro({references, babel}) { quasi.replaceWith(replacement) } }) -} +}) diff --git a/src/__tests__/fixtures/error-thrower.macro.ts b/src/__tests__/fixtures/error-thrower.macro.ts index 4662788..39f13b7 100644 --- a/src/__tests__/fixtures/error-thrower.macro.ts +++ b/src/__tests__/fixtures/error-thrower.macro.ts @@ -1,8 +1,6 @@ // const printAST = require('ast-pretty-print') -const {createMacro} = require('../../') +import {createMacro} from '../../' -module.exports = createMacro(evalMacro) - -function evalMacro() { +module.exports = createMacro(function evalMacro() { throw new Error('very unhelpful') -} +}) diff --git a/src/__tests__/fixtures/eval.macro.ts b/src/__tests__/fixtures/eval.macro.ts index 6ef978c..ba29245 100644 --- a/src/__tests__/fixtures/eval.macro.ts +++ b/src/__tests__/fixtures/eval.macro.ts @@ -1,54 +1,106 @@ -const {parse} = require('@babel/parser') +import {parse} from '@babel/parser' +import {Node, NodePath} from '@babel/traverse' +import {Expression, Statement, VariableDeclaration} from '@babel/types' // const printAST = require('ast-pretty-print') -const {createMacro} = require('../../') +import {createMacro} from '../../' -module.exports = createMacro(evalMacro) - -function evalMacro({references, state}) { +export default createMacro(function evalMacro({references, state}) { references.default.forEach(referencePath => { - if (referencePath.parentPath.type === 'TaggedTemplateExpression') { - asTag(referencePath.parentPath.get('quasi'), state) - } else if (referencePath.parentPath.type === 'CallExpression') { - asFunction(referencePath.parentPath.get('arguments'), state) - } else if (referencePath.parentPath.type === 'JSXOpeningElement') { - asJSX( - { - attributes: referencePath.parentPath.get('attributes'), - children: referencePath.parentPath.parentPath.get('children'), - }, - state, - ) + if (referencePath.parentPath?.type === 'TaggedTemplateExpression') { + asTag(referencePath.parentPath?.get('quasi')) + } else if (referencePath.parentPath?.type === 'CallExpression') { + const args = referencePath.parentPath?.get('arguments') + if (!Array.isArray(args)) { + throw new Error('Was expecting array') + } + asFunction(args) + } else if (referencePath.parentPath?.type === 'JSXOpeningElement') { + asJSX({ + attributes: referencePath.parentPath?.get('attributes'), + children: referencePath.parentPath?.parentPath?.get('children'), + }) } else { // TODO: throw a helpful error message } }) -} +}) + +function asTag(quasiPath: NodePath | NodePath[]) { + if (Array.isArray(quasiPath)) { + throw new Error("Don't know how to handle arrays") + } + + const parentQuasi = quasiPath.parentPath?.get('quasi') -function asTag(quasiPath) { - const value = quasiPath.parentPath.get('quasi').evaluate().value - quasiPath.parentPath.replaceWith(evalToAST(value)) + if (!parentQuasi) { + throw new Error('No quasi path on parent') + } + + if (Array.isArray(parentQuasi)) { + throw new Error("Don't know how to handle arrays") + } + const value = parentQuasi.evaluate().value + quasiPath.parentPath?.replaceWith(evalToAST(value)) } -function asFunction(argumentsPaths) { +function asFunction(argumentsPaths: NodePath[]) { const value = argumentsPaths[0].evaluate().value - argumentsPaths[0].parentPath.replaceWith(evalToAST(value)) + argumentsPaths[0].parentPath?.replaceWith(evalToAST(value)) +} + +type NodeWithValue = Node & { + value: any +} + +function isNodeWithValue(node: Node): node is NodeWithValue { + return Object.prototype.hasOwnProperty.call(node, 'value') } // eslint-disable-next-line no-unused-vars -function asJSX({attributes, children}) { +function asJSX({ + attributes, + children, +}: { + attributes: NodePath | NodePath[] + children: NodePath | NodePath[] | undefined +}) { // It's a shame you cannot use evaluate() with JSX - const value = children[0].node.value - children[0].parentPath.replaceWith(evalToAST(value)) + if (!Array.isArray(children)) { + throw new Error("Don't know how to handle single children") + } + const firstChild = children[0] + if (!isNodeWithValue(firstChild.node)) { + throw new Error("Don't know to handle nodes without values") + } + const value = firstChild.node.value + firstChild.parentPath?.replaceWith(evalToAST(value)) } -function evalToAST(value) { - let x +function evalToAST(value: Expression | null | undefined): Expression { + let x: Record = {} // eslint-disable-next-line eval(`x = ${value}`) return thingToAST(x) } -function thingToAST(object) { +function isVariableDeclaration( + statement: Statement, +): statement is VariableDeclaration { + return statement.type === 'VariableDeclaration' +} + +function thingToAST(object: Record) { const fileNode = parse(`var x = ${JSON.stringify(object)}`) - return fileNode.program.body[0].declarations[0].init + const firstStatement = fileNode.program.body[0] + + if (!isVariableDeclaration(firstStatement)) { + throw new Error('Only know how to handle VariableDeclarations') + } + + const initDeclaration = firstStatement.declarations[0].init + + if (!initDeclaration) { + throw new Error('Was expecting expression') + } + return initDeclaration } diff --git a/src/__tests__/fixtures/jsx-id-prefix.macro.ts b/src/__tests__/fixtures/jsx-id-prefix.macro.ts index f15bd64..db2a8be 100644 --- a/src/__tests__/fixtures/jsx-id-prefix.macro.ts +++ b/src/__tests__/fixtures/jsx-id-prefix.macro.ts @@ -1,20 +1,38 @@ // adds "prefix-" to each `id` attribute -const {createMacro} = require('../../') +import {createMacro} from '../../' +import { + JSXElement, + JSXExpressionContainer, + JSXFragment, + StringLiteral, +} from '@babel/types' -module.exports = createMacro(wrapWidget) - -function wrapWidget({references, babel}) { +module.exports = createMacro(function wrapWidget({references, babel}) { const {types: t} = babel references.default.forEach(wrap => { - wrap.parentPath.traverse({ + wrap.parentPath?.traverse({ JSXAttribute(path) { const name = path.get('name') if (t.isJSXIdentifier(name) && name.node.name === 'id') { const value = path.get('value') - if (t.isStringLiteral(value)) - value.replaceWith(t.stringLiteral(`macro-${value.node.value}`)) + if (isStringLiteral(value.node)) { + value.replaceWith(t.stringLiteral(`macro-${value.node?.value}`)) + } } }, }) }) +}) + +function isStringLiteral( + node: + | JSXElement + | JSXExpressionContainer + | JSXFragment + | StringLiteral + | null + | undefined, +): node is StringLiteral { + if (!node) return false + return node.type === 'StringLiteral' } diff --git a/src/__tests__/fixtures/jsx-id-prefix.plugin.ts b/src/__tests__/fixtures/jsx-id-prefix.plugin.ts index 5d12410..73d0110 100644 --- a/src/__tests__/fixtures/jsx-id-prefix.plugin.ts +++ b/src/__tests__/fixtures/jsx-id-prefix.plugin.ts @@ -1,19 +1,23 @@ // babel-plugin adding `plugin-` prefix to each "id" JSX attribute -module.exports = main +import {NodePath} from '@babel/core' +import {BabelType} from 'babel-plugin-tester' -function main({types: t}) { +export default function main({types: t}: BabelType) { return { visitor: { // intentionally traversing from Program, // if it matches JSXAttribute here the issue won't be reproduced - Program(progPath) { + Program(progPath: NodePath) { progPath.traverse({ - JSXAttribute(path) { + JSXAttribute(path: NodePath) { const name = path.get('name') - if (t.isJSXIdentifier(name) && name.node.name === 'id') { + if (t.isJSXIdentifier(name) && name.name === 'id') { /// DANGER! CODE CHANGE! const value = path.get('value') + if (Array.isArray(value)) { + throw new Error("Value path is an array. Don't know how to handle this") + } if (t.isStringLiteral(value)) - value.replaceWith(t.stringLiteral(`plugin-${value.node.value}`)) + value.replaceWith(t.stringLiteral(`plugin-${value.value}`)) /// DANGER! CODE CHANGE! } }, }) diff --git a/src/__tests__/fixtures/keep-imports.macro.ts b/src/__tests__/fixtures/keep-imports.macro.ts index 73480a0..3a107b5 100644 --- a/src/__tests__/fixtures/keep-imports.macro.ts +++ b/src/__tests__/fixtures/keep-imports.macro.ts @@ -1,7 +1,5 @@ -const {createMacro} = require('../../') +import {createMacro} from '../../' -module.exports = createMacro(keepImportMacro) - -function keepImportMacro() { +export default createMacro(function keepImportMacro() { return {keepImports: true} -} +}) diff --git a/src/__tests__/fixtures/macro-error-thrower.macro.ts b/src/__tests__/fixtures/macro-error-thrower.macro.ts index 426aa52..cbcf397 100644 --- a/src/__tests__/fixtures/macro-error-thrower.macro.ts +++ b/src/__tests__/fixtures/macro-error-thrower.macro.ts @@ -1,8 +1,6 @@ // const printAST = require('ast-pretty-print') -const {createMacro, MacroError} = require('../../') +import {createMacro, MacroError} from '../../' -module.exports = createMacro(evalMacro) - -function evalMacro() { +export default createMacro(function evalMacro() { throw new MacroError('very helpful') -} +}) diff --git a/src/__tests__/fixtures/path-replace-issue/variable-assignment.ts b/src/__tests__/fixtures/path-replace-issue/variable-assignment.ts index 97f9c01..3390c41 100644 --- a/src/__tests__/fixtures/path-replace-issue/variable-assignment.ts +++ b/src/__tests__/fixtures/path-replace-issue/variable-assignment.ts @@ -1,5 +1,9 @@ import myEval from '../eval.macro' -const result = myEval`+('4' + '2')` +const result = myEval`+('4' + '2')` as number + +declare global { + var result: number; + } global.result = result diff --git a/src/__tests__/fixtures/primitive-config/configurable.macro.ts b/src/__tests__/fixtures/primitive-config/configurable.macro.ts index 8836c5b..fcde319 100644 --- a/src/__tests__/fixtures/primitive-config/configurable.macro.ts +++ b/src/__tests__/fixtures/primitive-config/configurable.macro.ts @@ -1,10 +1,7 @@ -const {createMacro} = require('../../..') +import { createMacro } from '../../..' -const configName = 'configurableMacro' -const realMacro = jest.fn() -module.exports = createMacro(realMacro, {configName}) -// for testing purposes only -Object.assign(module.exports, { - realMacro, - configName, -}) +// exports for testing purposes only: +export const configName = 'configurableMacro' +export const realMacro = jest.fn() + +export default createMacro(realMacro, { configName }) diff --git a/src/__tests__/index.ts b/src/__tests__/index.ts index d8f84e8..6fbdf96 100644 --- a/src/__tests__/index.ts +++ b/src/__tests__/index.ts @@ -1,19 +1,29 @@ import path from 'path' -import {cosmiconfigSync as cosmiconfigSyncMock} from 'cosmiconfig' +import {cosmiconfigSync} from 'cosmiconfig' +import * as CosmicConfig from 'cosmiconfig' import cpy from 'cpy' import babel from '@babel/core' import pluginTester from 'babel-plugin-tester' import plugin from '../' +import {expect, jest} from '@jest/globals'; + +const cosmiconfigSyncMock: ComsicConfigSyncMock = cosmiconfigSync const projectRoot = path.join(__dirname, '../../') +type ComsicConfigSyncMock = typeof cosmiconfigSync & { + explorer?: ReturnType +} + jest.mock('cosmiconfig', () => { - const cosmiconfigExports = jest.requireActual('cosmiconfig') - const actualCosmiconfigSync = cosmiconfigExports.cosmiconfigSync - function fakeCosmiconfigSync(...args) { + const cosmiconfigExports = jest.requireActual('cosmiconfig') as typeof CosmicConfig + const actualCosmiconfigSync = cosmiconfigSync + + const fakeCosmiconfigSync: ComsicConfigSyncMock = (...args: Parameters) => { fakeCosmiconfigSync.explorer = actualCosmiconfigSync(...args) return fakeCosmiconfigSync.explorer } + return {...cosmiconfigExports, cosmiconfigSync: fakeCosmiconfigSync} }) @@ -31,13 +41,15 @@ beforeEach(() => { jest.spyOn(console, 'error').mockImplementation(() => {}) }) +type MockedConsoleError = jest.MockedFunction + afterEach(() => { - console.error.mockRestore() + (console.error as MockedConsoleError).mockRestore() jest.clearAllMocks() }) expect.addSnapshotSerializer({ - print(val) { + print(val: string) { return ( val .split(projectRoot) @@ -47,7 +59,7 @@ expect.addSnapshotSerializer({ .replace(/Error:[^:]*:/, 'Error:') ) }, - test(val) { + test(val: unknown) { return typeof val === 'string' }, }) @@ -60,7 +72,7 @@ pluginTester({ parserOpts: { plugins: ['jsx'], }, - generatorOpts: {quotes: 'double'}, + generatorOpts: { jsescOption: { quotes: 'double'}}, }, tests: [ { @@ -322,6 +334,9 @@ pluginTester({ error: true, fixture: path.join(__dirname, 'fixtures/config/code.js'), setup() { + if (!cosmiconfigSyncMock.explorer) { + throw new Error('No explorer, was cosmiconfigSync supposed to have been called?') + } jest .spyOn(cosmiconfigSyncMock.explorer, 'search') .mockImplementationOnce(() => { @@ -331,11 +346,12 @@ pluginTester({ return function teardown() { try { expect(console.error).toHaveBeenCalledTimes(1) - expect(console.error.mock.calls[0]).toMatchSnapshot() - console.error.mockClear() + const firstCall = (console.error as MockedConsoleError).mock.calls[0] + expect(firstCall).toMatchSnapshot() + ;(console.error as MockedConsoleError).mockClear() } catch (e) { console.error(e) - console.error.mockClear() + ;(console.error as MockedConsoleError).mockClear() throw e } } @@ -345,6 +361,9 @@ pluginTester({ title: 'when there is no config to load, then no config is passed', fixture: path.join(__dirname, 'fixtures/config/code.js'), setup() { + if (!cosmiconfigSyncMock.explorer) { + throw new Error('No explorer, was cosmiconfigSync supposed to have been called?') + } jest .spyOn(cosmiconfigSyncMock.explorer, 'search') .mockImplementationOnce(() => { @@ -440,7 +459,7 @@ pluginTester({ { title: 'when a custom isMacrosName option is used on a import', pluginOptions: { - isMacrosName(v) { + isMacrosName(v: string) { return v.endsWith('-macro.js') }, }, @@ -452,7 +471,7 @@ pluginTester({ { title: 'when a custom isMacrosName option is used on a require', pluginOptions: { - isMacrosName(v) { + isMacrosName(v: string) { return v.endsWith('-macro.js') }, }, diff --git a/src/index.ts b/src/index.ts index 3511eb8..a63257a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,6 +45,12 @@ import { LVal, ImportDeclaration, } from '@babel/types' +import * as BabelCoreNamespace from '@babel/core'; +import * as BabelTypesNamespace from '@babel/types' +import type { PluginObj, PluginPass } from '@babel/core' + +type BabelCore = typeof BabelCoreNamespace +type BabelTypes = typeof BabelTypesNamespace const macrosRegex = /[./]macro(\.c?js)?$/ const testMacrosRegex = (v: string) => macrosRegex.test(v) @@ -63,7 +69,7 @@ export class MacroError extends Error { } } -type MacroWrapperArgs = Record & { +type MacroFunctionArgs = Record & { source: string isBabelMacrosCall?: boolean } @@ -98,12 +104,34 @@ function loadCosmicConfig(): CosmicConfig { return out } +export type CreateMacroFunctionArgs = { + babel: BabelCore + references: Record & {default: NodePath[]} + state: unknown +} + type CreateMacroOptions = { configName: string } +type MacroWrapperOptionsArgs = { + source: string, + isBabelMacrosCall?: boolean + babel: BabelCore + references: Record & {default: NodePath[]} + state: PluginPass +} + +type MacroWrapperTemplateArgs = TemplateStringsArray + +type MacroWrapperArgs = MacroWrapperOptionsArgs | MacroWrapperTemplateArgs + +function isTemplateLiteralArgs(args: MacroWrapperArgs): args is MacroWrapperTemplateArgs { + return Array.isArray(args) +} + export function createMacro( - macro: (args: MacroWrapperArgs) => unknown, + macro: (args: CreateMacroFunctionArgs) => unknown, options: Partial = {}, ) { if (options.configName === 'options') { @@ -115,7 +143,10 @@ export function createMacro( macroWrapper.options = options return macroWrapper - function macroWrapper(args: MacroWrapperArgs) { + function macroWrapper(args: MacroWrapperOptionsArgs) { + if (isTemplateLiteralArgs(args)) { + throw new Error("Not expecting args to be a template array, even though that's a thing in the tests?") + } const {source, isBabelMacrosCall} = args if (!isBabelMacrosCall) { throw new MacroError( @@ -139,23 +170,16 @@ function nodeResolvePath(source: string, basedir: string) { }) } -type ProgramState = { - file: { - opts: {filename?: string} - scope: { - path: NodePath - } - } -} - -type MacrosPluginOptions = { +type MacrosPluginOptions = Record & { require?: NodeRequire resolvePath?(source: string, basedir: string): string isMacrosName?(v: string): boolean } -export function macrosPlugin( - babel: unknown, +export type Babel = typeof BabelCoreNamespace; + +export default function macrosPlugin( + babel: Babel, // istanbul doesn't like the default of an object for the plugin options // but I think older versions of babel didn't always pass options // istanbul ignore next @@ -165,7 +189,7 @@ export function macrosPlugin( isMacrosName = testMacrosRegex, ...options }: MacrosPluginOptions = {}, -) { +): PluginObj { function interopRequire(path: string) { // eslint-disable-next-line import/no-dynamic-require const o = _require(path) @@ -175,7 +199,7 @@ export function macrosPlugin( return { name: 'macros', visitor: { - Program(progPath: NodePath, state: ProgramState) { + Program: (progPath, state) => { progPath.traverse({ ImportDeclaration(path) { const isMacros = looksLike(path, { @@ -374,25 +398,30 @@ function isStringLiteral( type MacroRequireFunctionOptions = { references: Record[]> source: string - state: ProgramState - babel: unknown + state: PluginPass + babel: BabelCore config: unknown isBabelMacrosCall: true } type InteropRequireFunction = (( arg: MacroRequireFunctionOptions, -) => ApplyMacrosResult) & {isBabelMacro: boolean} +) => ApplyMacrosResult) & { + isBabelMacro: boolean, + options: { + configName?: string + } +} type ApplyMacrosOptions = { path: NodePath imports: Import | Import[] | undefined source: string - state: ProgramState - babel: unknown + state: PluginPass + babel: BabelCore interopRequire(path: string): InteropRequireFunction resolvePath(source: string, basedir: string): string - options: {} + options: Record } type ApplyMacrosResult = @@ -421,7 +450,7 @@ function applyMacros({ /* istanbul ignore next (pretty much only useful for astexplorer I think) */ const { file: { - opts: {filename = ''}, + opts: { filename }, }, } = state @@ -441,7 +470,7 @@ function applyMacros({ ) const isRelative = source.indexOf('.') === 0 - const requirePath = resolvePath(source, p.dirname(getFullFilename(filename))) + const requirePath = resolvePath(source, p.dirname(getFullFilename(filename ?? ''))) const macro = interopRequire(requirePath) if (!macro.isBabelMacro) { @@ -498,7 +527,7 @@ function applyMacros({ return result } -function getConfigFromFile(configName, filename) { +function getConfigFromFile(configName: string, filename: string) { try { const loaded = getConfigExplorer().search(filename) @@ -514,7 +543,7 @@ function getConfigFromFile(configName, filename) { return {} } -function getConfigFromOptions(configName, options) { +function getConfigFromOptions(configName: string, options: Record) { if (options.hasOwnProperty(configName)) { if (options[configName] && typeof options[configName] !== 'object') { // eslint-disable-next-line no-console @@ -522,16 +551,16 @@ function getConfigFromOptions(configName, options) { `The macro plugin options' ${configName} property was not an object or null.`, ) } else { - return {options: options[configName]} + return {options: options[configName] as {}} } } return {} } -function getConfig(macro, filename, source, options) { +function getConfig(macro: InteropRequireFunction, filename: string | null | undefined, source: string, options: Record) { const {configName} = macro.options if (configName) { - const fileConfig = getConfigFromFile(configName, filename) + const fileConfig = getConfigFromFile(configName, filename ?? "") const optionsConfig = getConfigFromOptions(configName, options) if ( @@ -581,8 +610,7 @@ function getFullFilename(filename: string) { return p.join(process.cwd(), filename) } -type Val = Record -function looksLike(a: Val, b: Val): boolean { +function looksLike(a: any, b: any): boolean { return ( a && b && @@ -592,14 +620,14 @@ function looksLike(a: Val, b: Val): boolean { if (typeof bVal === 'function') { return bVal(aVal) } - return isPrimitive(bVal as Val) + return isPrimitive(bVal) ? bVal === aVal - : looksLike(aVal as Val, bVal as Val) + : looksLike(aVal, bVal) }) ) } -function isPrimitive(val: Val) { +function isPrimitive(val: unknown) { // eslint-disable-next-line return val == null || /^[sbn]/.test(typeof val) }