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