Skip to content
This repository has been archived by the owner on Jul 5, 2024. It is now read-only.

Commit

Permalink
implement test framework (and more changes i forgot)
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinramharak committed Feb 27, 2021
1 parent 04216b9 commit 9f1193c
Show file tree
Hide file tree
Showing 25 changed files with 528 additions and 333 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
node_modules/
tmp/
build/
lib/
6 changes: 3 additions & 3 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
src
tmp
test
src/
build/
test/
.mocharc.json
tsconfig.json
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"description": "Typescript AST transformer to generate type checks at compile time",
"scripts": {
"build": "ttsc",
"test": "mocha"
"test": "mocha --timeout 0"
},
"author": "Kevin Ramharak <[email protected]>",
"repository": {
Expand All @@ -34,13 +34,13 @@
"@types/chai": "^4.2.14",
"@types/mocha": "^8.2.0",
"@types/node": "^14.14.22",
"@typescript/vfs": "^1.3.2",
"chai": "^4.2.0",
"mocha": "^8.2.1",
"ts-expose-internals": "^4.1.3",
"ts-node": "^9.1.1",
"ts-transform-runtime-check": "^0.0.1-alpha19",
"ts-transform-test-compiler": "^1.1.0",
"ttypescript": "^1.5.12",
"typescript": "^4.1.3"
"typescript": "^4.2.2"
}
}
4 changes: 1 addition & 3 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,10 @@ export interface IPublicPackageOptions {
*/
addParenthesis: boolean;
/**
* TODO: implement
* logs a warning if a package function is being used incorrectly
*/
warnOnInvalidUse: boolean;
/**
* TODO: implement
* throws an error if a package function is being used incorrectly
*/
throwOnInvalidUse: boolean;
Expand All @@ -46,7 +44,7 @@ const InternalPackageOptions: IInternalPackageOptions = {
const PackageOptions: IPublicPackageOptions = {
addTypeComment: true,
warnOnInvalidUse: true,
throwOnInvalidUse: false,
throwOnInvalidUse: true,
addParenthesis: true,
}

Expand Down
11 changes: 8 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@

// TODO: missing features
// - like<T>(value: any): typeof value quacks like T
// - createTypeCheck<T>(): (value: any ) => is<T>(value);
// - extends<A>(B: any): B extends A
// - ?<A>(value: B): A extends B
Expand All @@ -9,10 +8,16 @@
// - use json-schema's (at compile time and at runtime?)
// see https://github.com/vega/ts-json-schema-generator for example

class StubError extends Error {
public name = 'StubError'
constructor(fnName: string) {
super(`'${fnName}' is a stub function, calls to this function should have been removed by the transformer plugin`);
}
}

/**
* check if `value` conforms to the runtime type of `T`
*/
export function is<T>(value: unknown): value is T {
// TODO: better error message
throw new TypeError('this function');
throw new StubError('is');
};
46 changes: 46 additions & 0 deletions src/log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@

interface Logger {
debug(...input: unknown[]): void;
log(...input: unknown[]): void;
info(...input: unknown[]): void;
warn(...input: unknown[]): void;
error(...input: unknown[]): void;
assert(condition: boolean, ...input: unknown[]): asserts condition;
}

function noop(...input: unknown[]) {}

const noopLogger: Logger = {
debug: noop,
log: noop,
info: noop,
warn: noop,
error: noop,
assert(condition, ...input) {
if (!condition) {
throw new Error('assertion error: ' + input.join(', '));
}
}
};

let _logger: Logger = noopLogger;

export function useLogger(logger: Logger) {
_logger = logger;
}

export function debug(...input: unknown[]) {
_logger.debug(...input);
}

export function info(...input: unknown[]) {
_logger.info(...input);
}

export function warn(...input: unknown[]) {
_logger.warn(...input);
}

export function error(...input: unknown[]) {
_logger.error(...input);
}
1 change: 0 additions & 1 deletion src/transformer/createContextTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { IPackageOptions, IPublicPackageOptions } from '../config';
import { MarkedTransformer, ShouldTransform } from '../transformers';
import { MarkedVisitor, visitors } from '../visitors';

// TODO: support multipe visitors / transformers for the same note type
// TODO: is recursion needed/possible?
/**
* The difference between a visitor and a transformer is that a visitor will always visit its kind and return.
Expand Down
19 changes: 9 additions & 10 deletions src/transformer/createSourceFileTransformerFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import ts from 'typescript';
import { is } from 'ts-transform-runtime-check';

import { DefaultPackageOptions, IPackageOptions, IPublicPackageOptions } from '../config';
import { MarkedTransformer, ShouldTransform, transformers } from '../transformers';
import { warn } from '../log';
import { createContextTransformer } from './createContextTransformer';
import { noopContextTransformer } from './noop';
import { MarkedTransformer, ShouldTransform, transformers } from '../transformers';

function findSourceFileForModuleSpecifier(program: ts.Program, moduleSpecifier: string) {
// NOTE: this took a long time to figure out, not sure why there isn't a simpler api for this
Expand Down Expand Up @@ -36,7 +37,8 @@ export function createSourceFileTransformerFactory(program: ts.Program, _options
throw new TypeError('invalid configuration object');
}

let packageSymbolTable: ts.SymbolTable | undefined;

let packageSymbolTable: ts.SymbolTable;

// first try and find the package as if it has a normal definition `import { } from 'PACKAGE_MODULE_SPECIFIER'`
const packageSourceFile = findSourceFileForModuleSpecifier(program, options.PackageModuleName);
Expand All @@ -46,7 +48,7 @@ export function createSourceFileTransformerFactory(program: ts.Program, _options
packageSymbolTable = packageSourceFile.symbol.exports;
} else {
// we found the source file, but it has no exports
// TODO: log
warn(`'${options.PackageModuleName}' package was found but has no exports, defaulting to a noop transformer`);
return noopContextTransformer;
}
} else {
Expand All @@ -56,16 +58,13 @@ export function createSourceFileTransformerFactory(program: ts.Program, _options
const moduleSymbol = ambientModules.find(module => module.name === `"${options.PackageModuleName}"`);
if (moduleSymbol && moduleSymbol.exports) {
packageSymbolTable = moduleSymbol.exports;
} else {
// we found the ambient declaration file, but it has no exports
warn(`'${options.PackageModuleName}' ambient declaration was found but has no exports, defaulting to a noop transformer`);
return noopContextTransformer;
}
}

if (!packageSymbolTable) {
// could not find the source file nor the ambient module declaration
// TODO: log
console.warn('no package symbol table found')
return noopContextTransformer;
}

const transformerEntries = [...packageSymbolTable as Map<ts.__String, ts.Symbol>].map(([name, symbol]) => {
const transformer = transformers.find(transformer => transformer.name === name);
if (!transformer) {
Expand Down
11 changes: 7 additions & 4 deletions src/transformers/is.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ const ERROR = {
function createTypeCheckGenerator(checker: ts.TypeChecker, context: ts.TransformationContext) {
const compilerOptions = context.getCompilerOptions();
// useful: https://github.com/microsoft/TypeScript/blob/master/src/compiler/types.ts#L4936
// NOTE: it is (almost) guarenteed that integer indexes are sorted from 0 - n, so any TypeFlags with multiple flags will branch to the lowest bit set
// NOTE: it is (almost) guarenteed that integer indexes are sorted from 0 - n, so any TypeFlags with multiple flags can be impacted by this
// NOTE: TypeFlags.EnumLiteral is always a & with StringLiteral | NumberLiteral
// NOTE: TypeFlags.Enum is not actually used, Enums seems to be represented as unions
// NOTE: TypeFlags.Enum is not actually used, Enum's seem to be represented as unions
/**
* generates the code for `is<{ is.type }>({ node.value }: { node.type })` CallExpressions
*/
Expand Down Expand Up @@ -71,7 +71,7 @@ function createTypeCheckGenerator(checker: ts.TypeChecker, context: ts.Transform
}
});
},
// note: NonPrimitive is the plain `object` type
// NOTE: NonPrimitive is the plain `object` type
// see: https://www.typescriptlang.org/docs/handbook/basic-types.html#object
[ts.TypeFlags.NonPrimitive]() {
return branch(value.type.flags, {
Expand Down Expand Up @@ -374,10 +374,13 @@ function createTypeCheckGenerator(checker: ts.TypeChecker, context: ts.Transform

is.kind = ts.SyntaxKind.CallExpression;

/**
* @param declaration the `is<T>(value: unknown): value is T` declaration
*/
is.createShouldTransform = function createShouldTransform(declaration: ts.Declaration) {
return function shouldTransform(node, checker, context, options) {
const signature = checker.getResolvedSignature(node);
return signature && signature.declaration && signature.declaration=== declaration;
return signature && signature.declaration && signature.declaration === declaration;
}
} as CreateShouldTransform<ts.CallExpression>;

Expand Down
146 changes: 146 additions & 0 deletions test/createEnvironment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@

import ts from 'typescript';
import * as tsvfs from '@typescript/vfs';
import { readFileSync, writeFileSync } from 'fs';

import RuntimeCheck from '../src/transformer';
import { IPackageOptions } from '../src/config';
import { Transform } from 'stream';
import { expect } from 'chai';

export interface FileDiagnostic {
file: string;
line: number;
character: number;
message: string;
}

export interface GeneralDiagnostic {
message: string;
}

export type Diagnostic = FileDiagnostic | GeneralDiagnostic;

type TransformerOptions = Partial<IPackageOptions> & Pick<IPackageOptions, 'PackageModuleName'>;

function formatDiagnostic(diagnostic: Diagnostic = { message: 'stub diagnostic' }) {
return typeof (diagnostic as FileDiagnostic).file === 'string' ? `${(diagnostic as FileDiagnostic).file}:${(diagnostic as FileDiagnostic).line}:${(diagnostic as FileDiagnostic).character} ${diagnostic.message}` : diagnostic.message;
}

export function createEnvironment(options: ts.CompilerOptions, transformerOptions: TransformerOptions) {
const currentDirectory = process.cwd();
const fs = tsvfs.createDefaultMapFromNodeModules(options);
const system = tsvfs.createFSBackedSystem(fs, currentDirectory, ts);

// override the `getCurrentDirectory()`
// https://github.com/microsoft/TypeScript-Website/blob/v2/packages/typescript-vfs/src/index.ts#L432
system.getCurrentDirectory = () => currentDirectory;

const host = tsvfs.createVirtualCompilerHost(system, options, ts).compilerHost;
const typeDefs = readFileSync('./lib/index.d.ts', { encoding: 'utf-8' }).replace(/ declare function /g, ' function ');
writeFileSync('test/types.d.ts', `declare module '${transformerOptions.PackageModuleName}' {\n${typeDefs}\n}`);

const rootNames = ['test/types.d.ts', 'test/values.ts'];

const environment = {
fs,
system,
host,
compileString(input: string, inlineTransformerOptions: Partial<IPackageOptions> = {}) {
inlineTransformerOptions = Object.assign({}, transformerOptions, inlineTransformerOptions);
input = input.trim();
const tempFile = 'test/input.ts';
const template = `
import { is } from "${inlineTransformerOptions.PackageModuleName}";
output: {
${input}
}
`.trim();
fs.set(tempFile, template);
const program = ts.createProgram({
rootNames: [...rootNames, tempFile],
options,
host,
});
const { diagnostics } = environment.emit(program, tempFile, inlineTransformerOptions);
environment.assertNoDiagnostics(diagnostics);
const result = fs.get(tempFile.replace('.ts', '.js'));
if (!result) {
throw new Error(`failed to compile string: '${input}'`);
}
const extractor = /output:\s*{\s*([\s\S]+)\s*}/m
const extracted = result.match(extractor);
if (extracted && extracted[1]) {
const code = extracted[1];
// small hack to strip a trailing semi-colon if it was not present with the input
if (!input.endsWith(';') && code.endsWith(';')) {
return code.slice(0, -1);
}
return code;
}
throw new Error(`
failed to extract contents from compield string.
input:
------------
${input}
------------
output:
------------
${result}
------------
`.trim()
);
},
createProgram(files: string[], compilerOptions: Partial<ts.CompilerOptions> = {}) {
return ts.createProgram({
rootNames: [...rootNames, ...files],
options: Object.assign({}, options, compilerOptions),
host,
});
},
emit(program: ts.Program, target?: string | ts.SourceFile, options: Partial<IPackageOptions> = {}, writeFile: ts.WriteFileCallback = (fileName: string, content: string) => fs.set(fileName, content)) {
if (typeof target === 'string') {
const file = program.getSourceFile(target);
if (file) {
target = file;
} else {
throw new Error(`no source file exits for: '${target}'`);
}
}

const result = program.emit(target, writeFile, void 0, void 0, {
before: [
RuntimeCheck(program, Object.assign({}, transformerOptions, options)),
],
});

const diagnostics: Diagnostic[] = [...ts.getPreEmitDiagnostics(program), ...result.diagnostics].map(diagnostic => {
if (diagnostic.file) {
const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!);
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
return {
file: diagnostic.file.fileName,
line,
character,
message,
};
} else {
return {
message: diagnostic.messageText as string,
};
}
});

return {
diagnostics,
};
},
assertNoDiagnostics(diagnostics: Diagnostic[]) {
expect(diagnostics.length).to.equal(0, formatDiagnostic(diagnostics[0]));
},
};

return environment;
}
21 changes: 21 additions & 0 deletions test/createModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import path from 'path';
import { Module as _Module } from 'module';

interface ModuleConstructor {
_nodeModulePaths(directoryPath: string): string[];
}

interface ModuleInstance {
_compile(source: string, fileName: string): void;
}

const Module = _Module as typeof _Module & ModuleConstructor;

export function createModule<T extends Record<string, any>>(fileName: string, source: string): InstanceType<typeof Module> & { exports: T } {
fileName = path.resolve(process.cwd(), fileName);
const mod = new Module(fileName, require.main) as InstanceType<typeof Module> & ModuleInstance;
mod.filename = fileName;
mod.paths = Module._nodeModulePaths(path.dirname(fileName));
mod._compile(source, fileName);
return mod;
}
Loading

0 comments on commit 9f1193c

Please sign in to comment.