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

nshkrdotcom / ElixirScope / 263b75bcaeda1a9017c5ab33429630d229751173

29 May 2025 05:17PM UTC coverage: 58.515% (+0.3%) from 58.208%
263b75bcaeda1a9017c5ab33429630d229751173

push

github

NSHkr
perf test and mem mgr 1087 tests, 3 failures, 76 excluded

13 of 26 new or added lines in 4 files covered. (50.0%)

5 existing lines in 3 files now uncovered.

6082 of 10394 relevant lines covered (58.51%)

3193.89 hits per line

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

26.67
/lib/elixir_scope/ast_repository/enhanced_module_data.ex
1
defmodule ElixirScope.ASTRepository.EnhancedModuleData do
2
  @moduledoc """
3
  Comprehensive module representation with full AST and analysis metadata.
4

5
  This structure extends the basic ModuleData to support advanced AST analysis,
6
  comprehensive dependency tracking, and integration with CFG/DFG/CPG analysis.
7

8
  ## Features
9
  - Complete AST storage with size and depth metrics
10
  - Enhanced dependency tracking (imports, aliases, requires, uses)
11
  - OTP pattern detection and callback analysis
12
  - Comprehensive complexity metrics and code quality analysis
13
  - Security risk assessment
14
  - Efficient serialization for ETS storage
15

16
  ## Usage
17

18
      iex> module_data = EnhancedModuleData.new(MyModule, ast, source_file: "lib/my_module.ex")
19
      iex> EnhancedModuleData.validate(module_data)
20
      :ok
21
  """
22

23
  alias ElixirScope.ASTRepository.Enhanced.{
24
    EnhancedFunctionData,
25
    ComplexityMetrics
26
  }
27
  alias ElixirScope.ASTRepository.Enhanced.SupportingStructures.{
28
    MacroData,
29
    TypespecData,
30
    ModuleDependency,
31
    BehaviourUsage,
32
    CallbackData,
33
    ChildSpecData,
34
    CodeSmell,
35
    SecurityRisk
36
  }
37

38
  @type t :: %__MODULE__{
39
    # Identity
40
    module_name: atom(),
41
    file_path: String.t(),
42
    file_hash: String.t(),  # SHA256 for change detection
43

44
    # AST Data
45
    ast: Macro.t(),  # Complete module AST
46
    ast_size: non_neg_integer(),  # Number of AST nodes
47
    ast_depth: non_neg_integer(),  # Maximum AST depth
48

49
    # Module Components
50
    functions: [EnhancedFunctionData.t()],
51
    macros: [MacroData.t()],
52
    module_attributes: %{atom() => term()},
53
    typespecs: [TypespecData.t()],
54

55
    # Dependencies & Relationships
56
    imports: [ModuleDependency.t()],
57
    aliases: [ModuleDependency.t()],
58
    requires: [ModuleDependency.t()],
59
    uses: [BehaviourUsage.t()],
60

61
    # OTP Patterns
62
    behaviours: [atom()],
63
    callbacks_implemented: [CallbackData.t()],
64
    child_specs: [ChildSpecData.t()],
65

66
    # Analysis Metadata
67
    complexity_metrics: ComplexityMetrics.t(),
68
    code_smells: [CodeSmell.t()],
69
    security_risks: [SecurityRisk.t()],
70

71
    # Timestamps
72
    last_modified: DateTime.t(),
73
    last_analyzed: DateTime.t(),
74

75
    # Extensible Metadata
76
    metadata: map()
77
  }
78

79
  defstruct [
80
    # Identity
81
    :module_name,
82
    :file_path,
83
    :file_hash,
84

85
    # AST Data
86
    :ast,
87
    :ast_size,
88
    :ast_depth,
89

90
    # Module Components
91
    :functions,
92
    :macros,
93
    :module_attributes,
94
    :typespecs,
95

96
    # Dependencies & Relationships
97
    :imports,
98
    :aliases,
99
    :requires,
100
    :uses,
101

102
    # OTP Patterns
103
    :behaviours,
104
    :callbacks_implemented,
105
    :child_specs,
106

107
    # Analysis Metadata
108
    :complexity_metrics,
109
    :code_smells,
110
    :security_risks,
111

112
    # Timestamps
113
    :last_modified,
114
    :last_analyzed,
115

116
    # Extensible Metadata
117
    :metadata
118
  ]
119

