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