• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

homer0 / jsdoc-ts-utils / 4457244662

pending completion
4457244662

push

github

GitHub
Merge pull request #40 from homer0/next

103 of 103 branches covered (100.0%)

Branch coverage included in aggregate %.

1011 of 1011 relevant lines covered (100.0%)

2.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

100.0
/src/features/extendTypes.js
1
/**
1✔
2
 * @typedef {Object} ExtendTypesCommentWithProperties
1✔
3
 * @property {string}   name        The name of the type.
1✔
4
 * @property {string}   augments    The name of the type it extends.
1✔
5
 * @property {string}   comment     The raw comment.
1✔
6
 * @property {number}   linesCount  How many lines the comment has.
1✔
7
 * @property {string[]} lines       The list of lines from the comment that can be used on
1✔
8
 *                                  the type that it's being extended.
1✔
9
 * @property {number}   usedLines   This counter gets modified during parsing and it
1✔
10
 *                                  represents how many of the `lines` were used: if the
1✔
11
 *                                  type with the intersection already has one line, it's
1✔
12
 *                                  not added, but the class needs to know how many lines
1✔
13
 *                                  it needs to use to replace the comment when removed.
1✔
14
 * @ignore
1✔
15
 */
1✔
16

1✔
17
/**
1✔
18
 * This class allows the use of intersection types in oder to define types extensions by
1✔
19
 * transforming the code in two ways:
1✔
20
 * 1. If one of the types of the intersection uses `augments`/`extends` for the intersection type,
1✔
21
 * all its "lines" are moved to the intersection type and it gets removed.
1✔
22
 * 2. If the first scenario is not possible, it trasforms `&` into `|`. Yes, it becomes a union
1✔
23
 * type, but it's as closer as we can get with pure JSDoc.
1✔
24
 */
1✔
25
class ExtendTypes {
1✔
26
  /**
1✔
27
   * @param {EventEmitter} events       To hook to the necessary events to parse the code.
1✔
28
   * @param {EventNames}   EVENT_NAMES  To get the name of the events the class needs to
1✔
29
   *                                    listen for.
1✔
30
   */
1✔
31
  constructor(events, EVENT_NAMES) {
1✔
32
    /**
5✔
33
     * The list of comments that use intersections on a `typedef` statements.
5✔
34
     *
5✔
35
     * @type {string[]}
5✔
36
     * @access protected
5✔
37
     * @ignore
5✔
38
     */
5✔
39
    this._commentsWithIntersections = [];
5✔
40
    /**
5✔
41
     * A list with the information of comments that extend/agument types with
5✔
42
     * intersections.
5✔
43
     *
5✔
44
     * @type {ExtendTypesCommentWithProperties[]}
5✔
45
     * @access protected
5✔
46
     * @ignore
5✔
47
     */
5✔
48
    this._commentsWithProperties = [];
5✔
49
    // Setup the listeners.
5✔
50
    events.on(EVENT_NAMES.newComment, this._readComment.bind(this));
5✔
51
    events.on(EVENT_NAMES.commentsReady, this._replaceComments.bind(this));
5✔
52
  }
5✔
53
  /**
1✔
54
   * Given a list of types from an instersection definition, this methods tries to find a
1✔
55
   * comment that extends one of those types.
1✔
56
   * If the method finds only one comment for a type, it returns it, otherwise, it returns
1✔
57
   * `null`:
1✔
58
   * the class only supports extending one type.
1✔
59
   *
1✔
60
   * @param {string}   name   The name of the type that uses intersection.
1✔
61
   * @param {string[]} types  The list of types in the intersection.
1✔
62
   * @returns {?ExtendTypesCommentWithProperties}
1✔
63
   * @access protected
1✔
64
   * @ignore
1✔
65
   */
1✔
66
  _getCommentWithProperties(name, types) {
1✔
67
    const comments = types
3✔
68
      .map((type) =>
3✔
69
        this._commentsWithProperties.find(
6✔
70
          (comment) => comment.name === type && comment.augments === name,
6✔
71
        ),
3✔
72
      )
3✔
73
      .filter((comment) => comment);
3✔
74

3✔
75
    return comments.length === 1 ? comments[0] : null;
3✔
76
  }
3✔
77
  /**
1✔
78
   * Parses a JSDock comment block for a type definition that extends a type with
1✔
79
   * intersection.
1✔
80
   *
1✔
81
   * @param {string} comment  The raw comment to parse.
1✔
82
   * @returns {ExtendTypesCommentWithProperties}
1✔
83
   * @access protected
1✔
84
   * @ignore
1✔
85
   */
1✔
86
  _getCommentWithPropertiesInfo(comment) {
1✔
87
    const [, name] = /@typedef\s+\{[^\}]+\}[\s\*]*(.*?)\s/i.exec(comment);
2✔
88
    const [, augments] = /@(?:augments|extends)\s+(.*?)\s/i.exec(comment);
2✔
89
    const allLines = comment.split('\n');
2✔
90
    const linesCount = allLines.length;
2✔
91
    const lines = allLines.filter(
2✔
92
      (line) =>
2✔
93
        line.match(/\w/) && !line.match(/^\s*\*\s*@(?:typedef|augments|extends)\s+/i),
2✔
94
    );
2✔
95
    return {
2✔
96
      name: name.trim(),
2✔
97
      augments: augments.trim(),
2✔
98
      comment,
2✔
99
      linesCount,
2✔
100
      usedLines: 0,
2✔
101
      lines,
2✔
102
    };
2✔
103
  }