120
  @doc """
121
  Creates a new EnhancedModuleData structure from parsed AST.
122

123
  ## Parameters
124
  - `module_name` - The module name (atom)
125
  - `ast` - The parsed AST
126
  - `opts` - Optional parameters including file_path, functions, etc.
127

128
  ## Examples
129

130
      iex> ast = quote do: defmodule MyModule, do: def hello, do: :world
131
      iex> data = EnhancedModuleData.new(MyModule, ast, file_path: "lib/my_module.ex")
132
      iex> data.module_name
133
      MyModule
134
  """
135
  @spec new(atom(), Macro.t(), keyword()) :: t()
136
  def new(module_name, ast, opts \\ []) do
137
    now = DateTime.utc_now()
2,950✔
138
    file_path = Keyword.get(opts, :file_path, "")
2,950✔
139

140
    %__MODULE__{
2,950✔
141
      # Identity
142
      module_name: module_name,
143
      file_path: file_path,
144
      file_hash: generate_file_hash(ast, file_path),
145

146
      # AST Data
147
      ast: ast,
148
      ast_size: calculate_ast_size(ast),
149
      ast_depth: calculate_ast_depth(ast),
150

151
      # Module Components
152
      functions: Keyword.get(opts, :functions, []),
153
      macros: extract_macros(ast),
154
      module_attributes: extract_module_attributes(ast),
155
      typespecs: extract_typespecs(ast),
156

157
      # Dependencies & Relationships
158
      imports: extract_imports(ast),
159
      aliases: extract_aliases(ast),
160
      requires: extract_requires(ast),
161
      uses: extract_uses(ast),
162

163
      # OTP Patterns
164
      behaviours: extract_behaviours(ast),
165
      callbacks_implemented: extract_callbacks_implemented(ast),
166
      child_specs: extract_child_specs(ast),
167

168
      # Analysis Metadata
169
      complexity_metrics: calculate_complexity_metrics(ast),
170
      code_smells: detect_code_smells(ast),
171
      security_risks: assess_security_risks(ast),
172

173
      # Timestamps
174
      last_modified: now,
175
      last_analyzed: now,
176

177
      # Extensible Metadata
178
      metadata: Keyword.get(opts, :metadata, %{})
179
    }
180
  end
181

182
  @doc """
183
  Validates the structure and data integrity of an EnhancedModuleData.
184

185
  ## Returns
186
  - `:ok` if valid
187
  - `{:error, reason}` if invalid
188
  """
189
  @spec validate(t()) :: :ok | {:error, term()}
190
  def validate(%__MODULE__{} = data) do
191
    with :ok <- validate_required_fields(data),
×
192
         :ok <- validate_ast_consistency(data),
×
193
         :ok <- validate_dependencies(data),
×
194
         :ok <- validate_timestamps(data) do
×
195
      :ok
196
    end
197
  end
198

199
  @doc """
200
  Converts the structure to a format suitable for ETS storage.
201

202
  This function optimizes the data for storage by:
203
  - Compressing large AST structures
204
  - Converting datetime to timestamps
205
  - Optimizing nested structures
206
  """
207
  @spec to_ets_format(t()) :: {atom(), binary()}
208
  def to_ets_format(%__MODULE__{} = data) do
209
    compressed_data = %{
×
210
      module_name: data.module_name,
×
211
      file_path: data.file_path,
×
212
      file_hash: data.file_hash,
×
213
      ast_compressed: compress_ast(data.ast),
×
214
      ast_size: data.ast_size,
×
215
      ast_depth: data.ast_depth,
×
216
      functions_count: length(data.functions),
×
217
      macros_count: length(data.macros),
×
218
      complexity_score: get_complexity_score(data.complexity_metrics),
×
219
      last_modified_ts: DateTime.to_unix(data.last_modified),
×
220
      last_analyzed_ts: DateTime.to_unix(data.last_analyzed),
×
221
      metadata: data.metadata
×
222
    }
223

224
    {data.module_name, :erlang.term_to_binary(compressed_data, [:compressed])}
×
225
  end
226

227
  @doc """
228
  Converts from ETS storage format back to full structure.
229
  """
230
  @spec from_ets_format({atom(), binary()}) :: t()
231
  def from_ets_format({module_name, binary_data}) do
232
    compressed_data = :erlang.binary_to_term(binary_data)
×
233

