Skip to content

Commit

Permalink
Better type errors for text nodes (#2871)
Browse files Browse the repository at this point in the history
  • Loading branch information
RunDevelopment authored May 18, 2024
1 parent a3e33f4 commit a971d74
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 46 deletions.
100 changes: 60 additions & 40 deletions src/common/types/chainner-builtin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Intrinsic,
NeverType,
NumberPrimitive,
Scope,
StringLiteralType,
StringPrimitive,
StringType,
Expand All @@ -16,14 +17,14 @@ import {
literal,
union,
wrapBinary,
wrapQuaternary,
wrapScopedBinary,
wrapScopedQuaternary,
wrapScopedTernary,
wrapScopedUnary,
wrapTernary,
} from '@chainner/navi';
import path from 'path';
import { ColorJson } from '../common-types';
import { log } from '../log';
import { RRegex } from '../rust-regex';
import { isNotNullish } from '../util';

Expand Down Expand Up @@ -113,12 +114,31 @@ class ReplacementString {
}
}

type ErrorFactor = ((message: string) => StructInstanceType) & { default: StructInstanceType };
const createError = (scope: Scope): ErrorFactor => {
const errorDesc = getStructDescriptor(scope, 'Error');

const factory = (message: string) => {
return createInstance(errorDesc, {
message: literal(message),
});
};

const result = factory as ErrorFactor;
result.default = errorDesc.default;

return result;
};

