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