234
    %__MODULE__{
×
235
      module_name: module_name,
236
      file_path: compressed_data.file_path,
×
237
      file_hash: compressed_data.file_hash,
×
238
      ast: decompress_ast(compressed_data.ast_compressed),
×
239
      ast_size: compressed_data.ast_size,
×
240
      ast_depth: compressed_data.ast_depth,
×
241
      functions: [],  # Loaded separately for performance
242
      macros: [],
243
      module_attributes: %{},
244
      typespecs: [],
245
      imports: [],
246
      aliases: [],
247
      requires: [],
248
      uses: [],
249
      behaviours: [],
250
      callbacks_implemented: [],
251
      child_specs: [],
252
      complexity_metrics: %ComplexityMetrics{score: compressed_data.complexity_score},
×
253
      code_smells: [],
254
      security_risks: [],
255
      last_modified: DateTime.from_unix!(compressed_data.last_modified_ts),
×
256
      last_analyzed: DateTime.from_unix!(compressed_data.last_analyzed_ts),
×
257
      metadata: compressed_data.metadata
×
258
    }
259
  end
260

261
  @doc """
262
  Updates the module with new function data.
263
  """
264
  @spec add_function(t(), EnhancedFunctionData.t()) :: t()
265
  def add_function(%__MODULE__{} = data, function_data) do
NEW
266
    %{data |
×
267
      functions: [function_data | data.functions],
×
268
      last_analyzed: DateTime.utc_now()
269
    }
270
  end
271

272
  @doc """
273
  Gets all function names and arities for this module.
274
  """
275
  @spec get_function_signatures(t()) :: [{atom(), non_neg_integer()}]
276
  def get_function_signatures(%__MODULE__{} = data) do
277
    Enum.map(data.functions, fn func -> {func.function_name, func.arity} end)
×
278
  end
279

280
  @doc """
281
  Checks if the module has been modified since last analysis.
282
  """
283
  @spec needs_reanalysis?(t(), String.t()) :: boolean()
284
  def needs_reanalysis?(%__MODULE__{} = data, current_file_hash) do
285
    data.file_hash != current_file_hash
×
286
  end
287

288
  @doc """
289
  Gets the total complexity score for the module.
290
  """
291
  @spec get_total_complexity(t()) :: float()
292
  def get_total_complexity(%__MODULE__{} = data) do
293
    module_complexity = get_complexity_score(data.complexity_metrics)
×
NEW
294
    function_complexity =
×
295
      data.functions
×
296
      |> Enum.map(& &1.complexity_score)
×
297
      |> Enum.sum()
298

299
    module_complexity + function_complexity
×
300
  end
301

302
  @doc """
303
  Migrates from basic ModuleData to EnhancedModuleData.
304
  """
305
  @spec from_module_data(ElixirScope.ASTRepository.ModuleData.t()) :: t()
306
  def from_module_data(module_data) do
307
    now = DateTime.utc_now()
×
308

309
    %__MODULE__{
×
310
      module_name: module_data.module_name,
×
311
      file_path: module_data.source_file || "",
×
312
      file_hash: module_data.compilation_hash || "",
×
313
      ast: module_data.ast,
×
314
      ast_size: calculate_ast_size(module_data.ast),
×
315
      ast_depth: calculate_ast_depth(module_data.ast),
×
316
      functions: [],  # Will be populated separately
317
      macros: [],
318
      module_attributes: %{},
319
      typespecs: [],
320
      imports: [],
321
      aliases: [],
322
      requires: [],
323
      uses: [],
324
      behaviours: [],
325
      callbacks_implemented: [],
326
      child_specs: [],
327
      complexity_metrics: migrate_complexity_metrics(module_data.complexity_metrics),
×
328
      code_smells: [],
329
      security_risks: [],
330
      last_modified: DateTime.from_unix!(module_data.updated_at, :microsecond),
×
331
      last_analyzed: now,
332
      metadata: %{
333
        migrated_from: "ModuleData",
334
        migration_timestamp: DateTime.to_iso8601(now),
335
        original_version: module_data.version
×
336
      }
337
    }
338
  end
339

340
  # Private helper functions
341

342
  defp generate_file_hash(ast, file_path) do
343
    content = "#{inspect(ast)}#{file_path}"
2,950✔
344
    :crypto.hash(:sha256, content) |> Base.encode16(case: :lower)
2,950✔
345
  end
346

347
  defp calculate_ast_size(ast) do
348
    ast
349
    |> Macro.prewalk(0, fn node, acc -> {node, acc + 1} end)
74,585✔
350
    |> elem(1)
2,950✔
351
  end
