Skip to content

Commit 451b077

Browse files
committed
feat(analysis): Support opticss enabled analysis of css-blocks.
1 parent c253625 commit 451b077

17 files changed

+1005
-865
lines changed

packages/jsx-analyzer/src/analyzer/index.ts

+271-169
Large diffs are not rendered by default.

packages/jsx-analyzer/src/utils/ExpressionReader.ts

+110-87
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { ObjectDictionary, objectValues } from '@opticss/util';
12
import * as debugGenerator from 'debug';
2-
import { BlockObject, Block, BlockClass, State, StyleMapping } from 'css-blocks';
3+
import { Block, BlockClass, State, isBlockClass } from 'css-blocks';
34
import { Node } from 'babel-traverse';
45
import {
56
isCallExpression,
@@ -10,10 +11,13 @@ import {
1011
isBooleanLiteral,
1112
isIdentifier,
1213
isNumericLiteral,
13-
StringLiteral
14+
StringLiteral,
15+
NumericLiteral,
16+
BooleanLiteral,
17+
Expression,
18+
CallExpression,
1419
} from 'babel-types';
1520

16-
import Analysis from './Analysis';
1721
import { MalformedBlockPath, ErrorLocation } from '../utils/Errors';
1822

1923
const debug = debugGenerator('css-blocks:jsx');
@@ -23,66 +27,101 @@ const isValidSegment = /^[a-z|A-Z|_|$][a-z|A-Z|_|$|1-9]*$/;
2327
const PATH_START = Symbol('path-start');
2428
const PATH_END = Symbol('path-end');
2529
const CALL_START = Symbol('call-start');
26-
const CALL_END = Symbol('call-start');
30+
const CALL_END = Symbol('call-end');
2731

2832
export const DYNAMIC_STATE_ID = '*';
2933

3034
export type PathExpression = (string|symbol)[];
3135

32-
function isLiteralPart(part: Node) {
33-
return isStringLiteral(part) || isNumericLiteral(part) || isBooleanLiteral(part);
36+
function isLiteral(node: Node): node is StringLiteral | NumericLiteral | BooleanLiteral {
37+
return isStringLiteral(node) || isNumericLiteral(node) || isBooleanLiteral(node);
38+
}
39+
40+
function hasLiteralArguments(args: Array<Node>, length: number): boolean {
41+
return args.length === length && args.every(a => isLiteral(a));
42+
}
43+
44+
export type BlockClassResult = {
45+
block: Block;
46+
blockClass?: BlockClass;
47+
};
48+
export type BlockStateResult = BlockClassResult & {
49+
state: State;
50+
};
51+
export type BlockStateGroupResult = BlockClassResult & {
52+
stateGroup: ObjectDictionary<State>;
53+
dynamicStateExpression: Expression;
54+
};
55+
export type BlockExpressionResult = BlockClassResult
56+
| BlockStateResult
57+
| BlockStateGroupResult;
58+
59+
export function isBlockStateResult(result: BlockExpressionResult): result is BlockStateResult {
60+
return !!((<BlockStateResult>result).state);
61+
}
62+
export function isBlockStateGroupResult(result: BlockExpressionResult): result is BlockStateGroupResult {
63+
return !!((<BlockStateGroupResult>result).stateGroup);
3464
}
3565

3666
export class ExpressionReader {
37-
private expression: PathExpression;
38-
private index = 0;
67+
private pathExpression: PathExpression;
68+
private callExpression: CallExpression | undefined;
3969

4070
isBlockExpression: boolean;
4171
block: string | undefined;
4272
class: string | undefined;
4373
state: string | undefined;
4474
subState: string | undefined;
4575
isDynamic: boolean;
46-
concerns: BlockObject[] = [];
4776
err: null | string = null;
77+
loc: ErrorLocation;
4878

49-
constructor(expression: Node, analysis: Analysis | StyleMapping){
79+
constructor(expression: Node, filename: string){
5080

5181
// Expression location info object for error reporting.
52-
let loc: ErrorLocation = {
53-
filename: 'TODO',
82+
this.loc = {
83+
filename,
5484
line: expression.loc.start.line,
55-
column: expression.loc.start.line
85+
column: expression.loc.start.column
5686
};
5787

58-
this.expression = getExpressionParts(expression, loc);
88+
this.pathExpression = parsePathExpression(expression, this.loc);
5989

6090
// Register if this expression's sub-state is dynamic or static.
61-
if ( isCallExpression(expression) && expression.arguments[0] && !isLiteralPart(expression.arguments[0])) {
62-
this.isDynamic = true;
63-
}
64-
else {
65-
this.isDynamic = false;
91+
if (isCallExpression(expression)) {
92+
this.callExpression = expression;
93+
this.isDynamic = !hasLiteralArguments(expression.arguments, 1);
94+
if (expression.arguments.length > 1) {
95+
this.isBlockExpression = false;
96+
this.isDynamic = false;
97+
this.err = 'Only one argument can be supplied to a dynamic state';
98+
return;
99+
}
66100
}
67101

68-
let len = this.expression.length;
102+
if (this.pathExpression.length < 3) {
103+
this.isBlockExpression = false;
104+
return;
105+
}
69106

70107
// Discover block expression identifiers of the form `block[.class][.state([subState])]`
71-
for ( let i=0; i<len; i++ ) {
108+
for ( let i = 0; i < this.pathExpression.length; i++ ) {
72109

73110
if ( this.err ) {
74111
this.block = this.class = this.state = this.subState = undefined;
75112
break;
76113
}
77114

78-
let token = this.expression[i];
79-
let next = this.expression[i+1];
115+
let token = this.pathExpression[i];
116+
let next = this.pathExpression[i+1];
80117

81118
if ( token === PATH_START && this.block ) {
119+
// XXX This err appears to be completely swallowed?
82120
debug(`Discovered invalid block expression ${this.toString()} in objstr`);
83121
this.err = 'Nested expressions are not allowed in block expressions.';
84122
}
85123
else if ( token === CALL_START && !this.state ) {
124+
// XXX This err appears to be completely swallowed?
86125
debug(`Discovered invalid block expression ${this.toString()} in objstr`);
87126
this.err = 'Can not select state without a block or class.';
88127
}
@@ -95,88 +134,73 @@ export class ExpressionReader {
95134
}
96135
}
97136

98-
this.isBlockExpression = !!len && !this.err && !!this.block;
137+
this.isBlockExpression = !this.err && !!this.block;
138+
}
99139

100-
// Fetch the specified block. If no block found, fail silently.
101-
if ( !this.block ) { return; }
102-
let blockObj: Block | BlockClass = analysis.blocks[this.block];
103-
if ( !blockObj ) {
104-
debug(`Discovered Block ${this.block} from expression ${this.toString()}`);
105-
return;
140+
getResult(blocks: ObjectDictionary<Block>): BlockExpressionResult {
141+
if (!this.isBlockExpression) {
142+
if (this.err) {
143+
throw new MalformedBlockPath(this.err, this.loc);
144+
} else {
145+
throw new MalformedBlockPath('No block name specified.', this.loc);
146+
}
147+
}
148+
let block = blocks[this.block!];
149+
let blockClass: BlockClass | undefined = undefined;
150+
if (!block) {
151+
throw new MalformedBlockPath(`No block named ${this.block} exists in this scope.`, this.loc);
106152
}
107153

108154
// Fetch the class referenced in this selector, if it exists.
109155
if ( this.class && this.class !== 'root' ) {
110-
let classObj: BlockClass | undefined;
111-
classObj = (blockObj as Block).getClass(this.class);
112-
if ( !classObj ) {
113-
throw new MalformedBlockPath(`No class named "${this.class}" found on block "${this.block}"`, loc);
156+
blockClass = block.lookup(`.${this.class}`) as BlockClass | undefined;
157+
if ( !blockClass ) {
158+
let knownClasses = block.all(false).filter(s => isBlockClass(s)).map(c => c.asSource());
159+
throw new MalformedBlockPath(`No class named "${this.class}" found on block "${this.block}". ` +
160+
`Did you mean one of: ${knownClasses.join(', ')}`, this.loc);
114161
}
115-
blockObj = classObj;
116162
}
117163

118164
// If no state, we're done!
119165
if ( !this.state ) {
120166
debug(`Discovered BlockClass ${this.class} from expression ${this.toString()}`);
121-
this.concerns.push(blockObj);
122-
return;
123-
}
124-
125-
// Throw an error if this state expects a sub-state and nothing has been provided.
126-
let states = blockObj.states.resolveGroup(this.state) || {};
127-
if ( Object.keys(states).length > 1 && this.subState === undefined ) {
128-
throw new MalformedBlockPath(`State ${this.toString()} expects a sub-state.`, loc);
167+
return { block, blockClass };
129168
}
169+
let statesContainer = (blockClass || block).states;
130170

131171
// Fetch all matching state objects.
132-
let stateObjects = blockObj.states.resolveGroup(this.state, this.subState !== DYNAMIC_STATE_ID ? this.subState : undefined) || {};
172+
let stateGroup = statesContainer.resolveGroup(this.state, this.subState !== DYNAMIC_STATE_ID ? this.subState : undefined) || {};
173+
let stateNames = Object.keys(stateGroup);
174+
if (stateNames.length > 1 && this.subState !== DYNAMIC_STATE_ID) {
175+
throw new MalformedBlockPath(`State ${this.toString()} expects a sub-state.`, this.loc);
176+
}
133177

134178
// Throw a helpful error if this state / sub-state does not exist.
135-
if ( !Object.keys(stateObjects).length ) {
136-
let knownStates: State[] | undefined;
137-
let allSubStates = blockObj.states.resolveGroup(this.state) || {};
138-
if (allSubStates) {
139-
let ass = allSubStates;
140-
knownStates = Object.keys(allSubStates).map(k => ass[k]);
141-
}
179+
if ( stateNames.length === 0 ) {
180+
let allSubStates = statesContainer.resolveGroup(this.state) || {};
181+
let knownStates = objectValues(allSubStates);
142182
let message = `No state [state|${this.state}${this.subState ? '='+this.subState : ''}] found on block "${this.block}".`;
143-
if (knownStates) {
144-
if (knownStates.length === 1) {
145-
message += `\n Did you mean: ${knownStates[0].asSource()}?`;
146-
} else {
147-
message += `\n Did you mean one of: ${knownStates.map(s => s.asSource()).join(', ')}?`;
148-
}
183+
if (knownStates.length === 1) {
184+
message += `\n Did you mean: ${knownStates[0].asSource()}?`;
185+
} else if (knownStates.length > 0) {
186+
message += `\n Did you mean one of: ${knownStates.map(s => s.asSource()).join(', ')}?`;
149187
}
150-
throw new MalformedBlockPath(message, loc);
188+
throw new MalformedBlockPath(message, this.loc);
151189
}
152190

153191
debug(`Discovered ${this.class ? 'class-level' : 'block-level'} state ${this.state} from expression ${this.toString()}`);
154192

155-
// Push all discovered state / sub-state objects to BlockObject concerns list.
156-
([]).push.apply(this.concerns, (<any>Object).values(stateObjects));
157-
}
158-
159-
get length() {
160-
return this.expression.length;
161-
}
162-
163-
next(): string | undefined {
164-
let next = this.expression[this.index++];
165-
if (next === PATH_START) return this.next();
166-
if (next === PATH_END) return this.next();
167-
if (next === CALL_START) return this.next();
168-
if (next === CALL_END) return this.next();
169-
return <string>next;
170-
}
171-
172-
reset(): void {
173-
this.index = 0;
193+
if (this.subState === DYNAMIC_STATE_ID) {
194+
return { block, blockClass, stateGroup, dynamicStateExpression: this.callExpression!.arguments[0] };
195+
} else {
196+
return { block, blockClass, state: objectValues(stateGroup)[0] };
197+
}
174198
}
175199

176200
toString() {
177201
let out = '';
178-
let len = this.expression.length;
179-
this.expression.forEach((part, idx) => {
202+
let len = this.pathExpression.length;
203+
this.pathExpression.forEach((part, idx) => {
180204

181205
// If the first or last character, skip. These will always be path start/end symbols.
182206
if ( idx === 0 || idx === len-1 ) { return; }
@@ -206,14 +230,13 @@ export class ExpressionReader {
206230
/**
207231
* Given a `MemberExpression`, `Identifier`, or `CallExpression`, return an array
208232
* of all expression identifiers.
209-
* Ex: `foo.bar['baz']` => ['foo', 'bar', 'baz']
210-
* EX: `foo.bar[biz.baz].bar` => ['foo', 'bar', ['biz', 'baz'], 'bar']
233+
* Ex: `foo.bar['baz']` => [Symbol('path-start'), 'foo', 'bar', 'baz', Symbol('path-end')]
234+
* EX: `foo.bar[biz.baz].bar` => [Symbol('path-start'), 'foo', 'bar', Symbol('path-start'), 'biz', 'baz', Symbol('path-end'), 'bar', Symbol('path-end']
211235
* Return empty array if input is invalid nested expression.
212-
* @param expression The expression in question. Yes, any. We're about to do some
213-
* very explicit type checking here.
236+
* @param expression The expression node to be parsed
214237
* @returns An array of strings representing the expression parts.
215238
*/
216-
function getExpressionParts(expression: Node, loc: ErrorLocation): PathExpression {
239+
function parsePathExpression(expression: Node, loc: ErrorLocation): PathExpression {
217240

218241
let parts: PathExpression = [];
219242
let args: Node[] | undefined;
@@ -246,7 +269,7 @@ function getExpressionParts(expression: Node, loc: ErrorLocation): PathExpressio
246269
}
247270

248271
// If we encounter another member expression (Ex: foo[bar.baz])
249-
// Because Typescript has issues with recursively nested types, we use booleans
272+
// Because Typescript has issues with recursively nested types, we use symbols
250273
// to denote the boundaries between nested expressions.
251274
else if ( expression.computed && (
252275
isCallExpression(prop) ||
@@ -255,7 +278,7 @@ function getExpressionParts(expression: Node, loc: ErrorLocation): PathExpressio
255278
isJSXIdentifier(prop) ||
256279
isIdentifier(prop)
257280
)) {
258-
parts.unshift.apply(parts, getExpressionParts(prop, loc));
281+
parts.unshift(...parsePathExpression(prop, loc));
259282
}
260283

261284
else {
@@ -276,7 +299,7 @@ function getExpressionParts(expression: Node, loc: ErrorLocation): PathExpressio
276299
if ( args ) {
277300
parts.push(CALL_START);
278301
args.forEach((part) => {
279-
if ( isLiteralPart(part) ) {
302+
if ( isLiteral(part) ) {
280303
parts.push(String((part as StringLiteral).value));
281304
}
282305
else {

0 commit comments

Comments
 (0)