1*082815dcSEvan Baconimport { CodeBlock, insertContentsAtOffset, searchFromOffset } from '../utils/commonCodeMod'; 2*082815dcSEvan Baconimport { findMatchingBracketPosition } from '../utils/matchBrackets'; 3*082815dcSEvan Bacon 4*082815dcSEvan Baconinterface SwiftFunctionParam { 5*082815dcSEvan Bacon argumentLabel: string; 6*082815dcSEvan Bacon parameterName: string; 7*082815dcSEvan Bacon typeString: string; 8*082815dcSEvan Bacon} 9*082815dcSEvan Bacon 10*082815dcSEvan Baconinterface InsertContentFunctionOptions { 11*082815dcSEvan Bacon position: 'head' | 'tail' | 'tailBeforeLastReturn'; 12*082815dcSEvan Bacon indent?: number; 13*082815dcSEvan Bacon} 14*082815dcSEvan Bacon 15*082815dcSEvan Bacon/** 16*082815dcSEvan Bacon * Add Objective-C import 17*082815dcSEvan Bacon * @param source source contents 18*082815dcSEvan Bacon * @param imports array of imports, e.g. ['<Foundation/Foundation.h>'] 19*082815dcSEvan Bacon * @returns updated contents 20*082815dcSEvan Bacon */ 21*082815dcSEvan Baconexport function addObjcImports(source: string, imports: string[]): string { 22*082815dcSEvan Bacon const lines = source.split('\n'); 23*082815dcSEvan Bacon // Try to insert statements after first #import where would probably not in #if block 24*082815dcSEvan Bacon const lineIndexWithFirstImport = lines.findIndex((line) => line.match(/^#import .*$/)); 25*082815dcSEvan Bacon for (const importElement of imports) { 26*082815dcSEvan Bacon if (!source.includes(importElement)) { 27*082815dcSEvan Bacon const importStatement = `#import ${importElement}`; 28*082815dcSEvan Bacon lines.splice(lineIndexWithFirstImport + 1, 0, importStatement); 29*082815dcSEvan Bacon } 30*082815dcSEvan Bacon } 31*082815dcSEvan Bacon return lines.join('\n'); 32*082815dcSEvan Bacon} 33*082815dcSEvan Bacon 34*082815dcSEvan Bacon/** 35*082815dcSEvan Bacon * Find code block of Objective-C interface or implementation 36*082815dcSEvan Bacon * 37*082815dcSEvan Bacon * @param contents source contents 38*082815dcSEvan Bacon * @param declaration interface/implementation, e.g. '@interface Foo' 39*082815dcSEvan Bacon * @returns found CodeBlock, or null if not found 40*082815dcSEvan Bacon */ 41*082815dcSEvan Baconexport function findObjcInterfaceCodeBlock( 42*082815dcSEvan Bacon contents: string, 43*082815dcSEvan Bacon declaration: string 44*082815dcSEvan Bacon): CodeBlock | null { 45*082815dcSEvan Bacon const start = contents.search(new RegExp(`^${declaration}\\W`, 'm')); 46*082815dcSEvan Bacon if (start < 0) { 47*082815dcSEvan Bacon return null; 48*082815dcSEvan Bacon } 49*082815dcSEvan Bacon 50*082815dcSEvan Bacon let end = contents.indexOf('\n@end', start); 51*082815dcSEvan Bacon end += 5; // '\n@end'.length === 5 52*082815dcSEvan Bacon 53*082815dcSEvan Bacon return { 54*082815dcSEvan Bacon start, 55*082815dcSEvan Bacon end, 56*082815dcSEvan Bacon code: contents.substring(start, end), 57*082815dcSEvan Bacon }; 58*082815dcSEvan Bacon} 59*082815dcSEvan Bacon 60*082815dcSEvan Bacon/** 61*082815dcSEvan Bacon * Find code block of Objective-C function without declaration, will return only {} block 62*082815dcSEvan Bacon * 63*082815dcSEvan Bacon * @param contents source contents 64*082815dcSEvan Bacon * @param selector function selector, e.g. 'doSomething:withSomeValue:' 65*082815dcSEvan Bacon * @returns found CodeBlock, or null if not found. 66*082815dcSEvan Bacon */ 67*082815dcSEvan Baconexport function findObjcFunctionCodeBlock(contents: string, selector: string): CodeBlock | null { 68*082815dcSEvan Bacon const symbols = selector.split(':'); 69*082815dcSEvan Bacon const argsCount = symbols.length - 1; 70*082815dcSEvan Bacon let pattern = '^[\\-+]\\s*\\(.+?\\)'; 71*082815dcSEvan Bacon if (argsCount === 0) { 72*082815dcSEvan Bacon pattern += `${symbols[0]}\\s+`; 73*082815dcSEvan Bacon } else { 74*082815dcSEvan Bacon for (let i = 0; i < argsCount; ++i) { 75*082815dcSEvan Bacon const argSymbol = `${symbols[i]}:\\(.+\\)\\w+`; 76*082815dcSEvan Bacon pattern += `${argSymbol}\\s+`; 77*082815dcSEvan Bacon } 78*082815dcSEvan Bacon } 79*082815dcSEvan Bacon pattern += '{'; 80*082815dcSEvan Bacon let start = contents.search(new RegExp(pattern, 'm')); 81*082815dcSEvan Bacon if (start < 0) { 82*082815dcSEvan Bacon return null; 83*082815dcSEvan Bacon } 84*082815dcSEvan Bacon start = contents.indexOf('{', start); 85*082815dcSEvan Bacon 86*082815dcSEvan Bacon const end = findMatchingBracketPosition(contents, '{', start); 87*082815dcSEvan Bacon return { 88*082815dcSEvan Bacon start, 89*082815dcSEvan Bacon end, 90*082815dcSEvan Bacon code: contents.substring(start, end + 1), 91*082815dcSEvan Bacon }; 92*082815dcSEvan Bacon} 93*082815dcSEvan Bacon 94*082815dcSEvan Bacon/** 95*082815dcSEvan Bacon * Insert contents to the Objective-C function block 96*082815dcSEvan Bacon * 97*082815dcSEvan Bacon * @param srcContents source contents 98*082815dcSEvan Bacon * @param selector function selector, e.g. 'doSomething:withSomeValue:' 99*082815dcSEvan Bacon * @param insertion code to insert 100*082815dcSEvan Bacon * @param options insertion options 101*082815dcSEvan Bacon * @returns updated contents 102*082815dcSEvan Bacon */ 103*082815dcSEvan Baconexport function insertContentsInsideObjcFunctionBlock( 104*082815dcSEvan Bacon srcContents: string, 105*082815dcSEvan Bacon selector: string, 106*082815dcSEvan Bacon insertion: string, 107*082815dcSEvan Bacon options: InsertContentFunctionOptions 108*082815dcSEvan Bacon): string { 109*082815dcSEvan Bacon return insertContentsInsideFunctionBlock(srcContents, selector, insertion, options, 'objc'); 110*082815dcSEvan Bacon} 111*082815dcSEvan Bacon 112*082815dcSEvan Bacon/** 113*082815dcSEvan Bacon * Insert contents to the Objective-C interface/implementation block 114*082815dcSEvan Bacon * 115*082815dcSEvan Bacon * @param srcContents source contents 116*082815dcSEvan Bacon * @param declaration interface/implementation, e.g. '@interface Foo' 117*082815dcSEvan Bacon * @param insertion code to insert 118*082815dcSEvan Bacon * @param options insertion options 119*082815dcSEvan Bacon * @returns updated contents 120*082815dcSEvan Bacon */ 121*082815dcSEvan Baconexport function insertContentsInsideObjcInterfaceBlock( 122*082815dcSEvan Bacon srcContents: string, 123*082815dcSEvan Bacon declaration: string, 124*082815dcSEvan Bacon insertion: string, 125*082815dcSEvan Bacon options: { 126*082815dcSEvan Bacon position: 'head' | 'tail'; 127*082815dcSEvan Bacon } 128*082815dcSEvan Bacon): string { 129*082815dcSEvan Bacon const codeBlock = findObjcInterfaceCodeBlock(srcContents, declaration); 130*082815dcSEvan Bacon if (!codeBlock) { 131*082815dcSEvan Bacon return srcContents; 132*082815dcSEvan Bacon } 133*082815dcSEvan Bacon 134*082815dcSEvan Bacon const { position } = options; 135*082815dcSEvan Bacon if (position === 'head') { 136*082815dcSEvan Bacon const firstNewLineIndex = srcContents.indexOf('\n', codeBlock.start); 137*082815dcSEvan Bacon srcContents = insertContentsAtOffset(srcContents, insertion, firstNewLineIndex); 138*082815dcSEvan Bacon } else if (position === 'tail') { 139*082815dcSEvan Bacon const endLen = '@end'.length; 140*082815dcSEvan Bacon srcContents = insertContentsAtOffset(srcContents, insertion, codeBlock.end - endLen); 141*082815dcSEvan Bacon } 142*082815dcSEvan Bacon return srcContents; 143*082815dcSEvan Bacon} 144*082815dcSEvan Bacon 145*082815dcSEvan Bacon/** 146*082815dcSEvan Bacon * Find code block of Swift function without declaration, will return only {} block 147*082815dcSEvan Bacon * 148*082815dcSEvan Bacon * @param contents source contents 149*082815dcSEvan Bacon * @param selector function selector, e.g. 'doSomething(_:withSomeValue:)' 150*082815dcSEvan Bacon * @returns found CodeBlock, or null if not found. 151*082815dcSEvan Bacon */ 152*082815dcSEvan Baconexport function findSwiftFunctionCodeBlock(contents: string, selector: string): CodeBlock | null { 153*082815dcSEvan Bacon const parenthesesIndex = selector.indexOf('('); 154*082815dcSEvan Bacon // `functName` === 'doSomething' of 'doSomething(_:withSomeValue:)' 155*082815dcSEvan Bacon const funcName = selector.substring(0, parenthesesIndex); 156*082815dcSEvan Bacon // `argLabels` === ['_', 'withSomeValue'] 'doSomething(_:withSomeValue:)' 157*082815dcSEvan Bacon const argLabels = selector.substring(parenthesesIndex + 1, selector.length - 2).split(':'); 158*082815dcSEvan Bacon 159*082815dcSEvan Bacon let searchOffset = 0; 160*082815dcSEvan Bacon const funcCandidateRegExp = new RegExp(`\\sfunc\\s+${funcName}\\(`, 'm'); 161*082815dcSEvan Bacon let funcCandidateOffset = searchFromOffset(contents, funcCandidateRegExp, searchOffset); 162*082815dcSEvan Bacon while (funcCandidateOffset >= 0) { 163*082815dcSEvan Bacon // Parse function parameters 164*082815dcSEvan Bacon const paramsStartOffset = contents.indexOf('(', funcCandidateOffset); 165*082815dcSEvan Bacon const paramsEndOffset = findMatchingBracketPosition(contents, '(', paramsStartOffset); 166*082815dcSEvan Bacon const paramsString = contents.substring(paramsStartOffset + 1, paramsEndOffset); 167*082815dcSEvan Bacon const params = paramsString.split(',').map(parseSwiftFunctionParam); 168*082815dcSEvan Bacon 169*082815dcSEvan Bacon // Prepare offset for next round 170*082815dcSEvan Bacon searchOffset = paramsEndOffset + 1; 171*082815dcSEvan Bacon funcCandidateOffset = searchFromOffset(contents, funcCandidateRegExp, searchOffset); 172*082815dcSEvan Bacon 173*082815dcSEvan Bacon // Try to match function parameters 174*082815dcSEvan Bacon if (argLabels.length !== params.length) { 175*082815dcSEvan Bacon continue; 176*082815dcSEvan Bacon } 177*082815dcSEvan Bacon for (let i = 0; i < argLabels.length; ++i) { 178*082815dcSEvan Bacon if (argLabels[i] !== params[i].argumentLabel) { 179*082815dcSEvan Bacon continue; 180*082815dcSEvan Bacon } 181*082815dcSEvan Bacon } 182*082815dcSEvan Bacon 183*082815dcSEvan Bacon // This function is matched one, get the code block. 184*082815dcSEvan Bacon const codeBlockStart = contents.indexOf('{', paramsEndOffset); 185*082815dcSEvan Bacon const codeBlockEnd = findMatchingBracketPosition(contents, '{', paramsEndOffset); 186*082815dcSEvan Bacon const codeBlock = contents.substring(codeBlockStart, codeBlockEnd + 1); 187*082815dcSEvan Bacon return { 188*082815dcSEvan Bacon start: codeBlockStart, 189*082815dcSEvan Bacon end: codeBlockEnd, 190*082815dcSEvan Bacon code: codeBlock, 191*082815dcSEvan Bacon }; 192*082815dcSEvan Bacon } 193*082815dcSEvan Bacon 194*082815dcSEvan Bacon return null; 195*082815dcSEvan Bacon} 196*082815dcSEvan Bacon 197*082815dcSEvan Baconfunction parseSwiftFunctionParam(paramTuple: string): SwiftFunctionParam { 198*082815dcSEvan Bacon const semiIndex = paramTuple.indexOf(':'); 199*082815dcSEvan Bacon const [argumentLabel, parameterName] = paramTuple.substring(0, semiIndex).split(/\s+/); 200*082815dcSEvan Bacon const typeString = paramTuple.substring(semiIndex + 1).trim(); 201*082815dcSEvan Bacon return { 202*082815dcSEvan Bacon argumentLabel, 203*082815dcSEvan Bacon parameterName, 204*082815dcSEvan Bacon typeString, 205*082815dcSEvan Bacon }; 206*082815dcSEvan Bacon} 207*082815dcSEvan Bacon 208*082815dcSEvan Bacon/** 209*082815dcSEvan Bacon * Insert contents to the swift class block 210*082815dcSEvan Bacon * 211*082815dcSEvan Bacon * @param srcContents source contents 212*082815dcSEvan Bacon * @param declaration class/extension declaration, e.g. 'class AppDelegate' 213*082815dcSEvan Bacon * @param insertion code to append 214*082815dcSEvan Bacon * @param options insertion options 215*082815dcSEvan Bacon * @returns updated contents 216*082815dcSEvan Bacon */ 217*082815dcSEvan Baconexport function insertContentsInsideSwiftClassBlock( 218*082815dcSEvan Bacon srcContents: string, 219*082815dcSEvan Bacon declaration: string, 220*082815dcSEvan Bacon insertion: string, 221*082815dcSEvan Bacon options: { 222*082815dcSEvan Bacon position: 'head' | 'tail'; 223*082815dcSEvan Bacon } 224*082815dcSEvan Bacon): string { 225*082815dcSEvan Bacon const start = srcContents.search(new RegExp(`\\s*${declaration}.*?[\\(\\{]`)); 226*082815dcSEvan Bacon if (start < 0) { 227*082815dcSEvan Bacon throw new Error(`Unable to find class code block - declaration[${declaration}]`); 228*082815dcSEvan Bacon } 229*082815dcSEvan Bacon 230*082815dcSEvan Bacon const { position } = options; 231*082815dcSEvan Bacon if (position === 'head') { 232*082815dcSEvan Bacon const firstBracketIndex = srcContents.indexOf('{', start); 233*082815dcSEvan Bacon srcContents = insertContentsAtOffset(srcContents, insertion, firstBracketIndex + 1); 234*082815dcSEvan Bacon } else if (position === 'tail') { 235*082815dcSEvan Bacon const endBracketIndex = findMatchingBracketPosition(srcContents, '{', start); 236*082815dcSEvan Bacon srcContents = insertContentsAtOffset(srcContents, insertion, endBracketIndex); 237*082815dcSEvan Bacon } 238*082815dcSEvan Bacon return srcContents; 239*082815dcSEvan Bacon} 240*082815dcSEvan Bacon 241*082815dcSEvan Bacon/** 242*082815dcSEvan Bacon * Insert contents to the Swift function block 243*082815dcSEvan Bacon * 244*082815dcSEvan Bacon * @param srcContents source contents 245*082815dcSEvan Bacon * @param selector function selector, e.g. 'doSomething:withSomeValue:' 246*082815dcSEvan Bacon * @param insertion code to insert 247*082815dcSEvan Bacon * @param options insertion options 248*082815dcSEvan Bacon * @returns updated contents 249*082815dcSEvan Bacon */ 250*082815dcSEvan Baconexport function insertContentsInsideSwiftFunctionBlock( 251*082815dcSEvan Bacon srcContents: string, 252*082815dcSEvan Bacon selector: string, 253*082815dcSEvan Bacon insertion: string, 254*082815dcSEvan Bacon options: InsertContentFunctionOptions 255*082815dcSEvan Bacon): string { 256*082815dcSEvan Bacon return insertContentsInsideFunctionBlock(srcContents, selector, insertion, options, 'swift'); 257*082815dcSEvan Bacon} 258*082815dcSEvan Bacon 259*082815dcSEvan Baconfunction insertContentsInsideFunctionBlock( 260*082815dcSEvan Bacon srcContents: string, 261*082815dcSEvan Bacon selector: string, 262*082815dcSEvan Bacon insertion: string, 263*082815dcSEvan Bacon options: InsertContentFunctionOptions, 264*082815dcSEvan Bacon language: 'objc' | 'swift' 265*082815dcSEvan Bacon): string { 266*082815dcSEvan Bacon const codeBlock = 267*082815dcSEvan Bacon language === 'objc' 268*082815dcSEvan Bacon ? findObjcFunctionCodeBlock(srcContents, selector) 269*082815dcSEvan Bacon : findSwiftFunctionCodeBlock(srcContents, selector); 270*082815dcSEvan Bacon if (!codeBlock) { 271*082815dcSEvan Bacon return srcContents; 272*082815dcSEvan Bacon } 273*082815dcSEvan Bacon 274*082815dcSEvan Bacon const { position } = options; 275*082815dcSEvan Bacon const indent = ' '.repeat(options.indent ?? 2); 276*082815dcSEvan Bacon 277*082815dcSEvan Bacon if (position === 'head') { 278*082815dcSEvan Bacon srcContents = insertContentsAtOffset( 279*082815dcSEvan Bacon srcContents, 280*082815dcSEvan Bacon `\n${indent}${insertion}`, 281*082815dcSEvan Bacon codeBlock.start + 1 282*082815dcSEvan Bacon ); 283*082815dcSEvan Bacon } else if (position === 'tail') { 284*082815dcSEvan Bacon srcContents = insertContentsAtOffset(srcContents, `\n${indent}${insertion}`, codeBlock.end - 1); 285*082815dcSEvan Bacon } else if (position === 'tailBeforeLastReturn') { 286*082815dcSEvan Bacon let lastReturnIndex = srcContents.lastIndexOf(' return ', codeBlock.end); 287*082815dcSEvan Bacon if (lastReturnIndex < 0) { 288*082815dcSEvan Bacon throw new Error(`Cannot find last return statement:\n${srcContents}`); 289*082815dcSEvan Bacon } 290*082815dcSEvan Bacon lastReturnIndex += 1; // +1 for the prefix space 291*082815dcSEvan Bacon srcContents = insertContentsAtOffset(srcContents, `${insertion}\n${indent}`, lastReturnIndex); 292*082815dcSEvan Bacon } 293*082815dcSEvan Bacon 294*082815dcSEvan Bacon return srcContents; 295*082815dcSEvan Bacon} 296