352

353
  defp calculate_ast_depth(ast) do
354
    ast
355
    |> Macro.prewalk(0, fn
356
      {_, _, children}, depth when is_list(children) ->
357
        max_child_depth = children
5,900✔
358
        |> Enum.map(fn child -> calculate_ast_depth(child) end)
8,850✔
359
        |> Enum.max(fn -> 0 end)
×
360
        {{}, depth + 1 + max_child_depth}
361
      _, depth ->
5,900✔
362
        {{}, depth}
363
    end)
364
    |> elem(1)
11,800✔
365
  end
366

367
  defp extract_macros(_ast), do: []  # TODO: Implement macro extraction
2,950✔
368
  defp extract_module_attributes(_ast), do: %{}  # TODO: Implement attribute extraction
2,950✔
369
  defp extract_typespecs(_ast), do: []  # TODO: Implement typespec extraction
2,950✔
370
  defp extract_imports(_ast), do: []  # TODO: Implement import extraction
2,950✔
371
  defp extract_aliases(_ast), do: []  # TODO: Implement alias extraction
2,950✔
372
  defp extract_requires(_ast), do: []  # TODO: Implement require extraction
2,950✔
373
  defp extract_uses(_ast), do: []  # TODO: Implement use extraction
2,950✔
374
  defp extract_behaviours(_ast), do: []  # TODO: Implement behaviour extraction
2,950✔
375
  defp extract_callbacks_implemented(_ast), do: []  # TODO: Implement callback extraction
2,950✔
376
  defp extract_child_specs(_ast), do: []  # TODO: Implement child spec extraction
2,950✔
377

378
  defp calculate_complexity_metrics(_ast) do
379
    %ComplexityMetrics{
2,950✔
380
      score: 1.0,
381
      cyclomatic: 1,
382
      cognitive: 1,
383
      halstead: %{},
384
      maintainability_index: 100.0
385
    }
386
  end
387

388
  defp detect_code_smells(_ast), do: []  # TODO: Implement code smell detection
2,950✔
389
  defp assess_security_risks(_ast), do: []  # TODO: Implement security risk assessment
2,950✔
390

391
  defp validate_required_fields(%__MODULE__{module_name: nil}), do: {:error, :missing_module_name}
×
392
  defp validate_required_fields(%__MODULE__{ast: nil}), do: {:error, :missing_ast}
×
393
  defp validate_required_fields(_), do: :ok
×
394

395
  defp validate_ast_consistency(%__MODULE__{ast_size: size}) when size <= 0, do: {:error, :invalid_ast_size}
×
396
  defp validate_ast_consistency(%__MODULE__{ast_depth: depth}) when depth <= 0, do: {:error, :invalid_ast_depth}
×
397
  defp validate_ast_consistency(_), do: :ok
×
398

399
  defp validate_dependencies(_), do: :ok  # TODO: Implement dependency validation
×
400

401
  defp validate_timestamps(%__MODULE__{last_modified: nil}), do: {:error, :missing_last_modified}
×
402
  defp validate_timestamps(%__MODULE__{last_analyzed: nil}), do: {:error, :missing_last_analyzed}
×
403
  defp validate_timestamps(_), do: :ok
×
404

405
  defp compress_ast(ast) do
406
    ast
407
    |> :erlang.term_to_binary([:compressed])
408
    |> Base.encode64()
×
409
  end
410

411
  defp decompress_ast(compressed_string) do
412
    compressed_string
413
    |> Base.decode64!()
414
    |> :erlang.binary_to_term()
×
415
  end
416

417
  defp get_complexity_score(%ComplexityMetrics{score: score}), do: score
×
418
  defp get_complexity_score(_), do: 1.0
×
419

420
  defp migrate_complexity_metrics(nil) do
421
    %ComplexityMetrics{score: 1.0, cyclomatic: 1, cognitive: 1, halstead: %{}, maintainability_index: 100.0}
×
422
  end
423
  defp migrate_complexity_metrics(old_metrics) do
424
    %ComplexityMetrics{
×
425
      score: Map.get(old_metrics, :score, 1.0),
426
      cyclomatic: Map.get(old_metrics, :cyclomatic, 1),
427
      cognitive: Map.get(old_metrics, :cognitive, 1),
428
      halstead: Map.get(old_metrics, :halstead, %{}),
429
      maintainability_index: Map.get(old_metrics, :maintainability_index, 100.0)
430
    }
431
  end
432
end
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