import { parse as parsePath } from "path" ; import { AST_NODE_TYPES , ESLintUtils , TSESLint , TSESTree , } from "@typescript-eslint/experimental-utils" ; import * as ts from "typescript" ; export enum OfficeCalls { WRITE = "WRITE" , READ = "READ" , NOTOFFICE = "NOTOFFICE" } type RequiredParserServices = ReturnType < typeof ESLintUtils . getParserServices > ; export type Options = unknown [ ] ; export type MessageIds = string ; export const REPO_URL = "https://aka.ms/o-a-scripts" ; export const createRule = ESLintUtils . RuleCreator ( ( name ) => { const ruleName = parsePath ( name ) . name ; return `${ REPO_URL }/packages/eslint-plugin-excel-custom-functions/docs/rules/${ ruleName }.md` ; } ) ; export function isCustomFunction ( node , services ) { const functionStarts = getAllFunctionStarts ( node , services ) ; for ( let i = 0 ; i < functionStarts . length ; i ++ ) { if ( getJsDocCustomFunction ( ts . getJSDocTags ( functionStarts [ i ] ) ) ) { return true ; } } return false ; } export function superNodeMe ( nodeArray , helperFuncToHelperFuncMap ) { nodeArray . forEach ( ( node , index ) => { let currentVal = helperFuncToHelperFuncMap . get ( node ) ; if ( ! currentVal ) { currentVal = new Set < ts . Node > ( [ ] ) ; } for ( let i = 0 ; i < nodeArray . length ; i ++ ) { if ( i != index ) { currentVal . add ( nodeArray [ i ] ) ; } } helperFuncToHelperFuncMap . set ( node , currentVal ) ; } ) ; } export function getAllFunctionStarts ( node , services ) { let tsNode = services . esTreeNodeToTSNodeMap . get ( node ) ; let outputArray = [ ] ; while ( tsNode && tsNode . kind && ! ( tsNode . kind == ts . SyntaxKind . SourceFile ) ) { if ( isFunctionDeclarationLike ( tsNode ) ) { outputArray . push ( tsNode ) ; } tsNode = tsNode . parent ; } return outputArray ; } export function getStartOfFunction ( node , services ) { let tsNode = services . esTreeNodeToTSNodeMap . get ( node ) ; while ( tsNode && tsNode . kind && ! ( tsNode . kind == ts . SyntaxKind . SourceFile ) ) { if ( isFunctionDeclarationLike ( tsNode ) ) { return tsNode ; } tsNode = tsNode . parent ; } return undefined ; } function isFunctionDeclarationLike ( tsNode ) { return ! ! ( tsNode && tsNode . kind && ( tsNode . kind == ts . SyntaxKind . CallSignature || tsNode . kind == ts . SyntaxKind . ConstructSignature || tsNode . kind == ts . SyntaxKind . MethodSignature || tsNode . kind == ts . SyntaxKind . IndexSignature || tsNode . kind == ts . SyntaxKind . FunctionType || tsNode . kind == ts . SyntaxKind . ConstructorType || tsNode . kind == ts . SyntaxKind . JSDocFunctionType || tsNode . kind == ts . SyntaxKind . FunctionDeclaration || tsNode . kind == ts . SyntaxKind . MethodDeclaration || tsNode . kind == ts . SyntaxKind . Constructor || tsNode . kind == ts . SyntaxKind . GetAccessor || tsNode . kind == ts . SyntaxKind . SetAccessor || tsNode . kind == ts . SyntaxKind . FunctionExpression || tsNode . kind == ts . SyntaxKind . ArrowFunction ) ) ; } function getJsDocCustomFunction ( tags ) { for ( const tag of tags ) { if ( tag . tagName . escapedText === "customfunction" ) { return { reason : tag . tagName . escapedText || "" } ; } } return undefined ; } export function isOfficeFuncWriteOrRead ( node , typeChecker , services ) { if ( isOfficeObject ( node . callee , typeChecker , services ) ) { let type = typeChecker . getTypeAtLocation ( services . esTreeNodeToTSNodeMap . get ( node . callee ) ) ; let symbol = type . getSymbol ( ) ; let symbolText = symbol ? typeChecker . symbolToString ( symbol ) : undefined ; if ( symbolText && ( symbolText . toLowerCase ( ) . startsWith ( "set" ) || symbolText . toLowerCase ( ) . startsWith ( "add" ) || symbolText . toLowerCase ( ) . startsWith ( "clear" ) || symbolText . toLowerCase ( ) . startsWith ( "delete" ) || symbolText . toLowerCase ( ) . startsWith ( "remove" ) || symbolText . toLowerCase ( ) . startsWith ( "insert" ) || symbolText . toLowerCase ( ) . startsWith ( "copy" ) || symbolText . toLowerCase ( ) . startsWith ( "create" ) ) ) { return OfficeCalls . WRITE ; } let callSignatures = type . getCallSignatures ( ) ; return callSignatures . some ( ( callSignature ) => { return ( 1 << 14 === ( ( 1 << 14 ) & callSignature . getReturnType ( ) . flags . valueOf ( ) ) ) ; } ) ? OfficeCalls . WRITE : OfficeCalls . READ ; } return undefined ; } export function isOfficeObject ( node , typeChecker , services ) { let earlierMember = false ; if ( node . type == AST_NODE_TYPES . MemberExpression ) { earlierMember = isOfficeObject ( node . object , typeChecker , services ) ; } if ( earlierMember ) { return true ; } const officeDeclarations = getFunctionDeclarations ( node , typeChecker , services ) ; return officeDeclarations ? officeDeclarations . some ( isParentNodeOfficeNamespace ) : false ; } function isParentNodeOfficeNamespace ( node , index , decArray ) { const nodeText = node . getText ( ) ; if ( nodeText . startsWith ( "declare namespace Office" ) || nodeText . startsWith ( "declare namespace OfficeCore" ) || nodeText . startsWith ( "declare namespace Excel" ) ) { return true ; } else { return node . parent ? isParentNodeOfficeNamespace ( node . parent , index , decArray ) : false ; } } export function getFunctionDeclarations ( node , typeChecker , services ) { if ( node . type == AST_NODE_TYPES . CallExpression ) { node = ( < TSESTree . CallExpression > node ) . callee ; } let tsNode = services . esTreeNodeToTSNodeMap . get ( node ) ; let type = typeChecker . getTypeAtLocation ( tsNode ) ; let symbol = type . getSymbol ( ) ; if ( ! symbol ) { symbol = typeChecker . getSymbolAtLocation ( tsNode ) ; } return symbol ? symbol . declarations : undefined ; } export function isHelperFunc ( node , typeChecker , services ) { const functionDeclarations = getFunctionDeclarations ( node , typeChecker , services ) ; let output = functionDeclarations ? functionDeclarations . some ( ( declaration ) => { let sourceFile = declaration . getSourceFile ( ) ; return ! services . program . isSourceFileFromExternalLibrary ( sourceFile ) ; } ) : false ; return output ; } export function bubbleUpNewCallingFuncs ( node , helperFuncToHelperFuncMap ) { let outputSet = new Set < ts . Node > ( [ node ] ) ; let examiningSet = helperFuncToHelperFuncMap . get ( node ) ; if ( examiningSet ) { examiningSet . forEach ( ( nodeToExamine ) => { let addingSet = helperFuncToHelperFuncMap . get ( nodeToExamine ) ; if ( addingSet ) { addingSet . forEach ( ( nodeToAdd ) => { if ( ! outputSet . has ( nodeToAdd ) ) { examiningSet ?. add ( nodeToAdd ) ; } } ) ; } outputSet . add ( nodeToExamine ) ; examiningSet ?. delete ( nodeToExamine ) ; } ) ; } return outputSet ; } export function reportIfCalledFromCustomFunction ( nodeToBubbleUpFrom , ruleContext , helperFuncToHelperFuncMap , helperFuncToMentionsMap , officeCallingFuncs ? ) { bubbleUpNewCallingFuncs ( nodeToBubbleUpFrom , helperFuncToHelperFuncMap ) . forEach ( ( bubbledUp ) => { if ( officeCallingFuncs ) { officeCallingFuncs . add ( bubbledUp ) ; } helperFuncToMentionsMap . get ( bubbledUp ) ?. forEach ( ( mention ) => { ruleContext . report ( mention ) ; } ) ; helperFuncToMentionsMap . delete ( bubbledUp ) ; } ) ; } export function callExpressionAnalysis ( node , services , typeChecker , ruleContext , officeCallingFuncs , helperFuncToMentionsMap , helperFuncToHelperFuncMap , isCheckingForWrite = false ) { if ( isOfficeObject ( node , typeChecker , services ) ) { if ( isOfficeFuncWriteOrRead ( node , typeChecker , services ) === ( isCheckingForWrite ? OfficeCalls . WRITE : OfficeCalls . READ ) ) { if ( isCustomFunction ( node , services ) ) { ruleContext . report ( { messageId : isCheckingForWrite ? "officeWriteCall" : "officeReadCall" , loc : node . loc , node : node , } ) ; } const functionStart = getStartOfFunction ( node , services ) ; if ( functionStart ) { reportIfCalledFromCustomFunction ( functionStart , ruleContext , helperFuncToHelperFuncMap , helperFuncToMentionsMap , officeCallingFuncs ) ; } } } else if ( isHelperFunc ( node , typeChecker , services ) ) { const functionDeclarations = getFunctionDeclarations ( node , typeChecker , services ) ; if ( functionDeclarations && functionDeclarations . length > 0 ) { superNodeMe ( functionDeclarations , helperFuncToHelperFuncMap ) ; if ( isCustomFunction ( node , services ) ) { if ( functionDeclarations . some ( ( declaration ) => { return officeCallingFuncs . has ( declaration ) ; } ) ) { ruleContext . report ( { messageId : isCheckingForWrite ? "officeWriteCall" : "officeReadCall" , loc : node . loc , node : node , } ) ; } else { helperFuncToMentionsMap . set ( functionDeclarations [ 0 ] , ( helperFuncToMentionsMap . get ( functionDeclarations [ 0 ] ) || [ ] ) . concat ( { messageId : isCheckingForWrite ? "officeWriteCall" : "officeReadCall" , loc : node . loc , node : node , } ) ) ; } } const functionStart = getStartOfFunction ( node , services ) ; if ( functionStart ) { helperFuncToHelperFuncMap . set ( functionDeclarations [ 0 ] , ( helperFuncToHelperFuncMap . get ( functionDeclarations [ 0 ] ) || new Set < ts . Node > ( [ ] ) ) . add ( functionStart ) ) ; reportIfCalledFromCustomFunction ( functionStart , ruleContext , helperFuncToHelperFuncMap , helperFuncToMentionsMap ) ; } } } } export function assignmentExpressionAnalysis ( node , ruleContext , services , typeChecker , isCheckingForWrite = false ) { if ( isOfficeObject ( isCheckingForWrite ? node . left : node . right , typeChecker , services ) && isCustomFunction ( node , services ) ) { ruleContext . report ( { messageId : isCheckingForWrite ? "officeWriteCall" : "officeReadCall" , loc : node . loc , node : node , } ) ; } } export function variableDeclaratorAnalysis ( node , ruleContext , services , typeChecker ) { if ( node . init && isOfficeObject ( node . init , typeChecker , services ) && isCustomFunction ( node , services ) ) { ruleContext . report ( { messageId : "officeReadCall" , loc : node . loc , node : node , } ) ; } }