2

Is there a way to geth autocompletion with IntelliSense in Visual Studio Code work properly, when I read a json file and cast it to a PowerShell like:

$config = Get-Content "SOME_PATH" | ConvertFrom-Json
$config.attribute1 

The problem is that the file needs to be in memory before it can get the structure off the json file and propose attributes.

If I get the code out and execute it in the powershell terminal and then go back to the code editor, the autocompletion works fine.

nailuenlue
  • 61
  • 2
  • 7

2 Answers2

2

Currently IntelliSense doesn't do this.

Some of the reasons are because when you are coding, often times the specified file could not exist, be a different test version than expected, the file could be huge, malformed, etc. By default, it is sometimes safer to just not do this.

By running it in the terminal and loading it in memory, you are explicitly telling IntelliSense what you are using, and then it now "knows" about the object, and can then properly suggest the correct properties and attributes.

As @mklement0 suggests, using the keyboard shortcut F8 will conveniently execute the current line/selection in the integrated terminal, which would load the object into memory, and allow you to use IntelliSense in the editor.

HAL9256
  • 12,384
  • 1
  • 34
  • 46
  • It's just very inconvenient as I would like to load a config file into a PowerShell object in a Powershell module and return it back for usage. The caller of the module now needs to know the structure of the JSON file or to use F8 to call a "Get-Config" and then continue writing the code that relies on that config object. – nailuenlue Apr 03 '19 at 17:24
2

To complement HAL9256's helpful answer:

First, some background information; find a working solution in the bottom section.

IntelliSense for variables in Visual Studio Code works if their type is either:

  • explicitly declared (e.g., [datetime] $var = ...)
  • or can be inferred from an assigned value.

If an assignment is based on a command (cmdlet, function, script) call, the type can only be inferred from commands with explicitly defined output type(s):

  • many, but by no means all, cmdlets do declare their output types
  • functions and scripts must use the [OutputType(<type>)] attribute.

Additionally, with the nondescript [pscustomobject] type returned by ConvertFrom-Json - which has no inherent properties, only those you add on demand; a "property bag" - you only get IntelliSense:

  • if a variable was assigned from a custom-object literal (e.g., $var = [pscustomobject] @{ one = 1; two = 2 })
  • if you cast a custom object to a specific type, assuming instances of that type can be constructed from the custom object's properties, something that PowerShell makes easy - see this answer.

Solution with a custom class (PSv5+)

Visual Studio Code's IntelliSense (via the PowerShell Extension) does recognize the members of instances of PS custom classes defined via the PSv5+ class statement.

You can therefore use a custom class to mirror the structure of the JSON object you're loading and convert the [pscustomobject] "property bags" returned by ConvertFrom-Json to an instance of that class by way of a cast.

Note: The inherent limitation of this approach is that your class must anticipate all property names that the underlying JSON objects contain, and the two must be kept in sync; otherwise:

  • if a property name changes on the JSON side, your code will break if the class definition isn't updated accordingly.
  • if new properties are added on the JSON side, these will be inaccessible unless the class definition is updated accordingly.

class definitions can either be:

  • directly embedded in a script
  • imported from modules via the using module statement (note that using Import-Module does not load a module's classes).

To implement the solution at hand, you can use a class definition in one of two ways:

  • (a) Define a class directly in your script that matches the structure of the JSON objects, and cast the [pscustomobject] instance returned from ConvertFrom-Json to that type; a variable assigned this way supports IntelliSense.

  • (b) Wrap the JSON-loading functionality in a module that performs the above inside the module, and passes the class instance out from a function that declares its [OutputObject()] to be of that type; code that imports that module with using module will then get IntelliSense for variables that capture the output from that function.

A simple demonstration of (a):

# Define a class whose properties mirror the underlying JSON.
class Config {
  $foo
  $bar
}

# Load the JSON and cast the resulting [pscustomobject] to the class.
# Note: This cast only works if the JSON object's set of properties 
#       is either the same as that of the [Config] type or a subset of it.
[Config] $config = '{ "foo": "bar", "bar": 42 }' | ConvertFrom-Json

# Variable $config supports IntelliSense, because its is now known
# as type Config.
$config. # shows list of properties
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Thanks a lot for taking the time but the Class approach was known to me but maintaining a config file and a class definition is not an option as I prefer to being forced to check the configuration file (the single place to add/remove configuration) rather then keep those to sources in sync. – nailuenlue Apr 05 '19 at 20:35
  • Understood and understandable, @nailuenlue, but I'm not sure there's a good solution for that. The closest thing to what you want is to have some external process that runs periodically or monitors changes to the config file (re)create a class definition from the JSON and patch that into the module source code; a simple example: `" class Config { $((Get-Content some.json | ConvertFrom-Json)[0].ForEach({ $_.psobject.properties.name }) -replace '^', '$' -replace '$', "\`n") } "` – mklement0 Apr 05 '19 at 21:02