export const formatTextPattern = (
scope: Scope,
pattern: Arg<StringPrimitive>,
...args: Arg<StringPrimitive | StructType>[]
): Arg<StringPrimitive> => {
): Arg<StringPrimitive | StructInstanceType> => {
if (pattern.type === 'never') return NeverType.instance;

const newError = createError(scope);

const argsMap = new Map<string, Arg<StringPrimitive>>();
for (const [arg, name] of args.map((a, i) => [a, String(i + 1)] as const)) {
if (arg.type === 'never') {
Expand All @@ -130,14 +150,13 @@ export const formatTextPattern = (
}
}

if (pattern.type !== 'literal') return StringType.instance;
if (pattern.type !== 'literal') return union(StringType.instance, newError.default);

let parsed;
try {
parsed = new ReplacementString(pattern.value);
} catch {
// Invalid pattern
return NeverType.instance;
} catch (e) {
return newError(String(e));
}

const concatArgs: Arg<StringPrimitive>[] = [];
Expand All @@ -148,7 +167,7 @@ export const formatTextPattern = (
const arg = argsMap.get(token.name);
if (arg === undefined) {
// invalid reference
return NeverType.instance;
return newError(`Invalid reference {${token.name}} in pattern.`);
}
concatArgs.push(arg);
}
Expand All @@ -157,6 +176,15 @@ export const formatTextPattern = (
return Intrinsic.concat(...concatArgs);
};

const formatRRegexError = (error: unknown): string => {
let s = String(error);
const m = /\berror: (.+)$/.exec(s);
if (m) {
// eslint-disable-next-line prefer-destructuring
s = m[1];
}
return `Invalid regex: ${s}`;
};
const validateReplacementForRegex = (regex: RRegex, replacement: ReplacementString): void => {
// check replacement keys
const availableNames = new Set<string>([
Expand Down Expand Up @@ -210,20 +238,21 @@ const regexReplaceImpl = (

return result;
};
export const regexReplace = wrapQuaternary<
export const regexReplace = wrapScopedQuaternary<
StringPrimitive,
StringPrimitive,
StringPrimitive,
NumberPrimitive,
StringPrimitive
>((text, regexPattern, replacementPattern, count) => {
StringPrimitive | StructInstanceType
>((scope, text, regexPattern, replacementPattern, count) => {
const newError = createError(scope);

let regex;
if (regexPattern.type === 'literal') {
try {
regex = new RRegex(regexPattern.value);
} catch (error) {
log.debug('regexReplaceImpl', error);
return NeverType.instance;
return newError(formatRRegexError(error));
}
}

Expand All @@ -232,17 +261,15 @@ export const regexReplace = wrapQuaternary<
try {
replacement = new ReplacementString(replacementPattern.value);
} catch (error) {
log.debug('regexReplaceImpl', error);
return NeverType.instance;
return newError(String(error));
}
}

if (regex && replacement) {
try {
validateReplacementForRegex(regex, replacement);
} catch (error) {
log.debug('regexReplaceImpl', error);
return NeverType.instance;
return newError(String(error));
}
}

Expand All @@ -251,11 +278,10 @@ export const regexReplace = wrapQuaternary<
const result = regexReplaceImpl(text.value, regex, replacement, count.value);
return new StringLiteralType(result);
} catch (error) {
log.debug('regexReplaceImpl', error);
return NeverType.instance;
return newError(String(error));
}
}
return StringType.instance;
return union(StringType.instance, newError.default);
});
export const regexFindImpl = (
text: string,
Expand All @@ -272,19 +298,20 @@ export const regexFindImpl = (
Object.entries(match.name).forEach(([name, m]) => replacements.set(name, m.value));
return replacement.replace(replacements);
};
export const regexFind = wrapTernary<
export const regexFind = wrapScopedTernary<
StringPrimitive,
StringPrimitive,
StringPrimitive,
StringPrimitive
>((text, regexPattern, replacementPattern) => {
StringPrimitive | StructInstanceType
>((scope, text, regexPattern, replacementPattern) => {
const newError = createError(scope);

let regex;
if (regexPattern.type === 'literal') {
try {
regex = new RRegex(regexPattern.value);
} catch (error) {
log.debug('regexReplaceImpl', error);
return NeverType.instance;
return newError(formatRRegexError(error));
}
}

Expand All @@ -293,17 +320,15 @@ export const regexFind = wrapTernary<
try {
replacement = new ReplacementString(replacementPattern.value);
} catch (error) {
log.debug('regexReplaceImpl', error);
return NeverType.instance;
return newError(String(error));
}
}

if (regex && replacement) {
try {
validateReplacementForRegex(regex, replacement);
} catch (error) {
log.debug('regexReplaceImpl', error);
return NeverType.instance;
return newError(String(error));
}
}

Expand All @@ -312,11 +337,10 @@ export const regexFind = wrapTernary<
const result = regexFindImpl(text.value, regex, replacement);
return new StringLiteralType(result);
} catch (error) {
log.debug('regexReplaceImpl', error);
return NeverType.instance;
return newError(String(error));
}
}
return StringType.instance;
return union(StringType.instance, newError.default);
});

// Python-conform padding implementations.
Expand Down Expand Up @@ -517,15 +541,13 @@ export const goIntoDirectory = wrapScopedBinary(
basePath: StringPrimitive,
relPath: StringPrimitive
): Arg<StringPrimitive | StructInstanceType> => {
const errorDesc = getStructDescriptor(scope, 'Error');
const newError = createError(scope);

try {
if (relPath.type === 'literal') {
const error = validateRelPath(relPath.value);
if (error) {
return createInstance(errorDesc, {
message: literal(error),
});
return newError(error);
}

if (basePath.type === 'literal') {
Expand All @@ -534,11 +556,9 @@ export const goIntoDirectory = wrapScopedBinary(
}
}
} catch (e) {
return createInstance(errorDesc, {
message: literal(String(e)),
});
return newError(String(e));
}

return union(StringType.instance, errorDesc.default);
return union(StringType.instance, newError.default);
}
);
12 changes: 6 additions & 6 deletions src/common/types/chainner-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@ struct SplitFilePath {
ext: string,
}
intrinsic def formatPattern(pattern: string, ...args: string | null): string;
intrinsic def regexReplace(text: string, regex: string, replacement: string, count: uint | inf): string;
intrinsic def regexFind(text: string, regex: string, pattern: string): string;
intrinsic def formatPattern(pattern: string, ...args: string | null): string | Error;
intrinsic def regexReplace(text: string, regex: string, replacement: string, count: uint | inf): string | Error;
intrinsic def regexFind(text: string, regex: string, pattern: string): string | Error;
intrinsic def padStart(text: string, width: uint, padding: string): string;
intrinsic def padEnd(text: string, width: uint, padding: string): string;
intrinsic def padCenter(text: string, width: uint, padding: string): string;
Expand All @@ -135,9 +135,9 @@ export const getChainnerScope = lazy((): Scope => {
const builder = new ScopeBuilder('Chainner scope', globalScope);

const intrinsic: Record<string, (scope: Scope, ...args: NeverType[]) => Type> = {
formatPattern: makeScoped(formatTextPattern),
regexReplace: makeScoped(regexReplace),
regexFind: makeScoped(regexFind),
formatPattern: formatTextPattern,
regexReplace,
regexFind,
padStart: makeScoped(padStart),
padEnd: makeScoped(padEnd),
padCenter: makeScoped(padCenter),
Expand Down

0 comments on commit a971d74

Please sign in to comment.