2✔
104
  /**
1✔
105
   * This is called every time a new JSDoc comment block is found on a file. It validates
1✔
106
   * if the block uses intersection or if it's a `typedef` that extends another type in
1✔
107
   * order to save it to be parsed later.
1✔
108
   *
1✔
109
   * @param {string} comment  The comment to analyze.
1✔
110
   * @access protected
1✔
111
   * @ignore
1✔
112
   */
1✔
113
  _readComment(comment) {
1✔
114
    if (comment.match(/\*\s*@typedef\s+\{\s*\w+\s*&\s*\w+/i)) {
6✔
115
      this._commentsWithIntersections.push(comment);
3✔
116
    } else if (
3✔
117
      comment.match(/\*\s*@typedef\s+\{/i) &&
3✔
118
      comment.match(/\*\s*@(?:augments|extends)\s+\w+/i)
3✔
119
    ) {
3✔
120
      this._commentsWithProperties.push(this._getCommentWithPropertiesInfo(comment));
2✔
121
    }
2✔
122
  }
6✔
123
  /**
1✔
124
   * Replaces the JSDoc comment blocks for type definitions that extend types with
1✔
125
   * intersections with empty lines.
1✔
126
   *
1✔
127
   * @param {string} source  The code of the file where the comments should be removed.
1✔
128
   * @returns {string}
1✔
129
   * @access protected
1✔
130
   * @ignore
1✔
131
   */
1✔
132
  _removeCommentsWithProperties(source) {
1✔
133
    return this._commentsWithProperties.reduce(
4✔
134
      (acc, comment) =>
4✔
135
        acc.replace(
2✔
136
          comment.comment,
2✔
137
          new Array(comment.linesCount - comment.usedLines).fill('').join('\n'),
2✔
138
        ),
4✔
139
      source,
4✔
140
    );
4✔
141
  }
4✔
142
  /**
1✔
143
   * This is called after all the JSDoc comments block for a file were found and the
1✔
144
   * plugin is ready to make changes.
1✔
145
   * The method first _"fixes"_ the `typedef` statements with intersections and then
1✔
146
   * removes the comments for types that extend another types.
1✔
147
   *
1✔
148
   * @param {string} source  The code of the file being parsed.
1✔
149
   * @returns {string}
1✔
150
   * @access protected
1✔
151
   * @ignore
1✔
152
   */
1✔
153
  _replaceComments(source) {
1✔
154
    let result = this._replaceDefinitions(source);
4✔
155
    result = this._removeCommentsWithProperties(result);
4✔
156
    this._commentsWithIntersections = [];
4✔
157
    this._commentsWithProperties = [];
4✔
158
    return result;
4✔
159
  }
4✔
160
  /**
1✔
161
   * Parses the comments with intersections, validate if there's a type extending them
1✔
162
   * from where they can take "lines", or if they should be transformed into unions.
1✔
163
   *
1✔
164
   * @param {string} source  The code of the file where the comments should be replaced.
1✔
165
   * @returns {string}
1✔
166
   * @access protected
1✔
167
   * @ignore
1✔
168
   */
1✔
169
  _replaceDefinitions(source) {
1✔
170
    return this._commentsWithIntersections.reduce((acc, comment) => {
4✔
171
      // Extract the `typedef` types and name.
3✔
172
      const [typedefLine, rawTypes, name] = /@typedef\s*\{([^\}]+)\}[\s\*]*(.*?)\s/i.exec(
3✔
173
        comment,
3✔
174
      );
3✔
175
      // Transform the types into an array.
3✔
176
      const types = rawTypes.split('&').map((type) => type.trim());
3✔
177

3✔
178
      let replacement;
3✔
179
      // Try to find a type that extends this one.
3✔
180
      const commentWithProps = this._getCommentWithProperties(name, types);
3✔
181
      if (commentWithProps) {
3✔
182
        // If there was a type extending it...
2✔
183
        // Find the "real type" that it's being extended.
2✔
184
        const [baseType] = types.filter((type) => type !== commentWithProps.name);
2✔
185
        // Remove the intersection and leave just the "real type"
2✔
186
        const newTypedefLine = typedefLine.replace(rawTypes, baseType);
2✔
187
        // Replace the `typedef` with the new one, with the "real type".
2✔
188
        const newComment = comment.replace(typedefLine, newTypedefLine);
2✔
189
        // Transform the comment into an array of lines.
2✔
190
        const lines = newComment.split('\n');
2✔
191
        // Remove the line with `*/` so new lines can be added.
2✔
192
        const closingLine = lines.pop();
2✔
193
        /**
2✔
194
         * For each line of the type that extends the definition, check if it doesn't
2✔
195
         * already exists on the comment (this can happen with `access` or `memberof`
2✔
196
         * statements),
2✔
197
         * and if it doesn't, it not only adds it but it also increments the counter that
2✔
198
         * tracks how many lines of the comment are being used.
2✔
199
         *
2✔
200
         * The lines that are moved are counted so the class can later replace the comment
2✔
201
         * with enough empty lines so it won't mess up the line number of the rest of the
2✔
202
         * types.
2✔
203
         * For example: If we had a type with intersection with a single line, a type that
2✔
204
         * extends it with 3 lines lines with `property` and one with `access`
2✔
205
         * (plus the `typedef` and the `extends`/`augments`) and the intersection type
2✔
206
         * already has `access`, the class would replace the comment with `5` empty lines.
2✔
207
         * The comment had a total of 8 lines, 3 `property`, `typedef`,
2✔
208
         * `extends`/`augments`, `access` and the opening and closing lines; but the
2✔
209
         * properties were moved to another type.
2✔
210
         *
2✔
211
         * @ignore
2✔
212
         */
2✔
213
        const info = commentWithProps.lines.reduce(
2✔
214
          (infoAcc, line) => {
2✔
215
            let nextInfoAcc;
8✔
216
            if (infoAcc.lines.includes(line)) {
8✔
217
              nextInfoAcc = infoAcc;
2✔
218
            } else {
8✔
219
              nextInfoAcc = {
6✔
220
                lines: [...infoAcc.lines, line],
6✔
221
                count: infoAcc.count + 1,
6✔
222
              };
6✔
223
            }
6✔
224

8✔
225
            return nextInfoAcc;
8✔
226
          },
2✔
227
          {
2✔
228
            lines,
2✔
229
            count: commentWithProps.usedLines,
2✔
230
          },
2✔
231
        );
2✔
232
        // Add the closing line and put the comment back together.
2✔
233
        info.lines.push(closingLine);
2✔
234
        replacement = info.lines.join('\n');
2✔
235
        // Update the counter with the used lines.
2✔
236
        commentWithProps.usedLines = info.count;
2✔
237
      } else {
3✔
238
        // No comment was found, so transform the intersection into a union.
1✔
239
        const newTypedefLine = typedefLine.replace(rawTypes, types.join('|'));
1✔
240
        replacement = comment.replace(typedefLine, newTypedefLine);
1✔
241
      }
1✔
242

3✔
243
      return acc.replace(comment, replacement);
3✔
244
    }, source);
4✔
245
  }
4✔
246
}
1✔
247

1✔
248
module.exports.ExtendTypes = ExtendTypes;
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc