Skip to content

Commit

Permalink
Enable new State / Substate method syntax (#16)
Browse files Browse the repository at this point in the history
* Enable new State / Substate method syntax

 - Bump css-blocks so correlations aren't enumerated. Update tests accordingly.
 - Remove explicit state import from blocks
 - States are now methods on Blocks and Classes
 - Static and dynamic substates may be passed to State methods
 - Add error checking for mis-use of boolean states
 - States with sub-states must be passed a value
 - Initial infrastructure added to construct boolean expressions
 - Use new css-block analysis introspection methods in tests
 - Move BlockObject lookup logic to ExpressionReader
 - Remove detailed ast and source data from Template when no longer needed.
 - Add new babel-core types file locally until DefinitelyTyped/DefinitelyTyped#19428 lands
  • Loading branch information
amiller-gh authored and chriseppstein committed Nov 30, 2017
1 parent b996e11 commit 7b675c2
Show file tree
Hide file tree
Showing 30 changed files with 1,244 additions and 670 deletions.
11 changes: 6 additions & 5 deletions packages/jsx-analyzer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"scripts": {
"test": "npm run build && npm run tslint && mocha dist/test --recursive --opts test/mocha.opts",
"build": "rm -rf dist && tsc --jsx preserve -p tsconfig.json",
"watch": "watch 'npm run build' './src' --wait=3",
"watch": "watch 'npm run test' './src' './test' --wait=3",
"tslint": "tslint --project tsconfig.json --type-check"
},
"repository": {
Expand All @@ -32,11 +32,13 @@
},
"homepage": "https://github.com/css-blocks/jsx-analyzer#readme",
"devDependencies": {
"@types/babel-core": "^6.7.14",
"@types/babel-traverse": "^6.7.17",
"@types/babel-generator": "^6.25.0",
"@types/babel-traverse": "^6.25.2",
"@types/babel-types": "^6.7.16",
"@types/babylon": "^6.16.1",
"@types/chai": "^3.5.2",
"@types/debug": "0.0.29",
"@types/minimatch": "^2.0.29",
"@types/mocha": "^2.2.41",
"@types/node": "^7.0.18",
"babel-core": "^6.25.0",
Expand All @@ -56,14 +58,13 @@
"watch": "^1.0.2"
},
"dependencies": {
"@types/debug": "0.0.29",
"@types/minimatch": "^2.0.29",
"babel-traverse": "^6.24.1",
"babel-types": "^6.24.1",
"babylon": "^6.17.4",
"css-blocks": "^0.11.0",
"debug": "^2.6.8",
"minimatch": "^3.0.4",
"object.values": "^1.0.4",
"postcss": "^6.0.1",
"typescript": "^2.3.4"
}
Expand Down
239 changes: 64 additions & 175 deletions packages/jsx-analyzer/src/analyzer/index.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import Analysis from '../utils/Analysis';
import * as debugGenerator from 'debug';
import { NodePath, Binding } from 'babel-traverse';
import { Block, BlockClass, State, StateContainer } from 'css-blocks';
import { Block, BlockClass, State } from 'css-blocks';
import {
CallExpression,
JSXOpeningElement,
JSXAttribute,
ObjectMethod,
ObjectProperty,
SpreadProperty,
isCallExpression,
isIdentifier,
isImportDeclaration,
isJSXExpressionContainer,
isJSXIdentifier,
isJSXNamespacedName,
isLiteral,
isMemberExpression,
isObjectExpression,
isObjectProperty,
isStringLiteral
} from 'babel-types';

import Analysis from '../utils/Analysis';
import { ExpressionReader } from '../utils/ExpressionReader';
import { JSXOpeningElement,
JSXAttribute,
isJSXIdentifier,
isJSXNamespacedName,
isMemberExpression,
isJSXExpressionContainer,
isStringLiteral,
isIdentifier,
isVariableDeclarator,
isCallExpression,
isObjectExpression,
ObjectProperty,
isObjectProperty,
SpreadProperty,
ObjectMethod,
ObjectExpression,
isImportDeclaration,
isLiteral
} from 'babel-types';

const OBJSTR_PACKAGE_NAME = 'obj-str';
const STATE_NAMESPACE = 'state';
Expand All @@ -32,33 +32,28 @@ const CLASS_PROPERTIES = {
'className': 1
};

const debug = debugGenerator('css-blocks:jsx');

type Property = ObjectProperty | SpreadProperty | ObjectMethod;

/**
* Given a well formed Object String `CallExpression`, return the Array of `Property`
* objects passed to it. If input is not a well formed Object String, return empty array.
* @param path The current Path we are observing for access to scope.
* @param fund The suspected Object String `CallExpression` to process.
* @returns The array of `Property` values passed to Object String
* Given a well formed Object String `CallExpression`, add all Block style references
* to the given analysis object.
* @param analysis Theis template's analysis object.
* @param path The objstr CallExpression Path.
*/
function saveObjstrProps(analysis: Analysis, path: NodePath<any>, func: any) {
let props: Property[] = [];
function saveObjstrProps(analysis: Analysis, path: any) {

// If this node is not a call expression (ex: `objstr({})`), or is a complex
// call expression that we'll have trouble analyzing (ex: `(true && objstr)({})`)
// short circuit and continue execution.
let func: CallExpression = path.node;
if ( !isCallExpression(func) || !isIdentifier(func.callee) ) {
return;
}

// Fetch the function name. If we can't get the name, or the function is not in scope, throw
let name = func.callee.name;
let binding: Binding | undefined = path.scope.getBinding(name);
if ( !binding ) {
throw new Error(`Variable "${name}" is undefined`);
}
if ( !binding ) { return; }

// If this call expression is not an `objstr` call, or is in a form we don't
// recognize (Ex: first arg is not an object), short circuit and continue execution.
Expand All @@ -68,131 +63,51 @@ function saveObjstrProps(analysis: Analysis, path: NodePath<any>, func: any) {
return;
}

props = (<ObjectExpression>func.arguments[0]).properties;
// We consider every `objstr` call a single element's styles. Start a new element.
analysis.startElement({
line: path.node.loc.start.line,
column: path.node.loc.start.column,
});

if ( !props ) {
throw new Error(`Class attribute value "${name}" must be either an "objstr" call, or a Block reference`);
// Ensure the first argument passed to suspected `objstr` call is an object.
let obj: any = func.arguments[0];
if ( !isObjectExpression(obj) ) {
throw new Error(`Class attribute value "${name}" must either be a valid "objstr" call, or a Block reference`);
}

// For each property passed to objstr, parse the expression and attempt to save the style.
props.forEach((prop: Property) => {
obj.properties.forEach((prop: Property) => {

// Ignore non computed properties, they will never be blocks objects.
if ( !isObjectProperty(prop) || prop.computed === false ) {
return;
}

// Get expression from computed property name.
let parts: ExpressionReader = new ExpressionReader(prop.key);
saveStyle(parts, analysis, !isLiteral(prop.value));
// Get expression from computed property name and save to analysis.
let parts: ExpressionReader = new ExpressionReader(prop.key, analysis);

// Save all discovered BlockObjects to analysis
parts.concerns.forEach( (style) => {
analysis.addStyle(style, !isLiteral(prop.value));
});

});

analysis.endElement();
}

/**
* Given an array of expression string identifiers, fetch the corrosponding Block
* Object, validate its existence, and register it properly with our `Analytics`
* reporter.
* @param parts The array of strings representing expression identifiers.
* @param analysis The Analysis object with Block data and to store our results in.
* @param isDynamic If the style to save is dynamic.
* Babel visitors we can pass to `babel-traverse` to run analysis on a given JSX file.
* @param analysis The Analysis object to store our results in.
*/
function saveStyle(reader: ExpressionReader, analysis: Analysis, isDynamic = false): void {
let part: string | undefined;
let blockName: string | undefined;
let className: string | undefined;
let stateName: string | undefined;
let substateName: string | undefined;

// If nothing here, we don't care!
if ( reader.length === 0 ) {
return;
}

while ( part = reader.next() ){
if ( !blockName ) {
blockName = part;
}
else if ( analysis.template.localStates[blockName] === part ) {
stateName = reader.next();
substateName = reader.next();
}
else if ( !className ) {
className = part;
}
// If the user is referencing something deeper than allowed, throw.
else {
throw new Error(`Attempted to access non-existant block class or state "${reader.toString()}"`);
}
}

// If there is no block imported under this local name, this is a class
// we don't care about. Return.
let block: Block | undefined = blockName ? analysis.blocks[blockName] : undefined;
if ( !block ) {
return;
}

let stateContainer: StateContainer | undefined = undefined;

// If applying the root styles, either by `class="block.root"` or `class="block"`
if ( className === 'root' || ( !className ) ) {

if ( stateName ) {
debug(`state ${stateName} is a block-level state.`);
stateContainer = block.states;
}
else {
analysis.addStyle(block, isDynamic);
}
}

// Otherwise, fetch the class referenced in this selector. If it exists, add.
else {
let classBlock: BlockClass | undefined = className ? block.getClass(className) : undefined;
if ( !classBlock ){
throw new Error(`No class named "${className}" found on block "${blockName}"`);
}

if ( stateName ) {
debug(`state ${stateName} is a class-level state for ${className}`);
stateContainer = classBlock.states;
}
else {
analysis.addStyle(classBlock, isDynamic);
}
}
if (stateContainer && stateName) {
let statesInGroupMaybe = stateContainer.resolveGroup(stateName, substateName);
if (statesInGroupMaybe) {
let statesInGroup = statesInGroupMaybe; // for typesafety
Object.keys(statesInGroup).forEach((stateName) => {
analysis.addStyle(statesInGroup[stateName], isDynamic);
});
} else {
let knownStates: State[] | undefined;
let allSubstates = stateContainer.resolveGroup(stateName);
if (allSubstates) {
let ass = allSubstates;
knownStates = Object.keys(allSubstates).map(k => ass[k]);
}
let message = `No state [state|${stateName}=${substateName}] found on block "${block.name}".`;
if (knownStates) {
if (knownStates.length === 1) {
message += `\n Did you mean: ${knownStates[0].asSource()}?`;
} else {
message += `\n Did you mean one of: ${knownStates.map(s => s.asSource()).join(', ')}?`;
}
}
throw new Error(message);
}
}
}

// Consolidate all visitors into a hash that we can pass to `babel-traverse`
export default function visitors(analysis: Analysis): object {
return {

// Find all objstr class expressions. Parse as though a single element's styles.
CallExpression(path: NodePath<CallExpression>, state: any): void {
saveObjstrProps(analysis, path);
},

/**
* Primary analytics parser for Babylon. Crawls all JSX Elements and their attributes
* and saves all discovered block references. See README for valid JSX CSS Block APIs.
Expand Down Expand Up @@ -224,53 +139,27 @@ export default function visitors(analysis: Analysis): object {
// If this attribute's value is an expression, evaluate it for block references.
if ( isJSXExpressionContainer(value) ) {

// Discover identifiers we are concerned with. These include Block root
// references and `objstr` references in scope that contain block objects.
// ex: `blockname` || `objstrVar`
// Discover block root identifiers.
if ( isIdentifier(value.expression) ) {
let identifier = value.expression;
let name = identifier.name;

// Check if there is a block of this name imported. If so, add style and exit.
// Check if there is a block of this name imported. If so, save style and exit.
let block: Block | undefined = analysis.blocks[name];
if ( block ) {
analysis.addStyle(block);
return;
}

// If there is no `name` in scope, throw
let binding: Binding | undefined = path.scope.getBinding(name);
if ( !binding ) {
throw new Error(`Variable "${name}" is undefined`);
}

// Yup, `any`. We're about to do a lot of type checking
let objstr: any = binding.path.node;

// We most likely got a varialbe declarator, unwrap the value. We're
// going to test if its the objstr call.
if ( isVariableDeclarator(objstr) ) {
objstr = objstr.init;
}

// Optimistically assume we have an objstr call and try to save it.
// Will fail silently and continue with exection if it is not an objstr call.
saveObjstrProps(analysis, path, objstr);

}

// If we discover an inlined call expression, assume it is an objstr call
// until proven otherwise. Fails silently and continues with execution if is not.
if ( isCallExpression(value.expression) ) {
saveObjstrProps(analysis, path, value.expression);
}

// Discover direct references to an imported block.
// Ex: `blockName.foo` || `blockname['bar']`
if ( isMemberExpression(value.expression) ) {
// Ex: `blockName.foo` || `blockname['bar']` || `blockname.bar()`
if ( isMemberExpression(value.expression) || isCallExpression(value.expression) ) {
let expression: any = value.expression;
let parts: ExpressionReader = new ExpressionReader(expression);
saveStyle(parts, analysis, false);
let parts: ExpressionReader = new ExpressionReader(expression, analysis);
parts.concerns.forEach( (style) => {
analysis.addStyle(style, false);
});
}
}
}
Expand All @@ -285,7 +174,7 @@ export default function visitors(analysis: Analysis): object {
}

// Fetch selector parts and look for a block under the local name.
let reader: ExpressionReader = new ExpressionReader(property.name);
let reader: ExpressionReader = new ExpressionReader(property.name, analysis);
let blockName: string | undefined = reader.next();
let className: string | undefined = reader.next();
let stateName: string | undefined = reader.next() || className;
Expand Down

0 comments on commit 7b675c2

Please sign in to comment.