import { Analysis , Block , ElementAnalysis } from "@css-blocks/core" ; import { SourceLocation as TemplateSourceLocation , SourcePosition as TemplateSourcePosition , } from "@opticss/element-analysis" ; import { Binding , NodePath } from "babel-traverse" ; import { AssignmentExpression , CallExpression , Expression , Identifier , JSXAttribute , JSXOpeningElement , Node , SourceLocation , isCallExpression , isIdentifier , isJSXExpressionContainer , isJSXIdentifier , isMemberExpression , isVariableDeclarator , } from "babel-types" ; import { isCommonNameForStyling , isStyleFunction } from "../../styleFunctions" ; import { MalformedBlockPath , TemplateAnalysisError } from "../../utils/Errors" ; import { ExpressionReader , isBlockStateGroupResult , isBlockStateResult } from "../../utils/ExpressionReader" ; import { isConsoleLogStatement } from "../../utils/isConsoleLogStatement" ; import { JSXAnalysis } from "../index" ; import { TEMPLATE_TYPE } from "../Template" ; import { BooleanExpression , Flags , JSXElementAnalysis , StringExpression , TernaryExpression } from "../types" ; function htmlTagName ( el ) { return ( isJSXIdentifier ( el . name ) && el . name . name === el . name . name . toLowerCase ( ) ) ? el . name . name : undefined ; } function isLocation ( n ) { return ! ! ( ( < SourceLocation > n ) . start && ( < SourceLocation > n ) . end && typeof ( < SourceLocation > n ) . start . line === "number" ) ; } function isNodePath ( n ) { return ! ! ( < NodePath > n ) . node ; } export class JSXElementAnalyzer { private analysis : JSXAnalysis ; private filename : string ; private classProperties : Flags ; private isRewriteMode : boolean ; reservedClassNames : Set < string > ; constructor ( analysis , isRewriteMode = false ) { this . analysis = analysis ; this . isRewriteMode = isRewriteMode ; this . filename = analysis . template . identifier ; this . classProperties = { class : true , className : true , } ; this . reservedClassNames = analysis . reservedClassNames ( ) ; } private startElement ( location , tagName ? ) { if ( this . isRewriteMode ) { return new ElementAnalysis < BooleanExpression , StringExpression , TernaryExpression > ( location , this . reservedClassNames , tagName ) ; } return this . analysis . startElement < BooleanExpression , StringExpression , TernaryExpression > ( location , tagName ) ; } private endElement ( element ) { if ( this . isRewriteMode ) { return ; } return this . analysis . endElement < BooleanExpression , StringExpression , TernaryExpression > ( element ) ; } private isClassAttribute ( attr ) { return isJSXIdentifier ( attr . name ) && this . classProperties [ attr . name . name ] ; } classAttributePaths ( path ) { let attrPath = path . get ( "attributes.0" ) as NodePath < JSXAttribute > | undefined ; let found = new Array < NodePath < JSXAttribute > > ( ) ; while ( attrPath && attrPath . node ) { if ( this . isClassAttribute ( attrPath . node ) ) { found . push ( attrPath ) ; } attrPath = ( < any > attrPath ) . getNextSibling ( ) as NodePath < JSXAttribute > | undefined ; } return found ; } analyzeAssignment ( path ) { let assignment = path . node ; if ( assignment . operator !== "=" ) return ; let lVal = assignment . left ; let element ; if ( isMemberExpression ( lVal ) ) { let property = lVal . property ; if ( ! lVal . computed && isIdentifier ( property ) && property . name === "className" ) { element = this . startElement ( this . location ( path ) ) ; this . analyzeClassExpression ( path . get ( "right" ) , element ) ; this . endElement ( element ) ; } } return element ; } analyzeJSXElement ( path ) { let el = path . node ; if ( ! el . attributes || el . attributes . length === 0 ) { return ; } let classAttrs = this . classAttributePaths ( path ) ; if ( classAttrs . length === 0 ) return ; let element = this . startElement ( this . location ( path ) , htmlTagName ( el ) ) ; for ( let classAttr of classAttrs ) { this . analyzeClassAttribute ( classAttr , element ) ; } this . endElement ( element ) ; return element ; } private location ( loc ) { if ( isNodePath ( loc ) ) { loc = loc . node . loc ; } else if ( ! isLocation ( loc ) ) { loc = loc . loc ; } let location = { start : { ... loc . start } , end : { ... loc . end } , } ; location . start . filename = this . filename ; location . end ! . filename = this . filename ; return location ; } private nodeLoc ( node ) { return this . location ( node ) . start ; } styleVariableBinding ( path ) { let valuePath = path . get ( "value" ) ; if ( ! valuePath . node || ! isJSXExpressionContainer ( valuePath . node ) ) return ; if ( isIdentifier ( valuePath . node . expression ) ) { let identPath = valuePath . get ( "expression" ) as NodePath < Identifier > ; let identBinding = path . scope . getBinding ( identPath . node . name ) ; if ( identBinding && identBinding . kind === "module" || ! identBinding ) { return ; } if ( isVariableDeclarator ( identBinding . path . node ) ) { return identBinding ; } } return ; } private analyzeClassExpression ( expression , element , suppressErrors = false ) { if ( isIdentifier ( expression . node ) ) { let identifier = expression . node ; let identBinding = expression . scope . getBinding ( identifier . name ) ; if ( identBinding ) { if ( identBinding . constantViolations . length > 0 ) { if ( suppressErrors ) return ; throw new TemplateAnalysisError ( `illegal assignment to a style variable.` , this . nodeLoc ( identBinding . constantViolations [ 0 ] ) ) ; } if ( identBinding . kind === "module" ) { let name = identifier . name ; let block = this . analysis . getBlock ( name ) ; if ( block ) { element . addStaticClass ( block . rootClass ) ; } else { if ( suppressErrors ) return ; throw new TemplateAnalysisError ( `No block named ${ name } was found` , this . nodeLoc ( expression ) ) ; } } else { let identPathNode = identBinding . path . node ; let initialValueOfIdent ; if ( isVariableDeclarator ( identPathNode ) ) { initialValueOfIdent = identPathNode . init ; if ( identBinding . references > 1 ) { for ( let refPath of identBinding . referencePaths . filter ( p => p . parentPath . type !== "JSXExpressionContainer" ) ) { let parentPath = refPath . parentPath ; if ( ! isConsoleLogStatement ( parentPath . node ) ) { if ( suppressErrors ) return ; throw new TemplateAnalysisError ( `illegal use of a style variable.` , this . nodeLoc ( parentPath ) ) ; } } } } else { if ( suppressErrors ) return ; throw new TemplateAnalysisError ( `variable for class attributes must be initialized with a style expression.` , this . nodeLoc ( expression ) ) ; } this . addPossibleDynamicStyles ( element , initialValueOfIdent , identBinding . path ) ; } } } else if ( expression . isMemberExpression ( ) ) { let parts = new ExpressionReader ( expression . node , this . filename ) ; let expressionResult = parts . getResult ( this . analysis ) ; let blockClass = expressionResult . blockClass ; if ( isBlockStateGroupResult ( expressionResult ) || isBlockStateResult ( expressionResult ) ) { throw new Error ( "internal error, not expected on a member expression" ) ; } else { element . addStaticClass ( blockClass ) ; } } else if ( isCallExpression ( expression . node ) ) { let callExpr = expression . node ; let styleFn = isStyleFunction ( expression , callExpr ) ; if ( styleFn . type === "error" ) { if ( styleFn . canIgnore ) { try { let parts = new ExpressionReader ( callExpr , this . filename ) ; let expressionResult = parts . getResult ( this . analysis ) ; let blockClass = expressionResult . blockClass ; if ( isBlockStateGroupResult ( expressionResult ) ) { element . addDynamicGroup ( blockClass , expressionResult . stateGroup , expressionResult . dynamicStateExpression , false ) ; } else if ( isBlockStateResult ( expressionResult ) ) { element . addStaticAttr ( blockClass , expressionResult . state ) ; } else { throw new Error ( "internal error, not expected on a call expression" ) ; } } catch ( e ) { if ( e instanceof MalformedBlockPath ) { if ( isIdentifier ( callExpr . callee ) ) { let fnName = callExpr . callee . name ; if ( isCommonNameForStyling ( fnName ) ) { throw new TemplateAnalysisError ( `The call to style function '${ fnName }' does not resolve to an import statement of a known style helper.` , this . nodeLoc ( expression ) ) ; } else { throw new TemplateAnalysisError ( `Function called within class attribute value '${ fnName }' must be either an 'objstr' call, or a state reference` , this . nodeLoc ( expression ) ) ; } } } throw e ; } } else { if ( suppressErrors ) return ; throw new TemplateAnalysisError ( styleFn . message , styleFn . location ) ; } } else { styleFn . analyze ( this . analysis , element , this . filename , styleFn , callExpr ) ; } } else { } } private analyzeClassAttribute ( path , element ) { let value = path . get ( "value" ) ; if ( ! value . isJSXExpressionContainer ( ) ) return ; let expressionPath = value . get ( "expression" ) ; this . analyzeClassExpression ( expressionPath , element ) ; } param analysis param path private addPossibleDynamicStyles ( element , expression , path ) { if ( ! isCallExpression ( expression ) ) { return ; } let func = expression ; let styleFunc = isStyleFunction ( path , expression ) ; if ( styleFunc . type === "error" ) { if ( styleFunc . canIgnore ) { return ; } else { throw new TemplateAnalysisError ( styleFunc . message , { filename : this . filename , ... styleFunc . location } ) ; } } styleFunc . analyze ( this . analysis , element , this . filename , styleFunc , func ) ; } } param analysis export function elementVisitor ( analysis ) { let elementAnalyzer = new JSXElementAnalyzer ( analysis ) ; return { AssignmentExpression ( path ) { elementAnalyzer . analyzeAssignment ( path ) ; } , param path JSXOpeningElement ( path ) { elementAnalyzer . analyzeJSXElement ( path ) ; } , } ; }