summaryrefslogtreecommitdiff
path: root/platform/darwin/scripts/update-examples.js
blob: d453c0e4ba187fa841e5683ae02da757813beccb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
'use strict';

const fs = require('fs');
const execFileSync = require('child_process').execFileSync;
const _ = require('lodash');

const examplesSrc = fs.readFileSync('platform/darwin/test/MGLDocumentationExampleTests.swift', 'utf8');

// Regex extracts the following block
// /** Front matter to describe the example. **/
// func testMGLClass$member() {
//     ...
//     // #-example-code
//     let sampleCode: String?
//     // #-end-example-code
//     ...
// }
//
// into the following regex groups:
// 1 (test method name): "MGLClass" or "MGLClass$member" or "MGLClass$initWithArg_anotherArg_"
// 2 (indentation): "    "
// 3 (sample code): "let sampleCode: String?"
const exampleRegex = /func test([\w$]+)\s*\(\)\s*\{[^]*?\n([ \t]+)\/\/#-example-code\n([^]+?)\n\2\/\/#-end-example-code\n/gm;

/**
 * Returns the given source with example code inserted into the documentation
 * comment for the symbol at the given one-based line.
 *
 * @param {String} src Source code to insert the example code into.
 * @param {Number} line One-based line number of the symbol being documented.
 * @param {String} exampleCode Example code to insert.
 * @returns {String} `src` with `exampleCode` inserted just above `line`.
 */
function completeSymbolInSource(src, line, exampleCode) {
  // Split the file contents right before the symbol declaration (but after its documentation comment).
  let srcUpToSymbol = src.split('\n', line - 1).join('\n');
  let srcFromSymbol = src.substr(srcUpToSymbol.length);

  // Match the documentation comment block that is not followed by the beginning or end of a declaration.
  let commentMatch = srcUpToSymbol.match(/\/\*\*\s*(?:[^*]|\*(?!\/))+?\s*\*\/[^;{}]*?$/);

  // Replace the Swift code block with the test method’s contents.
  let completedComment = commentMatch[0].replace(/^([ \t]*)```swift\n[^]*?```/m, function (m, indentation) {
    // Apply the original indentation to each line.
    return ('```swift\n' + exampleCode + '\n```').replace(/^/gm, indentation);
  });

  // Splice the modified comment into the overall file contents.
  srcUpToSymbol = (srcUpToSymbol.substr(0, commentMatch.index) + completedComment +
                   srcUpToSymbol.substr(commentMatch.index + commentMatch[0].length));
  return srcUpToSymbol + srcFromSymbol;
}

let examples = {};
let match;
while ((match = exampleRegex.exec(examplesSrc)) !== null) {
  let testMethodName = match[1],
      indentation = match[2],
      exampleCode = match[3];

  // Trim leading whitespace from the example code.
  exampleCode = exampleCode.replace(new RegExp('^' + indentation, 'gm'), '');

  examples[testMethodName] = exampleCode;
}

function completeExamples(os) {
  console.log(`Installing ${os} SDK examples…`);

  let sdk = os === 'iOS' ? 'iphonesimulator' : 'macosx';
  let sysroot = execFileSync('xcrun', ['--show-sdk-path', '--sdk', sdk]).toString().trim();
  let umbrellaPath = `platform/${os.toLowerCase()}/src/Mapbox.h`;
  let docArgs = ['doc', '--objc', umbrellaPath, '--',
                 '-x', 'objective-c', '-I', 'platform/darwin/src/', '-isysroot', sysroot];
  let docStr = execFileSync('sourcekitten', docArgs).toString().trim();
  let docJson = JSON.parse(docStr);
  _.forEach(docJson, function (result) {
    _.forEach(result, function (structure, path) {
      let src;
      let newSrc;
      // Recursively search for code blocks in documentation comments and populate
      // them with example code from the test methods. Find and replace the code
      // blocks in reverse to keep the SourceKitten line numbers accurate.
      _.forEachRight(structure['key.substructure'], function completeSubstructure(substructure, idx, substructures, symbolPath) {
        if (!symbolPath) {
          symbolPath = [substructure['key.name']];
        }
        _.forEachRight(substructure['key.substructure'], function (substructure, idx, substructures) {
          completeSubstructure(substructure, idx, substructures, _.concat(symbolPath, substructure['key.name']));
        });

        let comment = substructure['key.doc.comment'];
        if (!comment || !comment.match(/^(?:\s*)```swift\n/m)) {
          return;
        }

        // Lazily read in the existing file.
        if (!src) {
          newSrc = src = fs.readFileSync(path, 'utf8');
        }

        // Get the contents of the test method whose name matches the symbol path.
        let testMethodName = symbolPath.join('$').replace(/\$[+-]/, '$').replace(/:/g, '_');
        let example = examples[testMethodName];
        if (!example) {
          console.error(`MGLDocumentationExampleTests.test${testMethodName}() not found.`);
          process.exit(1);
        }

        // Resolve conditional compilation blocks.
        example = example.replace(/^(\s*)#if\s+os\((iOS|macOS)\)\n([^]*?)(?:^\1#else\n([^]*?))?^\1#endif\b\n?/gm,
                                  function (m, indentation, ifOs, ifCase, elseCase) {
          return (os === ifOs ? ifCase : elseCase).replace(new RegExp('^    ', 'gm'), '');
        }).replace(/\n$/, '');

        // Insert the test method contents into the documentation comment just
        // above the substructure.
        let startLine = substructure['key.parsed_scope.start'];
        newSrc = completeSymbolInSource(newSrc, startLine, example);
      });

      if (!src) {
        return;
      }

      // Write out the modified file contents.
      if (src === newSrc) {
        console.log('Skipping', path);
      } else {
        console.log('Updating', path);
        if (['0', 'false'].indexOf(process.env.DRY_RUN || '0') !== -1) {
          fs.writeFileSync(path, newSrc);
        }
      }
    });
  });
}

function ensureSourceKittenIsInstalled() {
  try {
    execFileSync('which', ['sourcekitten']);
  } catch (e) {
    console.log(`Installing SourceKitten via Homebrew…`);
    execFileSync('brew', ['install', 'sourcekitten']);
  }
}

ensureSourceKittenIsInstalled();

// Where a particular comment is part of both SDKs, prefer the iOS example.
completeExamples('macOS');
completeExamples('iOS');