Compare commits

...

14 Commits

31 changed files with 287 additions and 40 deletions

View File

@ -9,13 +9,12 @@ declarations/
[lints]
all=error
unsafe-getters-setters=off
[options]
all=true
module.use_strict=true
munge_underscores=true
include_warnings=true
unsafe.enable_getters_and_setters=true
suppress_comment= \\(.\\|\n\\)*\\$FlowFixMe
suppress_comment= \\(.\\|\n\\)*\\$FlowIssue
suppress_type=$FlowFixMe

View File

@ -550,6 +550,7 @@ declare module '@babel/types' {
declare class BooleanLiteralTypeAnnotation extends Node {
type: 'BooleanLiteralTypeAnnotation';
value: boolean;
}
declare class NullLiteralTypeAnnotation extends Node {

View File

@ -42,7 +42,7 @@
"@babel/preset-flow": "^7.0.0-beta.32",
"@babel/register": "^7.0.0-beta.32",
"ajv": "^5.5.1",
"flow-bin": "^0.60.1",
"flow-bin": "^0.77.0",
"jasmine": "^2.8.0",
"mocha": "^4.0.1",
"nyc": "^11.3.0"

View File

@ -1,3 +1,5 @@
// @flow
import * as yaml from 'yaml-js';
import yargs from 'yargs';
import stringifyJson from 'json-stringify-pretty-compact';

View File

@ -1,3 +1,5 @@
// @flow
import wu from 'wu';
import type {Node} from '@babel/types';

View File

@ -1,3 +1,5 @@
// @flow
import wu from 'wu';
// @see flow#5376.
@ -164,8 +166,8 @@ function extractCommonjsNamedExternals<+T: Node>(nodes: T[], path: string): Exte
function processExportNamedDeclaration(ctx: Context, node: ExportNamedDeclaration) {
if (isDeclaration(node.declaration)) {
node.declaration.leadingComments = node.leadingComments;
const reference = processDeclaration(ctx, node.declaration);
ctx.provide(reference, reference);
}

View File

@ -1,8 +1,10 @@
// @flow
import wu from 'wu';
// @see flow#5376.
import type {
ArrayTypeAnnotation, ClassDeclaration, ClassProperty, Comment, FlowTypeAnnotation,
Node, ArrayTypeAnnotation, ClassDeclaration, ClassProperty, Comment, FlowTypeAnnotation,
GenericTypeAnnotation, InterfaceDeclaration, IntersectionTypeAnnotation, TypeAlias,
UnionTypeAnnotation, NullableTypeAnnotation, ObjectTypeIndexer, ObjectTypeProperty,
StringLiteralTypeAnnotation, ObjectTypeAnnotation, AnyTypeAnnotation, MixedTypeAnnotation,
@ -10,7 +12,8 @@ import type {
} from '@babel/types';
import {
isIdentifier, isObjectTypeProperty, isStringLiteralTypeAnnotation, isClassProperty,
isIdentifier, isStringLiteral, isObjectTypeProperty,
isStringLiteralTypeAnnotation, isClassProperty,
} from '@babel/types';
import Context from './context';
@ -28,12 +31,28 @@ import {invariant} from '../utils';
function processTypeAlias(ctx: Context, node: TypeAlias | DeclareTypeAlias) {
const {name} = node.id;
const type = makeType(ctx, node.right);
// TODO: support function aliases.
invariant(type);
if (name != 'integer') {
// Forward declaration for the recursive types
ctx.define(name, t.createAny());
ctx.define(name, type);
const type = makeType(ctx, node.right);
addComment(node, type);
// TODO: support function aliases.
invariant(type);
ctx.define(name, type);
}
}
function addComment(node: Node, type: Type) {
if (node.leadingComments) {
const cmt = node.leadingComments.map(c => c.value).join('\n').trim();
if (cmt) {
type.comment = cmt;
}
}
}
// TODO: type params.
@ -43,6 +62,7 @@ function processInterfaceDeclaration(
) {
const {name} = node.id;
const type = makeType(ctx, node.body);
addComment(node, type);
invariant(type);
@ -105,6 +125,8 @@ function makeType(ctx: Context, node: FlowTypeAnnotation): ?Type {
return t.createLiteral(null);
case 'BooleanTypeAnnotation':
return t.createBoolean();
case 'BooleanLiteralTypeAnnotation':
return t.createLiteral(node.value);
case 'NumberTypeAnnotation':
return t.createNumber('f64');
case 'StringTypeAnnotation':
@ -132,7 +154,7 @@ function makeType(ctx: Context, node: FlowTypeAnnotation): ?Type {
case 'MixedTypeAnnotation':
return t.createMixed();
case 'FunctionTypeAnnotation':
return null;
return t.createAny();
default:
invariant(false, `Unknown node: ${node.type}`);
}
@ -204,6 +226,7 @@ function makeField(ctx: Context, node: ObjectTypeProperty | ClassProperty): ?Fie
invariant(value);
type = makeType(ctx, value);
addComment(node, type);
}
if (!type) {
@ -213,10 +236,12 @@ function makeField(ctx: Context, node: ObjectTypeProperty | ClassProperty): ?Fie
// TODO: warning about computed properties.
invariant(isObjectTypeProperty(node) || !node.computed);
invariant(isIdentifier(node.key));
invariant(isIdentifier(node.key) || isStringLiteral(node.key));
const name = isIdentifier(node.key) ? node.key.name : node.key.value;
return {
name: node.key.name,
name,
value: type,
required: node.optional == null || !node.optional,
};
@ -225,12 +250,14 @@ function makeField(ctx: Context, node: ObjectTypeProperty | ClassProperty): ?Fie
function makeMap(ctx: Context, node: ObjectTypeIndexer): ?MapType {
const keys = makeType(ctx, node.key);
const values = makeType(ctx, node.value);
addComment(node, values);
return keys && values ? t.createMap(keys, values) : null;
}
function makeArray(ctx: Context, node: ArrayTypeAnnotation): ?ArrayType {
const items = makeType(ctx, node.elementType);
addComment(node, items);
return items != null ? t.createArray(items) : null;
}
@ -271,6 +298,9 @@ function makeIntersection(ctx: Context, node: IntersectionTypeAnnotation): ?Type
function makeReference(ctx: Context, node: GenericTypeAnnotation): ?Type {
const {name} = node.id;
if (name == 'integer') {
return t.createNumber('i64');
}
const params = node.typeParameters
&& wu(node.typeParameters.params).map(n => makeType(ctx, n)).toArray();

View File

@ -1,3 +1,5 @@
// @flow
import wu from 'wu';
import {invariant} from '../utils';
@ -196,6 +198,11 @@ function either(params: (?Type)[]): ?Type {
: t.createUnion(variants);
}
// $ FlowFixMe
function fixMe(): ?Type {
return t.createAny();
}
export default {
Object: object,
Buffer: buffer,
@ -212,4 +219,5 @@ export default {
$Diff: diff,
$All: all,
$Either: either,
$FlowFixMe: fixMe,
};

View File

@ -1,3 +1,5 @@
// @flow
import * as fs from 'fs';
import * as pathlib from 'path';
import wu from 'wu';

View File

@ -1,3 +1,5 @@
// @flow
import * as pathlib from 'path';
import * as resolve from 'resolve';

View File

@ -1,10 +1,13 @@
// @flow
import {invariant} from '../utils';
import type {Type} from '../types';
import * as t from '../types';
import {createNumber, isRepr} from '../types';
export type Pragma =
| TypePragma;
| TypePragma
;
export type TypePragma = {
kind: 'type',
@ -20,11 +23,11 @@ export function extractPragmas(text: string): Pragma[] {
while ((match = PRAGMA_RE.exec(text))) {
const repr = match[1];
invariant(['i32', 'i64', 'u32', 'u64', 'f32', 'f64'].includes(repr));
invariant(isRepr(repr));
pragmas.push({
kind: 'type',
value: t.createNumber(repr),
value: createNumber(repr),
});
}

View File

@ -1,3 +1,5 @@
// @flow
import type {Node} from '@babel/types';
import type Scope from './scope';
@ -9,7 +11,8 @@ export type Query =
| Template
| Definition
| External
| Special;
| Special
;
export type Unknown = {
kind: 'unknown',

View File

@ -1,3 +1,5 @@
// @flow
import wu from 'wu';
import type {Node} from '@babel/types';
@ -82,7 +84,7 @@ export default class Scope {
if (declared) {
invariant(decl);
invariant(decl.kind === 'declaration');
invariant(decl.kind === 'declaration' || decl.kind === 'definition');
} else {
invariant(!decl);
}

View File

@ -1,3 +1,5 @@
// @flow
import type {Node} from '@babel/types';
import {VISITOR_KEYS} from '@babel/types';

View File

@ -1,3 +1,5 @@
// @flow
import {invariant} from './utils';
import type {TypeId, Type} from './types';

View File

@ -1,3 +1,5 @@
// @flow
import wu from 'wu';
import {invariant, collect, partition} from '../utils';
@ -10,6 +12,7 @@ export type Schema = boolean | {
id?: string,
$ref?: string,
$schema?: string,
$comment?: string,
title?: string,
description?: string,
default?: mixed,
@ -43,6 +46,18 @@ export type Schema = boolean | {
};
function convert(fund: Fund, type: ?Type): Schema {
let schema = convertType(fund, type);
if (type && type.comment) {
if (schema === true) {
schema = { $comment: type.comment };
} else {
schema.$comment = type.comment;
}
}
return schema;
}
function convertType(fund: Fund, type: ?Type): Schema {
if (!type) {
return {
type: 'null',
@ -88,13 +103,13 @@ function convert(fund: Fund, type: ?Type): Schema {
};
case 'union':
const enumerate = wu(type.variants)
.filter(variant => variant.kind === 'literal')
.filter(variant => variant.kind === 'literal' && variant.value !== null)
.map(literal => (literal: $FlowFixMe).value)
.tap(value => invariant(value !== undefined))
.toArray();
const schemas = wu(type.variants)
.filter(variant => variant.kind !== 'literal')
.filter(variant => variant.kind !== 'literal' || variant.value === null)
.map(variant => convert(fund, variant))
.toArray();

View File

@ -1,3 +1,5 @@
// @flow
import Parser from './parser';
import Collector from './collector';
import type {Type} from './types';

View File

@ -1,3 +1,5 @@
// @flow
import * as babylon from 'babylon';
import type {File} from '@babel/types';

View File

@ -1,3 +1,5 @@
// @flow
export type Type =
| RecordType
| ArrayType
@ -12,12 +14,14 @@ export type Type =
| LiteralType
| AnyType
| MixedType
| ReferenceType;
| ReferenceType
;
export type TypeId = string[];
export type BaseType = {
id?: TypeId,
comment?: string,
};
export type RecordType = BaseType & {
@ -64,9 +68,11 @@ export type MaybeType = BaseType & {
export type NumberType = BaseType & {
kind: 'number',
repr: 'i32' | 'i64' | 'u32' | 'u64' | 'f32' | 'f64',
repr: Repr,
};
export type Repr = 'i32' | 'i64' | 'u32' | 'u64' | 'f32' | 'f64';
export type StringType = BaseType & {
kind: 'string',
};
@ -77,9 +83,11 @@ export type BooleanType = BaseType & {
export type LiteralType = BaseType & {
kind: 'literal',
value: string | number | boolean | null | void,
value: LiteralValue,
};
export type LiteralValue = string | number | boolean | null | void;
export type AnyType = BaseType & {
kind: 'any',
};
@ -93,20 +101,20 @@ export type ReferenceType = BaseType & {
to: TypeId,
};
export const createRecord = (fields: *): RecordType => ({kind: 'record', fields});
export const createArray = (items: *): ArrayType => ({kind: 'array', items});
export const createTuple = (items: *): TupleType => ({kind: 'tuple', items});
export const createMap = (keys: *, values: *): MapType => ({kind: 'map', keys, values});
export const createUnion = (variants: *): UnionType => ({kind: 'union', variants});
export const createIntersection = (parts: *): IntersectionType => ({kind: 'intersection', parts});
export const createMaybe = (value: *): MaybeType => ({kind: 'maybe', value});
export const createNumber = (repr: *): NumberType => ({kind: 'number', repr});
export const createRecord = (fields: Field[]): RecordType => ({kind: 'record', fields});
export const createArray = (items: Type): ArrayType => ({kind: 'array', items});
export const createTuple = (items: Array<?Type>): TupleType => ({kind: 'tuple', items});
export const createMap = (keys: Type, values: Type): MapType => ({kind: 'map', keys, values});
export const createUnion = (variants: Type[]): UnionType => ({kind: 'union', variants});
export const createIntersection = (parts: Type[]): IntersectionType => ({kind: 'intersection', parts});
export const createMaybe = (value: Type): MaybeType => ({kind: 'maybe', value});
export const createNumber = (repr: Repr): NumberType => ({kind: 'number', repr});
export const createString = (): StringType => ({kind: 'string'});
export const createBoolean = (): BooleanType => ({kind: 'boolean'});
export const createLiteral = (value: *): LiteralType => ({kind: 'literal', value});
export const createLiteral = (value: LiteralValue): LiteralType => ({kind: 'literal', value});
export const createAny = () => ({kind: 'any'});
export const createMixed = () => ({kind: 'mixed'});
export const createReference = (to: *) => ({kind: 'reference', to});
export const createReference = (to: TypeId) => ({kind: 'reference', to});
declare function clone(Type): Type;
declare function clone(TypeId): TypeId;
@ -162,3 +170,7 @@ function cloneType(type: Type): Type {
return createReference(type.to.slice());
}
}
export function isRepr(v: string): boolean %checks {
return v === 'i32' || v === 'i64' || v === 'u32' || v === 'u64' || v === 'f32' || v === 'f64';
}

View File

@ -1,3 +1,5 @@
// @flow
import * as assert from 'assert';
// I so much dream about the user guards...

View File

@ -1,20 +1,23 @@
// @flow
import * as assert from 'assert';
import * as fs from 'fs';
import * as path from 'path';
import * as yaml from 'yaml-js';
import wu from 'wu';
import Ajv from 'ajv';
import stringifyJson from 'json-stringify-pretty-compact';
import collect from '../src';
function run(title) {
function run(title, generateMissing) {
let actual, expectedTypes, expectedSchema;
// Run the collector only if the suite will be checked.
before(() => {
actual = collect(title + '/source.js');
expectedTypes = yaml.load(fs.readFileSync(title + '/types.yaml', 'utf8'));
expectedSchema = JSON.parse(fs.readFileSync(title + '/schema.json', 'utf8'));
expectedTypes = readFileAndPrepare(title + '/types.yaml', yaml.load);
expectedSchema = readFileAndPrepare(title + '/schema.json', JSON.parse);
});
it('should not include cycles', () => {
@ -22,7 +25,18 @@ function run(title) {
});
it('should provide expected types', () => {
assert.deepEqual(actual.types, expectedTypes);
if (expectedTypes === undefined && generateMissing) {
console.log('Generating types.yaml...');
const content = yaml.dump(actual.types, null, null, {
indent: 4,
width: 100,
}).trimRight();
fs.writeFileSync(title + '/types.yaml', content);
} else {
assert.deepEqual(actual.types, expectedTypes);
}
});
it('should generate valid JSON schema', () => {
@ -34,10 +48,37 @@ function run(title) {
});
it('should provide expected JSON schema', () => {
assert.deepEqual(actual.schema, expectedSchema);
if (expectedSchema === undefined && generateMissing) {
console.log('Generating schema.json...');
const content = stringifyJson(actual.schema, {
indent: 4,
maxLength: 100,
});
fs.writeFileSync(title + '/schema.json', content);
} else {
assert.deepEqual(actual.schema, expectedSchema);
}
});
}
function readFileAndPrepare<R>(path: string, prepare: string => R): R | void {
let data;
try {
data = fs.readFileSync(path, 'utf8');
} catch (ex) {
if (ex.code === 'ENOENT') {
return undefined;
}
throw ex;
}
return prepare(data);
}
function detectCycles(obj: mixed, cycles: Set<mixed> = new Set, objs: Set<mixed> = new Set) {
if (obj == null || typeof obj !== 'object') {
return cycles;
@ -61,8 +102,10 @@ function detectCycles(obj: mixed, cycles: Set<mixed> = new Set, objs: Set<mixed>
function main() {
process.chdir(path.join(__dirname, 'samples'));
const generateMissing = process.env.GENERATE_MISSING === '1';
for (const title of fs.readdirSync('.')) {
describe(title, () => run(title));
describe(title, () => run(title, generateMissing));
}
}

View File

@ -0,0 +1,11 @@
{
"$schema": "http://json-schema.org/draft-06/schema#",
"definitions": {
"fixMe::X": true,
"fixMe::Y": {
"type": "object",
"properties": {"y": true},
"required": ["y"]
}
}
}

View File

@ -0,0 +1,7 @@
type X = $FlowFixMe;
type Y = {
y: $FlowFixMe;
};
export {X, Y};

View File

@ -0,0 +1,8 @@
- kind: any
id: [fixMe, X]
- kind: record
fields:
- name: y
value: {kind: any}
required: true
id: [fixMe, Y]

View File

@ -0,0 +1,15 @@
export type A = {
a: number;
}
export type IgnoredType = {
ignored: number,
};
export interface IgnoredInterface {
ignored: number;
}
export default class IgnoredDefault {
ignored: number;
}

View File

@ -0,0 +1,10 @@
{
"$schema": "http://json-schema.org/draft-06/schema#",
"definitions": {
"selectiveImport::modules::first::A": {
"type": "object",
"properties": {"a": {"type": "number"}},
"required": ["a"]
}
}
}

View File

@ -0,0 +1,3 @@
import {A} from './modules/first';
export {A};

View File

@ -0,0 +1,6 @@
- kind: record
fields:
- name: a
value: {kind: number, repr: f64}
required: true
id: [selectiveImport, modules, first, A]

View File

@ -0,0 +1,20 @@
{
"$schema": "http://json-schema.org/draft-06/schema#",
"definitions": {
"stringKeys::X": {
"type": "object",
"properties": {"a b": {"type": "string"}},
"required": ["a b"]
},
"stringKeys::Y": {
"type": "object",
"properties": {"a b": {"type": "string"}},
"required": ["a b"]
},
"stringKeys::Z": {
"type": "object",
"properties": {"a b": {"type": "string"}},
"required": ["a b"]
}
}
}

View File

@ -0,0 +1,13 @@
type X = {
'a b': string;
};
interface Y {
'a b': string;
}
class Z {
'a b': string;
}
export {X, Y, Z};

View File

@ -0,0 +1,18 @@
- kind: record
fields:
- name: "a b"
value: {kind: string}
required: true
id: [stringKeys, X]
- kind: record
fields:
- name: "a b"
value: {kind: string}
required: true
id: [stringKeys, Y]
- kind: record
fields:
- name: "a b"
value: {kind: string}
required: true
id: [stringKeys, Z]