@flisk/analyze-tracking 0.7.2 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +1 -1
- package/package.json +9 -7
- package/src/analyze/go/astTraversal.js +121 -0
- package/src/analyze/go/constants.js +20 -0
- package/src/analyze/go/eventDeduplicator.js +47 -0
- package/src/analyze/go/eventExtractor.js +156 -0
- package/src/analyze/go/goAstParser/constants.js +39 -0
- package/src/analyze/go/goAstParser/expressionParser.js +281 -0
- package/src/analyze/go/goAstParser/index.js +52 -0
- package/src/analyze/go/goAstParser/statementParser.js +387 -0
- package/src/analyze/go/goAstParser/tokenizer.js +196 -0
- package/src/analyze/go/goAstParser/typeParser.js +202 -0
- package/src/analyze/go/goAstParser/utils.js +99 -0
- package/src/analyze/go/index.js +55 -0
- package/src/analyze/go/propertyExtractor.js +670 -0
- package/src/analyze/go/trackingDetector.js +71 -0
- package/src/analyze/go/trackingExtractor.js +54 -0
- package/src/analyze/go/typeContext.js +88 -0
- package/src/analyze/go/utils.js +215 -0
- package/src/analyze/index.js +11 -6
- package/src/analyze/javascript/constants.js +115 -0
- package/src/analyze/javascript/detectors/analytics-source.js +119 -0
- package/src/analyze/javascript/detectors/index.js +10 -0
- package/src/analyze/javascript/extractors/event-extractor.js +179 -0
- package/src/analyze/javascript/extractors/index.js +13 -0
- package/src/analyze/javascript/extractors/property-extractor.js +172 -0
- package/src/analyze/javascript/index.js +38 -0
- package/src/analyze/javascript/parser.js +126 -0
- package/src/analyze/javascript/utils/function-finder.js +123 -0
- package/src/analyze/python/index.js +111 -0
- package/src/analyze/python/pythonTrackingAnalyzer.py +814 -0
- package/src/analyze/ruby/detectors.js +46 -0
- package/src/analyze/ruby/extractors.js +258 -0
- package/src/analyze/ruby/index.js +51 -0
- package/src/analyze/ruby/traversal.js +123 -0
- package/src/analyze/ruby/types.js +30 -0
- package/src/analyze/ruby/visitor.js +66 -0
- package/src/analyze/typescript/constants.js +109 -0
- package/src/analyze/typescript/detectors/analytics-source.js +120 -0
- package/src/analyze/typescript/detectors/index.js +10 -0
- package/src/analyze/typescript/extractors/event-extractor.js +269 -0
- package/src/analyze/typescript/extractors/index.js +14 -0
- package/src/analyze/typescript/extractors/property-extractor.js +395 -0
- package/src/analyze/typescript/index.js +48 -0
- package/src/analyze/typescript/parser.js +131 -0
- package/src/analyze/typescript/utils/function-finder.js +114 -0
- package/src/analyze/typescript/utils/type-resolver.js +193 -0
- package/src/generateDescriptions/index.js +81 -0
- package/src/generateDescriptions/llmUtils.js +33 -0
- package/src/generateDescriptions/promptUtils.js +62 -0
- package/src/generateDescriptions/schemaUtils.js +61 -0
- package/src/index.js +7 -2
- package/src/{fileProcessor.js → utils/fileProcessor.js} +5 -0
- package/src/{repoDetails.js → utils/repoDetails.js} +5 -0
- package/src/{yamlGenerator.js → utils/yamlGenerator.js} +5 -0
- package/src/analyze/analyzeGoFile.js +0 -1164
- package/src/analyze/analyzeJsFile.js +0 -87
- package/src/analyze/analyzePythonFile.js +0 -42
- package/src/analyze/analyzeRubyFile.js +0 -419
- package/src/analyze/analyzeTsFile.js +0 -192
- package/src/analyze/go2json.js +0 -1069
- package/src/analyze/helpers.js +0 -656
- package/src/analyze/pythonTrackingAnalyzer.py +0 -541
- package/src/generateDescriptions.js +0 -196
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Analytics source detection module
|
|
3
|
+
* @module analyze/typescript/detectors/analytics-source
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const ts = require('typescript');
|
|
7
|
+
const { ANALYTICS_PROVIDERS } = require('../constants');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Detects the analytics provider from a CallExpression node
|
|
11
|
+
* @param {Object} node - TypeScript CallExpression node
|
|
12
|
+
* @param {string} [customFunction] - Custom function name to detect
|
|
13
|
+
* @returns {string} The detected analytics source or 'unknown'
|
|
14
|
+
*/
|
|
15
|
+
function detectAnalyticsSource(node, customFunction) {
|
|
16
|
+
if (!node.expression) {
|
|
17
|
+
return 'unknown';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Check for custom function first
|
|
21
|
+
if (customFunction && isCustomFunction(node, customFunction)) {
|
|
22
|
+
return 'custom';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check for function-based providers (e.g., gtag)
|
|
26
|
+
const functionSource = detectFunctionBasedProvider(node);
|
|
27
|
+
if (functionSource !== 'unknown') {
|
|
28
|
+
return functionSource;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check for member-based providers (e.g., analytics.track)
|
|
32
|
+
const memberSource = detectMemberBasedProvider(node);
|
|
33
|
+
if (memberSource !== 'unknown') {
|
|
34
|
+
return memberSource;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return 'unknown';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Checks if the node is a custom function call
|
|
42
|
+
* @param {Object} node - TypeScript CallExpression node
|
|
43
|
+
* @param {string} customFunction - Custom function name
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
function isCustomFunction(node, customFunction) {
|
|
47
|
+
return ts.isIdentifier(node.expression) &&
|
|
48
|
+
node.expression.escapedText === customFunction;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Detects function-based analytics providers
|
|
53
|
+
* @param {Object} node - TypeScript CallExpression node
|
|
54
|
+
* @returns {string} Provider name or 'unknown'
|
|
55
|
+
*/
|
|
56
|
+
function detectFunctionBasedProvider(node) {
|
|
57
|
+
if (!ts.isIdentifier(node.expression)) {
|
|
58
|
+
return 'unknown';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const functionName = node.expression.escapedText;
|
|
62
|
+
|
|
63
|
+
for (const provider of Object.values(ANALYTICS_PROVIDERS)) {
|
|
64
|
+
if (provider.type === 'function' && provider.functionName === functionName) {
|
|
65
|
+
return provider.name;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return 'unknown';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Detects member expression-based analytics providers
|
|
74
|
+
* @param {Object} node - TypeScript CallExpression node
|
|
75
|
+
* @returns {string} Provider name or 'unknown'
|
|
76
|
+
*/
|
|
77
|
+
function detectMemberBasedProvider(node) {
|
|
78
|
+
if (!ts.isPropertyAccessExpression(node.expression)) {
|
|
79
|
+
return 'unknown';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const objectName = node.expression.expression.escapedText;
|
|
83
|
+
const methodName = node.expression.name.escapedText;
|
|
84
|
+
|
|
85
|
+
if (!objectName || !methodName) {
|
|
86
|
+
return 'unknown';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const provider of Object.values(ANALYTICS_PROVIDERS)) {
|
|
90
|
+
if (provider.type === 'member' && matchesMemberProvider(provider, objectName, methodName)) {
|
|
91
|
+
return provider.name;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return 'unknown';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Checks if object and method names match a provider configuration
|
|
100
|
+
* @param {Object} provider - Provider configuration
|
|
101
|
+
* @param {string} objectName - Object name from TypeScript AST
|
|
102
|
+
* @param {string} methodName - Method name from TypeScript AST
|
|
103
|
+
* @returns {boolean}
|
|
104
|
+
*/
|
|
105
|
+
function matchesMemberProvider(provider, objectName, methodName) {
|
|
106
|
+
if (provider.methodName !== methodName) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Handle providers with multiple possible object names (e.g., mParticle/mparticle)
|
|
111
|
+
if (provider.objectNames) {
|
|
112
|
+
return provider.objectNames.includes(objectName);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return provider.objectName === objectName;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = {
|
|
119
|
+
detectAnalyticsSource
|
|
120
|
+
};
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Event extraction logic for different analytics providers
|
|
3
|
+
* @module analyze/typescript/extractors/event-extractor
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const ts = require('typescript');
|
|
7
|
+
const { extractProperties } = require('./property-extractor');
|
|
8
|
+
const { resolveIdentifierToInitializer } = require('../utils/type-resolver');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Event data structure
|
|
12
|
+
* @typedef {Object} EventData
|
|
13
|
+
* @property {string|null} eventName - The event name
|
|
14
|
+
* @property {Object|null} propertiesNode - AST node containing event properties
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Provider-specific extraction strategies
|
|
19
|
+
*/
|
|
20
|
+
const EXTRACTION_STRATEGIES = {
|
|
21
|
+
googleanalytics: extractGoogleAnalyticsEvent,
|
|
22
|
+
snowplow: extractSnowplowEvent,
|
|
23
|
+
mparticle: extractMparticleEvent,
|
|
24
|
+
default: extractDefaultEvent
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extracts event information from a CallExpression node
|
|
29
|
+
* @param {Object} node - TypeScript CallExpression node
|
|
30
|
+
* @param {string} source - Analytics provider source
|
|
31
|
+
* @param {Object} checker - TypeScript type checker
|
|
32
|
+
* @param {Object} sourceFile - TypeScript source file
|
|
33
|
+
* @returns {EventData} Extracted event data
|
|
34
|
+
*/
|
|
35
|
+
function extractEventData(node, source, checker, sourceFile) {
|
|
36
|
+
const strategy = EXTRACTION_STRATEGIES[source] || EXTRACTION_STRATEGIES.default;
|
|
37
|
+
return strategy(node, checker, sourceFile);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Extracts Google Analytics event data
|
|
42
|
+
* @param {Object} node - CallExpression node
|
|
43
|
+
* @param {Object} checker - TypeScript type checker
|
|
44
|
+
* @param {Object} sourceFile - TypeScript source file
|
|
45
|
+
* @returns {EventData}
|
|
46
|
+
*/
|
|
47
|
+
function extractGoogleAnalyticsEvent(node, checker, sourceFile) {
|
|
48
|
+
if (!node.arguments || node.arguments.length < 3) {
|
|
49
|
+
return { eventName: null, propertiesNode: null };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// gtag('event', 'event_name', { properties })
|
|
53
|
+
const eventName = getStringValue(node.arguments[1]);
|
|
54
|
+
const propertiesNode = node.arguments[2];
|
|
55
|
+
|
|
56
|
+
return { eventName, propertiesNode };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Extracts Snowplow event data
|
|
61
|
+
* @param {Object} node - CallExpression node
|
|
62
|
+
* @param {Object} checker - TypeScript type checker
|
|
63
|
+
* @param {Object} sourceFile - TypeScript source file
|
|
64
|
+
* @returns {EventData}
|
|
65
|
+
*/
|
|
66
|
+
function extractSnowplowEvent(node, checker, sourceFile) {
|
|
67
|
+
if (!node.arguments || node.arguments.length === 0) {
|
|
68
|
+
return { eventName: null, propertiesNode: null };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// tracker.track(buildStructEvent({ action: 'event_name', ... }))
|
|
72
|
+
const firstArg = node.arguments[0];
|
|
73
|
+
|
|
74
|
+
// Check if it's a direct buildStructEvent call
|
|
75
|
+
if (ts.isCallExpression(firstArg) &&
|
|
76
|
+
ts.isIdentifier(firstArg.expression) &&
|
|
77
|
+
firstArg.expression.escapedText === 'buildStructEvent' &&
|
|
78
|
+
firstArg.arguments.length > 0) {
|
|
79
|
+
const structEventArg = firstArg.arguments[0];
|
|
80
|
+
if (ts.isObjectLiteralExpression(structEventArg)) {
|
|
81
|
+
const actionProperty = findPropertyByKey(structEventArg, 'action');
|
|
82
|
+
const eventName = actionProperty ? getStringValue(actionProperty.initializer) : null;
|
|
83
|
+
return { eventName, propertiesNode: structEventArg };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Check if it's a variable reference
|
|
87
|
+
else if (ts.isIdentifier(firstArg)) {
|
|
88
|
+
const resolvedNode = resolveIdentifierToInitializer(checker, firstArg, sourceFile);
|
|
89
|
+
if (resolvedNode && ts.isCallExpression(resolvedNode) &&
|
|
90
|
+
ts.isIdentifier(resolvedNode.expression) &&
|
|
91
|
+
resolvedNode.expression.escapedText === 'buildStructEvent' &&
|
|
92
|
+
resolvedNode.arguments.length > 0) {
|
|
93
|
+
const structEventArg = resolvedNode.arguments[0];
|
|
94
|
+
if (ts.isObjectLiteralExpression(structEventArg)) {
|
|
95
|
+
const actionProperty = findPropertyByKey(structEventArg, 'action');
|
|
96
|
+
const eventName = actionProperty ? getStringValue(actionProperty.initializer) : null;
|
|
97
|
+
return { eventName, propertiesNode: structEventArg };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { eventName: null, propertiesNode: null };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Extracts mParticle event data
|
|
107
|
+
* @param {Object} node - CallExpression node
|
|
108
|
+
* @param {Object} checker - TypeScript type checker
|
|
109
|
+
* @param {Object} sourceFile - TypeScript source file
|
|
110
|
+
* @returns {EventData}
|
|
111
|
+
*/
|
|
112
|
+
function extractMparticleEvent(node, checker, sourceFile) {
|
|
113
|
+
if (!node.arguments || node.arguments.length < 3) {
|
|
114
|
+
return { eventName: null, propertiesNode: null };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// mParticle.logEvent('event_name', mParticle.EventType.Navigation, { properties })
|
|
118
|
+
const eventName = getStringValue(node.arguments[0]);
|
|
119
|
+
const propertiesNode = node.arguments[2];
|
|
120
|
+
|
|
121
|
+
return { eventName, propertiesNode };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Default event extraction for standard providers
|
|
126
|
+
* @param {Object} node - CallExpression node
|
|
127
|
+
* @param {Object} checker - TypeScript type checker
|
|
128
|
+
* @param {Object} sourceFile - TypeScript source file
|
|
129
|
+
* @returns {EventData}
|
|
130
|
+
*/
|
|
131
|
+
function extractDefaultEvent(node, checker, sourceFile) {
|
|
132
|
+
if (!node.arguments || node.arguments.length < 2) {
|
|
133
|
+
return { eventName: null, propertiesNode: null };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// provider.track('event_name', { properties })
|
|
137
|
+
const eventName = getStringValue(node.arguments[0]);
|
|
138
|
+
const propertiesNode = node.arguments[1];
|
|
139
|
+
|
|
140
|
+
return { eventName, propertiesNode };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Processes extracted event data into final event object
|
|
145
|
+
* @param {EventData} eventData - Raw event data
|
|
146
|
+
* @param {string} source - Analytics source
|
|
147
|
+
* @param {string} filePath - File path
|
|
148
|
+
* @param {number} line - Line number
|
|
149
|
+
* @param {string} functionName - Containing function name
|
|
150
|
+
* @param {Object} checker - TypeScript type checker
|
|
151
|
+
* @param {Object} sourceFile - TypeScript source file
|
|
152
|
+
* @returns {Object|null} Processed event object or null
|
|
153
|
+
*/
|
|
154
|
+
function processEventData(eventData, source, filePath, line, functionName, checker, sourceFile) {
|
|
155
|
+
const { eventName, propertiesNode } = eventData;
|
|
156
|
+
|
|
157
|
+
if (!eventName || !propertiesNode) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let properties = null;
|
|
162
|
+
|
|
163
|
+
// Check if properties is an object literal
|
|
164
|
+
if (ts.isObjectLiteralExpression(propertiesNode)) {
|
|
165
|
+
properties = extractProperties(checker, propertiesNode);
|
|
166
|
+
}
|
|
167
|
+
// Check if properties is an identifier (variable reference)
|
|
168
|
+
else if (ts.isIdentifier(propertiesNode)) {
|
|
169
|
+
const resolvedNode = resolveIdentifierToInitializer(checker, propertiesNode, sourceFile);
|
|
170
|
+
if (resolvedNode && ts.isObjectLiteralExpression(resolvedNode)) {
|
|
171
|
+
properties = extractProperties(checker, resolvedNode);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!properties) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Special handling for Snowplow: remove 'action' from properties
|
|
180
|
+
if (source === 'snowplow' && properties.action) {
|
|
181
|
+
delete properties.action;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Clean up any unresolved type markers
|
|
185
|
+
const cleanedProperties = cleanupProperties(properties);
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
eventName,
|
|
189
|
+
source,
|
|
190
|
+
properties: cleanedProperties,
|
|
191
|
+
filePath,
|
|
192
|
+
line,
|
|
193
|
+
functionName
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Gets string value from a TypeScript AST node
|
|
199
|
+
* @param {Object} node - TypeScript AST node
|
|
200
|
+
* @returns {string|null} String value or null
|
|
201
|
+
*/
|
|
202
|
+
function getStringValue(node) {
|
|
203
|
+
if (!node) return null;
|
|
204
|
+
if (ts.isStringLiteral(node)) {
|
|
205
|
+
return node.text;
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Finds a property by key in an ObjectLiteralExpression
|
|
212
|
+
* @param {Object} objectNode - ObjectLiteralExpression node
|
|
213
|
+
* @param {string} key - Property key to find
|
|
214
|
+
* @returns {Object|null} Property node or null
|
|
215
|
+
*/
|
|
216
|
+
function findPropertyByKey(objectNode, key) {
|
|
217
|
+
if (!objectNode.properties) return null;
|
|
218
|
+
|
|
219
|
+
return objectNode.properties.find(prop => {
|
|
220
|
+
if (prop.name) {
|
|
221
|
+
if (ts.isIdentifier(prop.name)) {
|
|
222
|
+
return prop.name.escapedText === key;
|
|
223
|
+
}
|
|
224
|
+
if (ts.isStringLiteral(prop.name)) {
|
|
225
|
+
return prop.name.text === key;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Cleans up properties by removing unresolved type markers
|
|
234
|
+
* @param {Object} properties - Properties object
|
|
235
|
+
* @returns {Object} Cleaned properties
|
|
236
|
+
*/
|
|
237
|
+
function cleanupProperties(properties) {
|
|
238
|
+
const cleaned = {};
|
|
239
|
+
|
|
240
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
241
|
+
if (value && typeof value === 'object') {
|
|
242
|
+
// Remove __unresolved marker
|
|
243
|
+
if (value.__unresolved) {
|
|
244
|
+
delete value.__unresolved;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Recursively clean nested properties
|
|
248
|
+
if (value.properties) {
|
|
249
|
+
value.properties = cleanupProperties(value.properties);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Clean array item properties
|
|
253
|
+
if (value.type === 'array' && value.items && value.items.properties) {
|
|
254
|
+
value.items.properties = cleanupProperties(value.items.properties);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
cleaned[key] = value;
|
|
258
|
+
} else {
|
|
259
|
+
cleaned[key] = value;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return cleaned;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
module.exports = {
|
|
267
|
+
extractEventData,
|
|
268
|
+
processEventData
|
|
269
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Central export for all extractor modules
|
|
3
|
+
* @module analyze/typescript/extractors
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { extractEventData, processEventData } = require('./event-extractor');
|
|
7
|
+
const { extractProperties, extractInterfaceProperties } = require('./property-extractor');
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
extractEventData,
|
|
11
|
+
processEventData,
|
|
12
|
+
extractProperties,
|
|
13
|
+
extractInterfaceProperties
|
|
14
|
+
};
|