Skip to content

Commit 59bb568

Browse files
committed
initial commit, support preconditions
0 parents  commit 59bb568

17 files changed

+455
-0
lines changed

.babelrc

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"presets": [
3+
"stage-0",
4+
"es2015"
5+
],
6+
"plugins": [
7+
"typecheck"
8+
]
9+
}

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.settings
2+
node_modules
3+
lib
4+
lib-checked
5+
*.transformed
6+
npm-debug.log

.npmignore

Whitespace-only changes.

.travis.yml

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
language: node_js
2+
3+
node_js:
4+
- "0.12"
5+
- "iojs"
6+
- "4.2.1"

LICENSE.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# The MIT License (MIT)
2+
3+
Copyright (c) 2015 codemix.com
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.

README.md

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Babel Contracts
2+
3+
This is a [Babel](https://babeljs.io/) plugin for design by contract for JavaScript.
4+
5+
[![Build Status](https://travis-ci.org/codemix/babel-plugin-contracts.svg)](https://travis-ci.org/codemix/babel-plugin-contracts)
6+
7+
# What?
8+
9+
Dunno yet.
10+
11+
# Installation
12+
13+
Install via [npm](https://npmjs.org/package/babel-plugin-contracts).
14+
```sh
15+
npm install --save-dev babel-plugin-contracts
16+
```
17+
Then, in your babel configuration (usually in your `.babelrc` file), add `"contracts"` to your list of plugins:
18+
```json
19+
{
20+
"plugins": ["contracts"]
21+
}
22+
```
23+
24+
# License
25+
26+
Published by [codemix](http://codemix.com/) under a permissive MIT License, see [LICENSE.md](./LICENSE.md).
27+

package.json

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"name": "babel-plugin-contracts",
3+
"version": "3.2.1",
4+
"description": "Transforms flow type annotations into runtime type checks.",
5+
"main": "lib/index.js",
6+
"scripts": {
7+
"build": "babel --plugins syntax-flow,transform-flow-strip-types -d ./lib ./src",
8+
"build-typed": "npm run build && babel --plugins ./lib,syntax-flow,transform-flow-strip-types -d ./lib-checked ./src",
9+
"prepublish": "npm run build",
10+
"pretest": "npm run build",
11+
"test": "mocha ./test/index.js",
12+
"test-checked": "npm run build-typed && CONTRACTS_USE_LIBCHECKED=1 mocha ./test/index.js",
13+
"watch": "NODE_WATCH=1 mocha --watch"
14+
},
15+
"repository": {
16+
"type": "git",
17+
"url": "https://github.com/codemix/babel-plugin-contracts"
18+
},
19+
"keywords": [
20+
"babel",
21+
"babel-plugin",
22+
"design by contract",
23+
"types",
24+
"typing",
25+
"contracts",
26+
"type check",
27+
"dbyc"
28+
],
29+
"author": "Charles Pick <[email protected]>",
30+
"license": "MIT",
31+
"bugs": {
32+
"url": "https://github.com/codemix/babel-plugin-contracts/issues"
33+
},
34+
"homepage": "https://github.com/codemix/babel-plugin-contracts",
35+
"devDependencies": {
36+
"babel-cli": "^6.1.0",
37+
"babel-core": "^6.1.0",
38+
"babel-generator": "^6.1.2",
39+
"babel-plugin-syntax-class-properties": "^6.1.18",
40+
"babel-plugin-syntax-flow": "^6.0.14",
41+
"babel-plugin-transform-es2015-modules-commonjs": "^6.1.3",
42+
"babel-plugin-transform-flow-strip-types": "^6.0.14",
43+
"babel-plugin-typecheck": "^3.2.1",
44+
"babel-polyfill": "^6.0.16",
45+
"babel-preset-es2015": "^6.1.0",
46+
"babel-preset-react": "^6.1.0",
47+
"babel-preset-stage-0": "^6.1.18",
48+
"babel-preset-stage-1": "^6.1.0",
49+
"mocha": "~2.2.4",
50+
"should": "^6.0.1"
51+
}
52+
}

src/index.js

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import generate from "babel-generator";
2+
3+
type Plugin = {
4+
visitor: Visitors
5+
};
6+
7+
type PluginParams = {
8+
types: Object;
9+
template: (source: string) => (ids: Object) => Node;
10+
};
11+
12+
type Visitors = {
13+
[key: string]: Visitor
14+
}
15+
16+
type Visitor = (path: NodePath) => void;
17+
18+
type Node = {
19+
type: string;
20+
};
21+
22+
type Literal = {
23+
type: 'StringLiteral' | 'BooleanLiteral' | 'NumericLiteral' | 'NullLiteral' | 'RegExpLiteral'
24+
};
25+
26+
type Identifier = {
27+
type: string;
28+
name: string;
29+
};
30+
31+
type Scope = {};
32+
33+
type NodePath = {
34+
type: string;
35+
node: Node;
36+
scope: Scope;
37+
};
38+
39+
40+
/**
41+
* # Design By Contract Transformer
42+
*/
43+
export default function ({types: t, template}: PluginParams): Plugin {
44+
45+
const PRECONDITION_NAME = 'pre';
46+
const POSTCONDITION_NAME = 'post';
47+
const INVARIANT_NAME = 'invariant';
48+
49+
const preconditionAsserter: (ids: {[key: string]: Node}) => Node = template(`
50+
if (!condition) {
51+
throw new Error(message);
52+
}
53+
`);
54+
55+
const functionVisitor: Visitors = {
56+
Function (path: NodePath): void {
57+
// This will be handled by the outer visitor, so skip it.
58+
path.skip();
59+
},
60+
61+
LabeledStatement (path: NodePath): void {
62+
const label: NodePath = path.get('label');
63+
64+
if (label.node.name === PRECONDITION_NAME) {
65+
assemblePrecondition(path);
66+
}
67+
68+
}
69+
};
70+
71+
72+
function assemblePrecondition (path: NodePath): void {
73+
const body: NodePath = path.get('body');
74+
const fn: NodePath = path.getFunctionParent();
75+
const name: string = fn.node.id ? `"${fn.node.id.name}" `: ' ';
76+
body.traverse({
77+
VariableDeclaration (item: NodePath): void {
78+
throw path.buildCodeFrameError(`Preconditions cannot have side effects.`);
79+
},
80+
Function (item: NodePath): void {
81+
throw path.buildCodeFrameError(`Preconditions cannot have side effects.`);
82+
},
83+
AssignmentExpression (item: NodePath): void {
84+
throw path.buildCodeFrameError(`Preconditions cannot have side effects.`);
85+
},
86+
UpdateExpression (item: NodePath): void {
87+
throw path.buildCodeFrameError(`Preconditions cannot have side effects.`);
88+
},
89+
ExpressionStatement (statement: NodePath): void {
90+
let condition: NodePath = statement.get('expression');
91+
let message;
92+
if (condition.isSequenceExpression()) {
93+
const expressions = condition.get('expressions');
94+
condition = expressions[0];
95+
message = expressions[1];
96+
}
97+
else {
98+
message = t.stringLiteral(`Function ${name}precondition failed: ${generate(condition.node).code}`);
99+
}
100+
statement.replaceWith(preconditionAsserter({
101+
condition,
102+
message
103+
}));
104+
}
105+
});
106+
107+
if (body.isBlockStatement()) {
108+
path.replaceWithMultiple(path.get('body').node.body);
109+
}
110+
else {
111+
path.replaceWith(path.get('body'));
112+
}
113+
114+
}
115+
116+
function expression (input: string): Function {
117+
const fn: Function = template(input);
118+
return function (...args) {
119+
const node: Node = fn(...args);
120+
return getExpression(node);
121+
};
122+
}
123+
124+
return {
125+
visitor: {
126+
Function (path: NodePath): void {
127+
if (path.isArrowFunctionExpression() && !path.get('body').isBlockStatement()) {
128+
// Naked arrow functions cannot contain contracts.
129+
return;
130+
}
131+
path.traverse(functionVisitor);
132+
}
133+
}
134+
};
135+
}

test-polyfill.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
require("babel-core/register")({
2+
"presets": ["stage-1", "es2015"],
3+
"plugins": [
4+
//"syntax-flow",
5+
"transform-flow-strip-types"
6+
]
7+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function demo (input) {
2+
pre: {
3+
typeof input === 'string';
4+
input = false;
5+
}
6+
return input.length;
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function demo (input) {
2+
pre: {
3+
let i = 0;
4+
typeof input[i] === 'string';
5+
}
6+
return input.length;
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
let thing = 0;
2+
export default function demo (input) {
3+
pre: {
4+
thing++;
5+
}
6+
return input.length;
7+
}
+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default function demo (input) {
2+
pre: {
3+
typeof input === 'string';
4+
input.length > 3;
5+
input.length < 6;
6+
}
7+
return input.length;
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default function demo (input) {
2+
pre: {
3+
typeof input === 'string', "Input must be a string";
4+
}
5+
return input.length;
6+
}

test/fixtures/precondition.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default function demo (input) {
2+
pre: {
3+
typeof input === 'string';
4+
}
5+
return input.length;
6+
}

0 commit comments

Comments
 (0)